# Flow E — Stealth Request

**Lunar · experimental.** A P2P request (like [Flow B](/docs/flow-b-request.md)) that settles to a one-time **ERC-5564 stealth address** — nothing on-chain links the payment to the recipient. This is *confidential receiving*, not anonymity: privacy holds against chain observers and the payer, **never** against the operator (which stores the announcement). Native USDC only; the ed25519 flavor additionally awaits an external cryptography review before mainnet.

> **Drop-in first, headless below.** The `createStealthRequest` primitive, the `useStealthRequest` hook, and recover & withdraw are in the [Headless](/docs/flow-e-stealth-request.md) section at the bottom of this page.

## How it differs from Flow B

A `stealth_request` session is **barred from open-token mode**, so it does **not** commit an intent at creation the way Flow B does. Instead it rides **detect-then-sign**: the session's burner signs a bounded Intent to the stealth destination when a deposit is detected — so **the recipient must be online** when the payment arrives (unlike Flow B, which settles with no one online). The stealth address is derived client-side from the recipient's meta-address; the announcement (`ephemeral_pubkey` + `view_tag`) is stored so the key can be recovered later.

## Drop-in (`StealthRequestWidget`)

`StealthRequestWidget` (in `@peltier/react-native`) is the native drop-in — structurally the `DepositWidget` (payers scan the burner **deposit** address), but the settled USDC lands at a one-time stealth address only the recipient can spend from. Its engine is the `useStealthRequest` hook.

```typescript
import { PeltierProvider, generateStealthMeta } from '@peltier/react-native'
import { StealthRequestWidget } from '@peltier/react-native'
import QRCode from 'react-native-qrcode-svg'
import * as Clipboard from 'expo-clipboard'

// Generate + PERSIST the recipient's stealth keys. Losing them strands the funds —
// there is no server-side spend key. Use generateStealthMetaSol() for a Solana destination.
const keys = generateStealthMeta()   // { spendPrivkey, viewPrivkey, meta }

<PeltierProvider apiUrl="https://api.peltier.dev" publishableKey="pk_live_…">
  <StealthRequestWidget
    keys={keys}
    destinationChainId={8453}
    renderQr={(deposit) => <QRCode value={deposit} size={200} />}
    onCopyAddress={(deposit) => Clipboard.setStringAsync(deposit)}
    onComplete={(sessionId) => console.log('Received:', sessionId)}
  />
</PeltierProvider>
```

Payers scan the burner **deposit** address; USDC settles to the one-time stealth address. Because it's detect-then-sign, keep the widget mounted until the deposit lands. After settlement, recover the one-time key from your persisted `keys` + the stored announcement and cash out — see the [Headless](/docs/flow-e-stealth-request.md) section below.

> **This flow is web-absent by design.** The stealth widget ships only in `@peltier/react-native`; on the web, drive Flow E headless with `createStealthRequest` + `useStealthRequest`.

## Headless

**Experimental** — native USDC only, and the ed25519 (Solana) flavor still awaits an external cryptography review.

### Hook (`useStealthRequest`)

`useStealthRequest({ apiUrl, implementationAddress, keys, destinationChainId })` (from `@peltier/react-native`) wraps derive → register → the detect-then-sign commit, and exposes `burnerAddress`, `stealthAddress`, `announcement`, `status`, `error`, `connectionState`, `retryCommit`, `reset`, and `recoverKey()` → `{ stealthPrivkey, stealthAddress }` (the local one-time-key recovery). `error` is a structured `PeltierError` or `null`. `StealthRequestWidget` is just its native rendering — show `burnerAddress` to the payer, watch `status`, and call `recoverKey()` once it settles.

### Primitive (`createStealthRequest`)

Off React, `createStealthRequest` runs the same derive → register the widget's engine does. You mint the session burner; it derives a fresh stealth address for the recipient's own meta-address and pins the session to it, storing the announcement.

```typescript
import {
  createBurnerWallet, createStealthRequest, generateStealthMeta,
} from '@peltier/react-native'

const apiUrl = 'https://api.peltier.dev'
const impl = '0x33333A781cbe9aC82Ba510BfF7b26c47a8FDecD4'

// Generate + PERSIST once — there is no server-side spend key, so losing these strands the funds.
const keys = generateStealthMeta()               // { spendPrivkey, viewPrivkey, meta }
const burner = await createBurnerWallet(impl)     // the session's throwaway EIP-7702 signer

const { session, stealthAddress, announcement } = await createStealthRequest({
  apiUrl,
  burner,
  destinationChainId: 8453,      // SOLANA_CHAIN_ID + generateStealthMetaSol() for a Solana destination
  meta: keys.meta,               // the recipient's OWN stealth meta-address
})

// Show `burner.address` to the payer (the DEPOSIT address). Persist { keys, announcement } —
// USDC settles to `stealthAddress`, and only that pair recovers the one-time key later.
```

Because a `stealth_request` is **detect-then-sign**, `createStealthRequest` only *registers* — the burner must stay online to sign the settlement Intent when the deposit lands (drive that leg with `useDepositSession`, which `useStealthRequest` already wraps).

### Recover & withdraw

After settlement, recover the one-time key from your persisted `keys` + the announcement, then cash out:

```typescript
// Solana — one call (fee-payer separation; the sweep needs no gas from you):
import { computeStealthKeySol, sweepSolanaStealth } from '@peltier/core'
const { stealthPrivkey } = computeStealthKeySol(keys.spendPrivkey, keys.viewPrivkey, announcement.ephemeralPubkey)
const { tx_hash } = await sweepSolanaStealth(apiUrl, stealthPrivkey, myWallet)

// EVM — gasless relay via the StealthForwarder. Recover the key, sign the forwarder
// Withdraw + the USDC EIP-3009 authorization with it, then relay:
import { computeStealthKey, submitStealthWithdraw } from '@peltier/core'
const { stealthPrivkey, stealthAddress } = computeStealthKey(keys.spendPrivkey, keys.viewPrivkey, announcement.ephemeralPubkey)
await submitStealthWithdraw(apiUrl, {
  chainId: 8453, from: stealthAddress, to: myWallet, value, fee, nonce,
  deadline, validAfter, validBefore, withdrawSig, sig3009,
})
```

The relay can only choose *when* — the destination and exact fee are pinned inside both signatures and re-verified on-chain, so a compromised relayer cannot redirect or over-charge.

## Notes & caveats

- **No standing-refund escape hatch on the create path.** `createStealthRequest` deliberately calls `register` with no `standing_refund`. Combined with detect-then-sign (the recipient must be online for the commit), a stranded `stealth_request` must be recovered manually via a burner-signed `POST /sessions/:id/refund` while the burner key is still in hand.
- **Announcement recovery.** The announcement (`ephemeral_pubkey` + `view_tag`) is stored **redundantly** — client-side *and* server-side. If the client copy is lost, `getStealthAnnouncement(apiUrl, sessionId)` (→ `GET /sessions/:id/stealth`, no auth for anonymous Flow E) returns the stored announcements. Losing **every** copy of the ephemeral pubkey strands the funds — there is no server-side spend key.
