# Flow D — Stealth Checkout

**Lunar · experimental.** Merchant checkout ([Flow A](/docs/flow-a-checkout.md)) that settles each session to a **fresh one-time ERC-5564 stealth address** derived server-side from the merchant's registered meta-address. The payer runs an ordinary checkout — all stealth derivation is backend-side — so there is **no new payer-facing API**. Same privacy model as [Flow E](/docs/flow-e-stealth-request.md): confidential vs chain observers + the payer, never vs the operator. Native USDC only.

Unlike Flow E, **Flow D is not a `flow` value** — it is a `stealth: true` flag on `createPaymentSession`.

> **Drop-in first, headless below.** The server-side register-meta → create-session → recover-and-withdraw sequence is in the [Headless](/docs/flow-d-stealth-checkout.md) section at the bottom of this page.

## What your server does (in brief)

1. **Register your stealth meta-address once** (public keys only; keep the spend keys cold) via `setMerchantStealthMeta` → `PUT /v1/stealth-meta`.
2. **Create the session with `stealth: true`.** `createPaymentSession(apiUrl, sk, { amount, stealth: true })` returns `{ sessionId, clientSecret }` **plus** the derived one-time destination(s): `session.stealth` (secp/EVM) and, when a Solana meta is registered, `session.stealthSolana` (ed25519).

Both steps are server-side code — see the [Headless](/docs/flow-d-stealth-checkout.md) section below for the full listing (and for recover & withdraw after settlement).

## Drop-in (`<PeltierCheckout>`)

The same `<PeltierCheckout>` as Flow A. The **one** Flow-D-specific detail: set `destinationAddress` / `destinationChainId` to the **derived one-time address** the create call returned (`session.stealth.stealthAddress`). The burner signs that into the Intent, and the commit rejects any other destination — the stealth address **overrides** your destinations table for this session, so you neither pin nor pass your own address here.

```typescript
import { PeltierProvider, PeltierCheckout } from '@peltier/react'
import '@peltier/react/styles.css'

// server: const session = await createPaymentSession(apiUrl, sk, { amount: '1000000', stealth: true })
<PeltierProvider apiUrl="https://api.peltier.dev" publishableKey="pk_live_…">
  <PeltierCheckout
    sessionId={session.sessionId}
    clientSecret={session.clientSecret}
    destinationAddress={session.stealth.stealthAddress}   // the one-time address — MUST match or the commit is rejected
    destinationChainId={8453}
    onComplete={(id) => console.log('settled', id)}
  />
</PeltierProvider>
```

The payer sees a normal any-token checkout (burner → detect → commit); nothing on screen reveals it is stealth. To settle on Solana, pass `session.stealthSolana.stealthAddress` with `destinationChainId={7565164}` instead (requires a Solana meta registered first).

> There is no Flow-D-specific payer hook — `<PeltierCheckout>` wraps `<DepositWidget>` over `useDepositSession`, the same engine Flow A uses. The stealth surface is entirely server-side.

## Headless

All of Flow D is headless on your server; the payer side is an ordinary `<PeltierCheckout>` pointed at the derived one-time address (above). **Experimental · native USDC only.**

### 1. Register the merchant meta-address (once)

Generate the stealth keypair on a trusted device and register only the **public** keys with your secret key. Keep the spend private keys **cold** — they alone can spend settled funds.

```typescript
import {
  generateStealthMeta, generateStealthMetaSol, setMerchantStealthMeta,
} from '@peltier/node'

const evm = generateStealthMeta()      // { spendPrivkey, viewPrivkey, meta } — store privkeys cold
const sol = generateStealthMetaSol()   // optional Solana (ed25519) leg — both keys or neither

await setMerchantStealthMeta(apiUrl, secretKey, {
  spendPubkey: evm.meta.spendPubkey,
  viewPubkey: evm.meta.viewPubkey,
  solSpendPubkey: sol.meta.spendPubkey,   // omit both for EVM-only
  solViewPubkey: sol.meta.viewPubkey,
})   // → PUT /v1/stealth-meta (sk_) → { ok: true, solana: true }
```

### 2. Create a stealth checkout session

Pass `stealth: true`. The backend derives the one-time destination(s) at creation and returns them; hand `{ sessionId, clientSecret }` + your publishable key to the browser exactly as with a normal checkout.

```typescript
import { createPaymentSession } from '@peltier/node'

const session = await createPaymentSession(apiUrl, secretKey, {
  amount: '1000000',   // 1 USDC (base units)
  stealth: true,
})
// session.stealth        → { stealthAddress, ephemeralPubkey, viewTag, scheme } (secp / EVM)
// session.stealthSolana  → the ed25519 leg, when a Solana meta was registered
```

Mount `<PeltierCheckout>` with `destinationAddress = session.stealth.stealthAddress` — see the drop-in section above.

### 3. Recover & withdraw

The one-time destination + its announcement come back on the create response (and from `getStealthAnnouncement(apiUrl, sessionId, { clientSecret, publishableKey })`). Recover the key with the merchant's stored spend/view keys, then withdraw — `submitStealthWithdraw` on EVM, `sweepSolanaStealth` on Solana (the exact withdraw calls are in [Flow E's Headless section](/docs/flow-e-stealth-request.md)).

```typescript
import { computeStealthKey } from '@peltier/node'

const { stealthPrivkey } = computeStealthKey(
  evm.spendPrivkey, evm.viewPrivkey, session.stealth.ephemeralPubkey,
)
// → submitStealthWithdraw(apiUrl, { chainId, from: session.stealth.stealthAddress, to, … })
```

## Notes & caveats

- **The stealth destination overrides your pinned destinations at commit.** A `stealth: true` session settles to the backend-derived one-time address, which overrides the destinations table for that session. You do not (and must not) pin the stealth address yourself.
- **Register the Solana meta *before* creating sessions you want to settle on Solana.** Sessions created before you register `sol_spend_pubkey` / `sol_view_pubkey` carry only the secp/EVM leg and cannot settle to Solana — register the Solana keys first (or re-create the session afterward).
- **Scanning helpers.** `checkAnnouncement` (secp) and `checkAnnouncementSol` (ed25519) test whether a stored announcement belongs to you using the `view_tag` as a cheap pre-filter before the full shared-secret derivation — useful when reconciling a batch of announcements against your view key.
