---
name: usdctofiat
description: Create non-custodial USDC-to-fiat offramp deposits on Base using the @usdctofiat/offramp SDK. Use when the user wants to sell USDC for fiat (Venmo, Revolut, Wise, Zelle, PayPal, Cashapp, Chime, Monzo, N26), build a peer-to-peer off-ramp flow in their app, wire up OTC (private) deposits, register lifecycle webhooks, or delegate pricing to the managed vault. Handles wallet signing via viem WalletClient, maker registration, escrow deposit, delegation, and idempotency.
---

# USDCtoFiat offramp skill

USDCtoFiat is the ZKP2P-powered USDC ↔ fiat surface on Base. This skill ships the one-function SDK (`offramp()`) and the signed webhook contract that lets any app onboard makers without running rates, oracles, or payout rails.

- **SDK:** `@usdctofiat/offramp` (TypeScript, ESM + CJS + `/react`)
- **CLI template:** `npx create-offramp-app@latest my-app --template=next|vite|telegram-bot`
- **llms.txt:** `https://usdctofiat.xyz/llms.txt`
- **Full reference:** `https://usdctofiat.xyz/llms-full.txt`
- **Starters:** `https://github.com/ADWilkinson/usdctofiat-peerlytics-starters`
- **Chain:** Base mainnet (`chain_id: 8453`)

## When to reach for this skill

| User intent                          | Entry point                                                                 |
| ------------------------------------ | --------------------------------------------------------------------------- |
| "Add a sell-USDC button"             | `offramp(walletClient, params)` or `useOfframp()`                           |
| "Build a server-side maker bot"      | `offramp(walletClient, params)` in Node (idempotency key recommended)       |
| "Private deal with one counterparty" | `offramp(..., { otcTaker })` → shareable `otc.usdctofiat.xyz/d/...` link    |
| "List a maker's deposits"            | `deposits(address)`                                                         |
| "Close a deposit and withdraw"       | `close(walletClient, depositId)`                                            |
| "Subscribe to fills"                 | Register outbound webhooks (see below)                                      |
| "Look up a PayPal/Wise maker"        | Prompt user through Peer extension, catch `EXTENSION_REGISTRATION_REQUIRED` |

## Install and bootstrap

```bash
npm install @usdctofiat/offramp
# or scaffold an app directly
npx create-offramp-app@latest my-offramp --template=next
```

```ts
import { offramp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

const result = await offramp(walletClient, {
  amount: "100", // USDC, decimal string
  platform: PLATFORMS.REVOLUT,
  currency: CURRENCIES.EUR,
  identifier: "alice", // Revtag / @username / email / IBAN, platform-specific
  integratorId: "your-app", // ERC-8021 attribution
  referralId: "partner-123", // optional partner code
  idempotencyKey: `order-${orderId}`, // replay protection for 10 min
});

// result: { depositId, txHash, resumed, otcLink? }
```

React variant:

```tsx
import { useOfframp } from "@usdctofiat/offramp/react";

const { offramp, step, state, error, isLoading, result } = useOfframp({
  integratorId: "your-app",
});
```

## What the SDK owns, what you own

| Step                                                  | Owner                                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
| 01 Validate amount / platform / currency / identifier | SDK (`PLATFORMS.*` zod schemas)                                          |
| 02 Approve USDC on escrow                             | Your wallet signs, SDK builds the call                                   |
| 03 Register maker on curator                          | SDK posts to `POST /v2/makers/create`                                    |
| 04 Create escrow deposit                              | SDK                                                                      |
| 05 Delegate pricing to the vault                      | SDK (`setRateManager`)                                                   |
| 06 Emit lifecycle events                              | USDCtoFiat dispatcher (`deposit.*`, `otc.*`)                             |
| Fiat payout between buyer and seller                  | **You and the buyer, directly**. The SDK never touches fiat.             |
| Key custody                                           | **You / your user's wallet**. SDK uses whatever `WalletClient` you pass. |

Deposits MUST delegate to the Delegate vault — that's the business model. Pricing is `track_market` (vault oracle adjusts). Referrer is hardcoded Galleon Labs attribution. `retainOnEmpty` is always false so deposits self-close when filled.

## Payment platforms

| Platform            | Currencies                                                                                                                            | Identifier                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `PLATFORMS.VENMO`   | USD                                                                                                                                   | `@username` (no `@`)            |
| `PLATFORMS.CASHAPP` | USD                                                                                                                                   | `$cashtag` (no `$`)             |
| `PLATFORMS.CHIME`   | USD                                                                                                                                   | `$chimesign` (lowercased)       |
| `PLATFORMS.REVOLUT` | 23 currencies incl. USD, EUR, GBP, SGD, NZD, AUD, CAD, HKD, MXN, SAR, AED, THB, TRY, PLN, CHF, ZAR, CZK, CNY, DKK, HUF, NOK, RON, SEK | Revtag (no `@`)                 |
| `PLATFORMS.WISE`    | 31 currencies                                                                                                                         | Wisetag (no `@`)                |
| `PLATFORMS.ZELLE`   | USD                                                                                                                                   | Email                           |
| `PLATFORMS.PAYPAL`  | USD, EUR, GBP, SGD, NZD, AUD, CAD                                                                                                     | paypal.me username (normalized) |
| `PLATFORMS.MONZO`   | GBP                                                                                                                                   | Monzo.me username               |
| `PLATFORMS.N26`     | EUR                                                                                                                                   | IBAN                            |

`PLATFORMS.MERCADO_PAGO` is in the catalog but currently disabled in usdctofiat.xyz.

## OTC (private) deposits

Pass `otcTaker: "0x..."` to restrict the deposit to one wallet. The result includes a shareable `otcLink` (`otc.usdctofiat.xyz/d/<escrow>/<depositId>`). Re-enable or rotate via `enableOtc(walletClient, depositId, taker)` / `disableOtc(walletClient, depositId)`.

