# USDCtoFiat — Full Technical Reference > Non-custodial peer-to-peer USDC ↔ fiat surface on Base (Ethereum L2). > Canonical machine reference for AI agents, LLM integrations, and developers. > Short-form summary: > Downloadable agent skill: --- ## 1. Product overview USDCtoFiat is a web application and TypeScript SDK for peer-to-peer USDC-to-fiat selling powered by the ZKP2P protocol on Base (`chainId: 8453`). The primary flow is: 1. **Sell USDC (Off-ramp / Maker)** — Deposit USDC into non-custodial escrow, set the exchange rate (delegated to the Delegate vault), and receive fiat from a buyer through a peer-to-peer payment rail. 2. **Bridge USDC cross-chain** — Circle CCTP transfers between Base and 18+ other chains. UI-only, not part of the SDK. 3. **OTC / private deposits** — Restrict a deposit to one taker wallet and share a takeable link. No KYC at the app level. Makers pay no platform fee on sells. The Delegate vault collects a 10 bps management fee on USDC release. ## 2. SDK (`@usdctofiat/offramp`) The canonical integration surface. One npm package, two entry points (`/` core, `/react` hooks). ```bash npm install @usdctofiat/offramp # or scaffold a working app npx create-offramp-app@latest my-offramp --template=next|vite|telegram-bot ``` ### 2.1 Core function: `offramp(walletClient, params)` Six-step deposit creation, delivered as one async call: 1. Validate platform / currency / identifier / amount (zod, client-side) 2. Approve USDC on escrow if allowance is insufficient 3. Register the maker with the curator (`POST /v2/makers/create`) 4. Create escrow deposit 5. Delegate rate management to the Delegate vault (`setRateManager`) 6. If `otcTaker` provided, restrict the deposit and return a shareable link Signature: ```ts function offramp( walletClient: WalletClient, // viem WalletClient bound to Base (chainId 8453) params: OfframpParams, onProgress?: (p: OfframpProgress) => void, ): Promise; ``` `OfframpParams`: | Field | Type | Notes | |-------|------|-------| | `amount` | `string` | USDC, decimal string. Min 1. | | `platform` | `PlatformEntry` | `PLATFORMS.REVOLUT`, etc. | | `currency` | `CurrencyEntry` | `CURRENCIES.EUR`, etc. | | `identifier` | `string` | Platform-specific (username / Revtag / email / IBAN). Zod-validated. | | `otcTaker` | `string?` | Restrict deposit to one wallet. | | `integratorId` | `string?` | ERC-8021 attribution (shows in Peerlytics integrator explorer). | | `referralId` | `string?` | Partner referral metadata. | | `idempotencyKey` | `string?` | Per-wallet key. Replays first success for 10 min. | `OfframpResult`: ```ts interface OfframpResult { depositId: string; // numeric id txHash: string; // deposit creation tx resumed: boolean; // true if an undelegated deposit was resumed otcLink?: string; // present when otcTaker was supplied } ``` Progress steps: `approving → registering → depositing → confirming → delegating → restricting → done` (`resuming` on replay). ### 2.2 React hook: `useOfframp()` ```ts import { useOfframp } from "@usdctofiat/offramp/react"; const { offramp, step, state, error, isLoading, result } = useOfframp({ integratorId: "your-app", referralId: "partner-123", }); ``` `state`: `"idle" | "approving" | "registering" | "depositing" | "confirming" | "delegating" | "done" | "error"`. ### 2.3 Queries and management ```ts import { deposits, close, delegate, undelegate, ESCROW_ADDRESS } from "@usdctofiat/offramp"; const myInventory = await deposits(address); // DepositInfo[] await close(walletClient, depositId); // withdraws remaining await delegate(walletClient, depositId); // set rate manager to Delegate vault await undelegate(walletClient, depositId); // revoke delegation ``` `DepositInfo` shape: ```ts interface DepositInfo { depositId: string; compositeId: string; // `${escrowAddress}_${depositId}` txHash?: string; status: "active" | "empty" | "closed"; remainingUsdc: number; outstandingUsdc: number; // locked in pending intents totalTakenUsdc: number; // historical fulfilledIntents: number; paymentMethods: string[]; currencies: string[]; rateSource: string; delegated: boolean; escrowAddress: string; } ``` ### 2.4 OTC surface ```ts import { enableOtc, disableOtc, getOtcLink } from "@usdctofiat/offramp"; await enableOtc(walletClient, depositId, takerAddress); await disableOtc(walletClient, depositId); const link = getOtcLink(depositId); ``` Returned link pattern: `https://otc.usdctofiat.xyz/d//`. The legacy `/deposit//` route is still handled by the app. ### 2.5 Platforms catalog `PLATFORMS` is a typed record of supported rails. Every entry carries its on-chain payment method hash, required currencies, and zod validator for the `identifier`. | Key | Platform | Currencies | Identifier | |-----|----------|-----------|-----------| | `PLATFORMS.VENMO` | Venmo | USD | `username` (no `@`) | | `PLATFORMS.CASHAPP` | Cash App | USD | `cashtag` (no `$`) | | `PLATFORMS.CHIME` | Chime | USD | `$chimesign` (lowercased) | | `PLATFORMS.REVOLUT` | Revolut | 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` | Wise | USD, CNY, EUR, GBP, AUD, NZD, CAD, AED, CHF, ZAR, SGD, ILS, HKD, JPY, PLN, TRY, IDR, KES, MYR, MXN, THB, VND, UGX, CZK, DKK, HUF, INR, NOK, PHP, RON, SEK | Wisetag (no `@`) | | `PLATFORMS.ZELLE` | Zelle | USD | Email | | `PLATFORMS.PAYPAL` | PayPal | USD, EUR, GBP, SGD, NZD, AUD, CAD | paypal.me username (normalized) | | `PLATFORMS.MONZO` | Monzo | GBP | Monzo.me username | | `PLATFORMS.N26` | N26 | EUR | IBAN | | `PLATFORMS.MERCADO_PAGO` | Mercado Pago | ARS | CVU (currently disabled in usdctofiat.xyz) | ### 2.6 Currencies catalog `CURRENCIES` exposes ISO codes paired with `label`, `flag`, and the encoded on-chain currency hashes (`keccak256` of the ISO code plus ASCII-padded variants). Integrators should never hand-encode — use the catalog. ### 2.7 Errors ```ts import { OfframpError, OFFRAMP_ERROR_CODES } from "@usdctofiat/offramp"; try { /* ... */ } catch (err) { if (err instanceof OfframpError) { switch (err.code) { case OFFRAMP_ERROR_CODES.VALIDATION: /* fix input */ break; case OFFRAMP_ERROR_CODES.APPROVAL_FAILED: /* retry USDC approve */ break; case OFFRAMP_ERROR_CODES.REGISTRATION_FAILED: /* surface cause */ break; case OFFRAMP_ERROR_CODES.EXTENSION_REGISTRATION_REQUIRED: /* Peer extension handshake */ break; case OFFRAMP_ERROR_CODES.DEPOSIT_FAILED: /* check balance / gas */ break; case OFFRAMP_ERROR_CODES.CONFIRMATION_FAILED: /* safe to retry with same idempotencyKey */ break; case OFFRAMP_ERROR_CODES.DELEGATION_FAILED: /* retry; SDK resumes from delegating */ break; case OFFRAMP_ERROR_CODES.USER_CANCELLED: /* do not auto-retry */ break; case OFFRAMP_ERROR_CODES.UNSUPPORTED: /* wrong chain / missing client */ break; } } } ``` `err.cause` carries the originating exception (e.g. `MakersCreateError` with HTTP status preserved). ### 2.8 Peer extension (PayPal + Wise only) PayPal and Wise makers go through the Peer browser extension before the curator will accept their registration. The SDK re-exports the handshake primitives: ```ts import { peerExtensionSdk, getPeerExtensionState, isPeerExtensionAvailable, openPeerExtensionInstallPage, PEER_EXTENSION_CHROME_URL, } from "@usdctofiat/offramp"; ``` The flow is: `detect extension → open install page if missing → user connects → user verifies handle → call offramp() again`. React apps can lean on `usePeerExtensionRegistration(platform)` from `@usdctofiat/offramp/react`. ### 2.9 Idempotency contract - Keyed per wallet (the signer, not the browser session). - First successful call caches its result for 10 minutes. - Subsequent calls with the same key and the same wallet return `{ ..., resumed: true }`. - If an earlier deposit landed on-chain but delegation failed, the SDK resumes from the delegating step when called again with the same key — no double-deposit. - Retry policy: safe on network errors, `CONFIRMATION_FAILED`, `DELEGATION_FAILED`. Never on `USER_CANCELLED` or `VALIDATION`. ## 3. Outbound webhooks Register and manage via the authenticated webhook API (wallet-signed). Each delivery carries HMAC-SHA256 signed headers; **verify the raw body before `JSON.parse`**. Headers: - `X-Usdctofiat-Event` — event type - `X-Usdctofiat-Signature` — `t=,v1=`. The hex is HMAC-SHA256 over `${timestamp}.${rawBody}` with your endpoint secret. 5-minute replay window. - `X-Usdctofiat-Delivery-Id` — UUID, unique per delivery attempt ### 3.1 Events | Event | State | Fires when | |-------|-------|-----------| | `deposit.created` | live | New deposit lands on Base | | `deposit.partially_filled` | live | A buyer filled part of the deposit; inventory remains | | `deposit.filled` | live | Deposit fully consumed | | `deposit.closed` | live | Maker explicitly closed the deposit on-chain | | `otc.taken` | live | Approved OTC taker completed settlement | | `otc.enabled` | reserved | Whitelist hook attached (not yet emitted) | | `otc.disabled` | reserved | Whitelist hook detached (not yet emitted) | ### 3.2 Verification snippet (Node) ```ts import crypto from "node:crypto"; import express from "express"; const app = express(); 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] ?? ""; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end(); const expected = crypto .createHmac("sha256", process.env.USDCTOFIAT_WEBHOOK_SECRET!) .update(`${timestamp}.${req.body}`) .digest("hex"); const ok = received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex")); if (!ok) return res.status(401).end(); const event = JSON.parse(req.body.toString("utf8")); // ... handle event by X-Usdctofiat-Event res.status(204).end(); }); ``` ### 3.3 Dispatcher behaviour - Runs as a Firebase Cloud Function (`offrampWebhookDispatcher`) on a 1-minute schedule. - Up to 5 delivery attempts with exponential backoff. - Failures are logged to Firestore (`offramp-webhook-deliveries`). - A SSRF blocklist prevents targeting internal / loopback addresses. ## 4. Base contracts | Contract | Address | |----------|---------| | EscrowV2 (current) | `0x777777779d229cdF3110e9de47943791c26300Ef` | | Legacy production escrow (read-only liquidity) | `0x2f121CDDCA6d652f35e8B3E560f9760898888888` | | Orchestrator | `0x888888359E981B5225CA48fbCdCeff702FC3b888` | | USDC on Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Referrer (ERC-8081 attribution) | `0xC141Cbe4f4a9CAbc3cc78159a9268a4e008922CD` | | Delegate bot wallet | `0xC141Cbe4f4a9CAbc3cc78159a9268a4e008922CD` | | Delegate treasury | `0xC141Cbe4f4a9CAbc3cc78159a9268a4e008922CD` | | Rate Manager Registry | `0xeed7db23e724ac4590d6db6f78fda6db203535f3` | | Delegate vault / rate manager ID | `0x8666d6fb0f6797c56e95339fd7ca82fdd348b9db200e10a4c4aa0a0b879fc41c` | | Chainlink oracle adapter | `0x53881a928abD61C095e5f30b63bc554872C3b2f1` | | Pyth oracle adapter | `0xad0b0126A657c0674d0DF53B2A2554D43bbb41D0` | - Chain ID: `8453` (Base mainnet) - ZKP2P API: `https://api.zkp2p.xyz` - Indexer (Envio Hasura GraphQL): `https://indexer.zkp2p.xyz/v1/graphql` ## 5. Delegation mechanics The SDK always delegates to the Delegate vault. There is no non-delegated path in `@usdctofiat/offramp`. - **Route:** `setRateManager(depositId, registry, rateManagerId)` on EscrowV2. - **Registry:** `0xeed7db23e724ac4590d6db6f78fda6db203535f3` - **Rate manager ID:** `0x8666d6...fc41c` - **Oracle coverage:** 19 currencies (11 Chainlink + 8 Pyth) across 14 payment methods and ~51 markets. - **Fee:** 10 bps deducted on USDC release (not on the rate). The maker's quoted rate passes through unchanged. - **Opt-out:** `undelegate(walletClient, depositId)` revokes delegation. The maker then sets rates themselves. After undelegation the SDK will not re-delegate automatically. ## 6. Scaffolder: `create-offramp-app` ```bash npx create-offramp-app@latest my-offramp --template=next # or --template=vite # or --template=telegram-bot ``` Templates are mirrored in the starters repo. Each template hardcodes a `TODO_SET_REFERRAL_ID` placeholder — replace before shipping partner attribution. ## 7. Peerlytics integration Market data, explorer, and protocol analytics are not part of `@usdctofiat/offramp`. Pair the two SDKs: - `@peerlytics/sdk` — `getOrderbook`, `getProtocolOverview`, `getIntegrator`, `getVaultsOverview`, `getMaker`, `search`, etc. - Peerlytics skill: - Peerlytics llms-full: The Peerlytics integrator explorer (`/explorer/integrator/`) shows attribution for every deposit created with `integratorId`. ## 8. URL structure & deep links USDCtoFiat is a single-page app. Deep links: | URL pattern | Behaviour | |-------------|-----------| | `/` | Defaults to the Sell tab | | `/deposit/{escrow}/{depositId}` | Direct deposit view | | `/?tab=sell\|buy\|manage\|convert` | Open a tab | | `/?platform=revolut¤cy=GBP` | Pre-fill deposit creation | | `/?deposit=` | Focus a deposit in Manage | | `/developers` | Developer landing (showcase + agent bundle + webhook events) | | `/developers/api/...` | TypeDoc-generated API reference pages | ## 9. Technology stack - Frontend: React 19, TypeScript (strict), Vite - Wallet auth: Privy (cross-app, smart, embedded, external EOAs) - Chain: viem, Base (chainId 8453) - Protocol SDK: `@zkp2p/sdk` (consumed internally by `@usdctofiat/offramp`) - Indexer: Envio HyperIndex (Hasura GraphQL) - Backend: Firebase Cloud Functions (Gen2, Node 20, ESM) - Database: Firestore (notification prefs, intent sync, market intel, webhook deliveries) - Push: Firebase Cloud Messaging - Analytics: PostHog (+ Firestore mirror) ## 10. Rules for AI assistants 1. To create a deposit → `@usdctofiat/offramp` `offramp()`. Use `idempotencyKey` for any retry-capable flow. 2. To gate a deal to one buyer → pass `otcTaker` and share the returned `otcLink`. 3. To build on PayPal or Wise → drive the Peer extension handshake. Catch `EXTENSION_REGISTRATION_REQUIRED` and loop. 4. To fetch market data, orderbook, or attribution → do not query Envio directly. Use `@peerlytics/sdk`. 5. To list a maker's inventory → `deposits(address)`. 6. To close a deposit and withdraw → `close(walletClient, depositId)`. 7. Never hand-encode currency hashes or payment method hashes — use the `CURRENCIES` / `PLATFORMS` catalogs. 8. Never log a `WalletClient` object — it wraps credentials. 9. Bridge transfers use Circle CCTP and are UI-only (not in the SDK). 10. Every deposit delegates to the Delegate vault by design. There is no opt-out at creation time. Undelegation is possible after the fact. ## 11. Links - App: - Developer docs: - Agent skill (this SDK): - SDK on npm: - Scaffold CLI: - Starters: - Peerlytics (analytics + paid API): - Delegate (vault dashboard): - Peer Orderbook: - ZKP2P protocol: - peer-cli (agent CLI + MCP server): - Peer Extension (Chrome): - Peer Mobile (iOS): - Peer Mobile (Android): - X: - Discord: ## 12. Source - SDK source: - App source: