# Flow C — Push / Send

Flow C is the **connected-wallet** path: a sender connects their own wallet and pushes any token → USDC — **no burner, no EIP-7702, no relayer**. The sender signs and submits their own transaction(s) and pays their own gas. Non-custodial by construction: the platform never touches funds and never broadcasts. Flow C is stateless server-side (no DB writes).

> **Drop-in first, headless below.** The raw `fetchBalances` / `fetchQuote` / calldata-verification functions and the `useSendFromWallet` hook are in the **Headless** section at the bottom of this page.

> **Availability (pre-mainnet).** The per-chain **Universal Router** address is still a zero placeholder in `chains.rs`. Because the client and server both *fail closed* rather than sign against an unpinnable `tx.to`, **any path with a swap leg currently returns an error** — that is every same-chain quote and every cross-chain quote from a non-USDC source. The only path that works end-to-end today is **cross-chain USDC → USDC**. Wiring the real router addresses is a documented pre-mainnet item. (Consolidated view: the [availability matrix](/docs/availability.md).)

## Recipient semantics (read this first)

Who receives the USDC depends on whether the quote crosses chains, because a same-chain Uniswap swap can only ever deliver to the **swapper** (the connected wallet):

- **Same-chain (`src == dest`) — send-to-self only.** The swap delivers USDC back to the sender's own wallet. Passing a third-party `recipient` here is **rejected with `400`** (FC-005). Omit `recipient` (or set it to the wallet).
- **Cross-chain (`src != dest`) — push to anyone.** The Across bridge leg pins `depositV3.recipient`, so USDC settles to the chosen `recipient` on the destination chain. This is the supported third-party push.

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

`<PeltierSend>` lists the wallet's held tokens (`GET /balances`), the payer picks one + an amount, and it quotes → verifies → approves → sends, settling USDC to the fixed `recipient`. The payer never chooses the destination.

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

// The connected wallet (EIP-6963 / window.ethereum), bridged to an EIP-1193 provider.
const provider = window.ethereum as Eip1193Provider

<PeltierProvider apiUrl="https://api.peltier.dev" publishableKey="pk_live_…">
  <PeltierSend
    provider={provider}
    recipient="0xMerchantOrRecipient…"   // where the USDC settles (fixed — the payer never picks it)
    allowedRouters={ROUTERS}             // REQUIRED: real Universal Router / Across SpokePool addresses; an empty list fails closed (FC-001)
    onComplete={(txHash) => console.log('sent', txHash)}
  />
</PeltierProvider>
```

`allowedRouters` is **required**: it's the whitelist of acceptable `tx.to` targets. An empty/omitted list **fails closed** — the wallet is never prompted. `minOutTolerance` defaults to `0.01` (1%).

**React Native.** The same flow ships as `<SendWidget>` in `@peltier/react-native`, driven by any EIP-1193 provider (WalletConnect's RN provider on mobile), with the same fail-closed calldata verification. See [React Native](/docs/react-native.md).

## How settlement works

Under the hood the drop-in runs the four-step engine — pull payable balances → quote → **mandatory client-side calldata verification** (recipient + `minOut` + router whitelist) → approve(EXACT) → multi-leg send → poll. A compromised backend cannot redirect funds because the client refuses to sign mismatched calldata. Flow C settles *directly* through Uniswap/Across (it does **not** route through `PeltierDelegate`), so it takes **no platform fee and no app fee** — `fee_usd` in the preview is the user's real cost (Across relayer fee + swap cost). The full quote-proxy contract, the ordered `transactions` plan, and the per-leg verification rules follow below.

## Headless

Every request is a plain exported function and every response type is exported — this is exactly what `<PeltierSend>` calls under the hood.

### Quote proxy (server-only Uniswap key)

The backend `POST /quote` endpoint is the **only** path to Uniswap — the `UNISWAP_API_KEY` is server-only and the client never calls Uniswap directly. The proxy is public and IP-rate-limited. It wraps the Uniswap quote + swap calldata (plus the Across bridge legs when cross-chain), **verifies the output recipient in the decoded calldata**, and returns Uniswap's calldata *as-is*.

```json
// POST /quote  (public · IP-rate-limited)
{
  "wallet": "0xSender…",       // connected sender wallet (the swapper)
  "src_chain_id": 8453,        // chain the sender holds the token on
  "token": "0xToken…",         // token the sender sends (in)
  "amount": "1000000000",      // input amount (uint base-units string)
  "dest_chain_id": 42161,      // chain to settle USDC on
  "recipient": "0xRecipient…"  // OPTIONAL — defaults to `wallet`. Third-party recipient only when src != dest.
}
```

The response carries an **ordered execution plan** (`transactions`), one step per on-chain tx the sender submits, in order:

- **Same-chain:** `[swap]` — token → USDC delivered to the wallet.
- **Cross-chain, non-USDC source:** `[swap, bridge]` — swap token → USDC on the source chain, then bridge that USDC to the recipient. The bridge leg's input is the swap leg's **exact quoted output**, so the two legs cannot drift apart.
- **Cross-chain, USDC source:** `[bridge]` — the swap leg is skipped.

Each step names the ERC-20 to approve (`approve_token`) to that step's `tx.to` for **exactly** `approve_amount` (never infinite). `gas` is returned as `"0"` (the wallet estimates). The quote expires after 60 seconds. The `preview` carries `input` / `output` (symbol, amount, `value_usd`) and `fee_usd` — the user's real cost (no platform or app fee is taken; Flow C settles directly through Uniswap/Across).

### Mandatory client-side verification

Before the wallet is ever prompted, the client **must** decode each step's `tx.data` and verify it — the binding gate. A compromised backend cannot redirect funds because the client refuses to sign mismatched calldata. Per leg:

- **Whitelisted target:** every `tx.to` must be in the caller-supplied `allowedRouters`. An empty whitelist **fails closed** (FC-001).
- **Whitelisted commands:** `verifyQuoteCalldata` decodes Universal Router command bytes and **fails closed** on anything outside the known set (`V3_SWAP_EXACT_IN`, `SWEEP`, `TRANSFER`, `PAY_PORTION`, `V2_SWAP_EXACT_IN`, `WRAP_ETH`, `UNWRAP_WETH`); the recipient word of every recipient-bearing command is pinned.
- **Recipient match:** the decoded recipient must equal the leg's expected receiver — the **swapper** (wallet) for a swap leg, the chosen **recipient** for a bridge leg.
- **Bound spend:** pass `{ amountIn }` as the optional fifth argument to bind the decoded spend amount to what the user actually entered; the result echoes `amountIn: bigint | null`.
- **Sane `minOut`:** the on-chain floor decoded from the **final** leg must be ≥ `preview.output.amount` (already USDC base units) within tolerance (1% default via `minOutTolerance`; the standalone `verifyMinOut` helper defaults to 5%).

### Raw functions

```typescript
import {
  fetchBalances, fetchQuote, verifyQuoteCalldata, verifyMinOut,
  type SendQuoteResponse,
} from '@peltier/core'