## Idempotency

Pass `idempotencyKey`. For 10 minutes the SDK replays the first success (`result.resumed === true` on the replay). Safe to reuse for HTTP retries, dropped WebSockets, or Telegram bot reconnects.

## Outbound webhooks

Register at `https://usdctofiat.xyz/api/webhooks` (signed wallet request). Every delivery carries:

- `X-Usdctofiat-Event: deposit.created | deposit.partially_filled | deposit.filled | deposit.closed | otc.taken`
- `X-Usdctofiat-Signature: t=<unix>,v1=<hex>` (HMAC-SHA256 over `{timestamp}.{raw body}`, 5 min replay window)
- `X-Usdctofiat-Delivery-Id: <uuid>`

Verify the **raw body** before `JSON.parse`. Dispatcher retries up to 5 times with exponential backoff. Reserved (not yet emitted): `otc.enabled`, `otc.disabled`.

## Errors

All errors extend `OfframpError` with a typed `code`:

| Code                              | Meaning                                     | Recovery                                                                                  |
| --------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `VALIDATION`                      | Bad input                                   | Fix the input, don't retry blindly                                                        |
| `APPROVAL_FAILED`                 | USDC approve tx reverted or wallet rejected | Ask user to retry or top up gas                                                           |
| `REGISTRATION_FAILED`             | Curator rejected the maker                  | Surface the `cause` message                                                               |
| `EXTENSION_REGISTRATION_REQUIRED` | PayPal / Wise need Peer extension handshake | `usePeerExtensionRegistration(platform)` or drive `peerExtensionSdk` manually, then retry |
| `DEPOSIT_FAILED`                  | Escrow create reverted                      | Check USDC balance, nonce, chain                                                          |
| `CONFIRMATION_FAILED`             | Post-deposit confirmation missed            | Safe to retry with same `idempotencyKey`                                                  |
| `DELEGATION_FAILED`               | `setRateManager` failed                     | Retry with same key; SDK resumes from "delegating"                                        |
| `USER_CANCELLED`                  | Wallet rejected a prompt                    | Do not retry automatically                                                                |
| `UNSUPPORTED`                     | Non-Base chain or missing client            | Switch network / pass a Base `WalletClient`                                               |

Progress callback via `onProgress: (p) => void` with `step` values: `approving`, `registering`, `depositing`, `confirming`, `delegating`, `restricting`, `resuming`, `done`.

## Rules for agents

- Pass an `idempotencyKey` for any server-side or automated flow.
- Never log `walletClient` or private keys. The SDK does not read them directly — it signs through the client you hand it.
- Use `deposits(address)` to check inventory before creating a duplicate. The SDK also resumes an undelegated in-flight deposit automatically.
- Treat the SDK as a pure function of its inputs: passing the same params + key is safe.
- For PayPal and Wise you MUST walk the user through the Peer extension before `offramp()` will succeed. Catch `EXTENSION_REGISTRATION_REQUIRED` and reach for `@usdctofiat/offramp` re-exports: `peerExtensionSdk`, `getPeerExtensionState`, `openPeerExtensionInstallPage`.
- Webhooks are the only way to get fill events in realtime. The dispatcher runs every ~60s, so build your UX around "soon", not "instant".
- Market data and integrator attribution live on Peerlytics (`@peerlytics/sdk`). Combine the two skills to build dashboards.

## Examples

Client-side sell button:

```tsx
"use client";
import { useOfframp } from "@usdctofiat/offramp/react";
import { PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

export function SellButton({ walletClient }: { walletClient: unknown }) {
  const { offramp, step, isLoading, error, result } = useOfframp({
    integratorId: "demo-app",
  });
  if (result)
    return (
      <p>
        Deposit {result.depositId} created in {result.txHash}
      </p>
    );
  return (
    <button
      disabled={isLoading}
      onClick={() =>
        offramp(walletClient, {
          amount: "100",
          platform: PLATFORMS.VENMO,
          currency: CURRENCIES.USD,
          identifier: "alice",
        })
      }
    >
      {isLoading ? (step ?? "Working...") : "Sell 100 USDC"}
    </button>
  );
}
```

Node bot with idempotency + OTC:

```ts
import { offramp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

async function sellToBuyer(orderId: string, buyer: `0x${string}`) {
  return offramp(botWallet, {
    amount: "250",
    platform: PLATFORMS.REVOLUT,
    currency: CURRENCIES.EUR,
    identifier: "desk-ops",
    otcTaker: buyer,
    idempotencyKey: `order-${orderId}`,
    integratorId: "desk-bot",
  });
}
```

Webhook receiver (Node / Express):

```ts
import crypto from "node:crypto";

app.post("/hook", express.raw({ type: "*/*" }), (req, res) => {
  const sig = req.header("X-Usdctofiat-Signature") ?? "";
  const [tPart, v1Part] = sig.split(",");
  const timestamp = tPart.split("=")[1];
  const received = v1Part.split("=")[1];
  const expected = crypto
    .createHmac("sha256", process.env.USDCTOFIAT_WEBHOOK_SECRET!)
    .update(`${timestamp}.${req.body}`)
    .digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex"))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // handle event by type
  res.status(204).end();
});
```

## Further reading

- `https://usdctofiat.xyz/llms-full.txt` — full machine reference (all types, contracts, events)
- `https://github.com/ADWilkinson/usdctofiat-peerlytics-starters` — working Next.js / Vite / Telegram bot templates
- Peerlytics skill: market data, explorer, orderbook → pairs with this for a full integration
- Source: https://github.com/ADWilkinson/galleonlabs-zkp2p/tree/main/packages/offramp-sdk
