Documentation

How PayPhone works.

A 72-hour build for EasyA Consensus Miami 2026. Below: the architecture, the design choices that surprised us, and the four on-chain proofs.

TL;DR

The whole thing in three sentences.

PayPhone is per-second video billing on Base. A buyer-side AI agent authorizes “up to $5” via x402’s upto scheme; when the call ends, on-chain settlement transfers only the actual amount used in one Permit2 USDC transfer. Built for EasyA Consensus Miami 2026 — Agentic Track, Coinbase + AWS sponsored.

Architecture

Three phases. One signature. One transfer.

01 · AUTHORIZEBrowser signs “up to $5”; backend verifies + creates room + persists.Browser(buyer agent)/api/sessions(Next.js)CDP FacilitatorverifyDaily.cocreateRoomDynamoDBstatus: AUTHORIZEDsigned POSTverifycreateRoom02 · TALKLive video. Ticker counts up. No on-chain activity yet.Browser(in-call)Daily RoomWebRTC + transcriptionBrowser(expert)WebRTCWebRTC03 · SETTLEUser hangs up. One USDC transfer. DDB flipped to COMPLETED.Daily.comeeting.ended/api/webhooks/dailyHMAC-verifiedBase mainnetUSDC transferPOST (HMAC)settle (via CDP)

The settlement amount is floor(duration_sec) × $0.01, capped at the signed maximum. The unspent allowance simply expires on-chain — no refund path, no leftover state.

Why these choices

Four decisions that shaped the build.

Why `upto`, not `exact`

The `exact` scheme requires the buyer to know the price in advance. We don’t — duration is whatever the call ends up being. With `upto`, the buyer signs a "I’ll spend at most $5" witness; the proxy contract enforces `amount <= permitted.amount` on-chain via `AmountExceedsPermitted`. Unspent allowance never moves; the chain doesn’t care that we asked for less than we authorized.

Why hand-rolled, not paymentMiddleware

CDP’s `paymentMiddleware` / `withX402` settles at request-time. We need to defer settlement until the call hangs up — that’s the entire pitch. Hand-rolling the 402 → verify → settle dance lets `verify` run when the buyer signs (synchronous, fail-fast on bad payment) but `settle` run from the Daily.co `meeting.ended` webhook (asynchronous, with the actual duration in hand).

Why Permit2

Two USDC schemes are available on EVM: EIP-3009 (`transferWithAuthorization`) and Permit2 (`signatureTransfer`). EIP-3009 is fixed-amount: signing for $5 means $5 moves, period. Permit2 supports underspend: the witness says "up to $5," the actual transfer can be any value $0 ≤ x ≤ $5. There is no other primitive on EVM that lets a single signature authorize a range and the chain enforce the cap.

Why CDP Agentic Wallets

The buyer-side agent signs without holding ETH. CDP’s facilitator declares `eip2612GasSponsoring` + `erc20ApprovalGasSponsoring` extensions in the 402 challenge; the buyer signs an EIP-2612 permit + the Permit2 witness, and the facilitator combines them into a single sponsored transaction. The buyer wallet stays gas-free. This is what makes "no-wallet AI agents" work for the consumer side — they show up with an api key, not a funded EOA.

On-chain proofs

Four milestones, four BaseScan links.

Tech stack

What the dependencies look like.

GroupPackageVersion
RuntimeNext.js16.2.4
React19.2.4
TypeScript5.x
Tailwind CSS4.x
Payments@coinbase/cdp-sdk1.48.2
@coinbase/x4022.1.0
@x402/core2.11.0
@x402/evm2.9.0
viem2.48.8
Video@daily-co/daily-js0.89.1
Cloud@aws-sdk/client-dynamodb3.1043.x
AI@anthropic-ai/sdk0.94.0
ai (Vercel AI SDK)6.0.x
UImotion12.38.0
lucide-react1.14.0
shadcn/ui CLI4.7.0

Mitigations

Five things that bite, all guarded for.

  1. 1

    Issue #1065: gas estimation flake

    CDP mainnet `/settle` returns "unable to estimate gas" ~40% of the time. We wrap settle in retry-with-backoff (3 attempts: 2s, 5s, 10s) in `lib/x402.ts:settleWithRetry`. Pre-warming the buyer wallet with a tiny dry-run before stage cuts the rate further.

  2. 2

    Permit2 first-time approval

    A buyer’s first `upto` payment requires `approve(Permit2, max_uint256)` on USDC. We declare `erc20ApprovalGasSponsoring` in `PaymentRequirements.extensions`, so the facilitator pays the gas for the approval. Done once per wallet during dress rehearsal #1; subsequent calls skip it.

  3. 3

    EIP-712 domain mismatch

    USDC’s `name()` differs by network: `"USD Coin"` on mainnet, `"USDC"` on Sepolia. Hardcoding the wrong value silently produces a signature the facilitator’s on-chain `staticcall` rejects with revert. `lib/constants.ts:USDC_DOMAIN` is keyed by network, derived empirically from `probe-usdc.ts`.

  4. 4

    Idempotent settle (double-spend guard)

    Daily’s webhook retries on non-2xx (3 attempts before circuit-open). `lib/db.ts:markSessionCompleted` is conditional on `status = AUTHORIZED`; the second call fails with `ConditionalCheckFailedException`, which the route catches and returns 200 to stop further retries. The Permit2 nonce inside the witness is consumed on first settle and prevents a duplicate transfer at the contract level too.

  5. 5

    Daily safety rails

    No cloud recording (billable, no free tier). No live streaming, no telephony. Rooms set `exp = now + 30 min` and `eject_at_room_exp: true`; transcription stops when the last participant leaves. Webhook signatures are HMAC-SHA256 verified against `DAILY_WEBHOOK_SECRET` using `crypto.timingSafeEqual`.

72 hours · MIT licensed

Read the code, file an issue.

Every line is on GitHub. The README walks through running it locally with seeded credentials and a Sepolia faucet drip.

Buyer wallets are seeded testnet keys. Don’t paste a real private key into a hackathon demo. Ever.