const apiUrl = 'https://api.peltier.dev'

// 1. Pull the wallet's payable balances (held tokens across supported chains).
const { options } = await fetchBalances(apiUrl, wallet)   // TokenOption[] — { chain_id, token, symbol, decimals, amount, value_usd, … }
const token = options[0]                                   // let the payer pick one

// 2. Pull a quote — `recipient` is pinned; `amount` is base units.
const quote: SendQuoteResponse = await fetchQuote(apiUrl, {
  wallet,
  src_chain_id: token.chain_id,
  token: token.token,
  amount: '1000000',            // e.g. 1 USDC (6 decimals) — exact base units
  dest_chain_id: 8453,
  recipient,                    // the USDC settlement address
})

// 3. MANDATORY client-side verification BEFORE any wallet prompt (LD-3, fail-closed).
for (const step of quote.transactions ?? []) {
  const expected = step.kind === 'swap' ? wallet : recipient
  const ok = verifyQuoteCalldata(step.tx.data, step.tx.to, expected, allowedRouters,
    { amountIn: 1_000_000n })  // optional: bind the decoded spend to the entered amount
  if (!ok.verified) throw new Error(`refusing to sign ${step.kind}: ${ok.error}`)
}
const floor = verifyMinOut(quote.tx.data, quote.preview.output.amount) // standalone default: 5% tolerance (the hook passes 1%)
if (!floor.verified) throw new Error(`refusing to sign: ${floor.error}`)

// 4. Execute `quote.transactions` IN ORDER — per leg: ERC-20 approve the exact
//    `approve_amount` to `step.tx.to`, send `step.tx`, await the receipt, then the next leg.
```

### Hook (`useSendFromWallet`)

`useSendFromWallet` wraps all four steps — connect → quote → verify → approve → multi-leg send → poll — and exposes `getQuote()`, `send()`, `clearQuote()`, `reset()`, and reactive `phase` / `quote` / `txHash` / `error`. `<PeltierSend>` is just its DOM rendering, so reach for the hook when you want a custom UI but not the plumbing. It re-fetches + re-verifies a fresh quote if the displayed one is within ~10 s of expiry, so a near-stale on-chain `deadline` is never signed.

- `allowedRouters` is **required** in the options — the hook fails closed without it.
- `error` is a structured `PeltierError` (`{ code, message, retryable, cause }`) or `null` — e.g. a token with undiscoverable decimals surfaces as a `config_error` rather than a silent 18-decimals assumption.
- **Per-leg resume:** if the bridge leg of a `[swap, bridge]` plan fails, retrying resumes at the bridge — the confirmed swap is never re-run.

The hook ships identically in `@peltier/react-native` (`<SendWidget>` is its RN rendering).
