checkout402_
Guides

Recurring payments#

A checkout can be a series: the buyer signs one payment per period at checkout, the first settles at once, and the rest settle on their dates. No card on file, no allowance, no custody. It is the crypto-native shape of a subscription, a lease or an instalment plan.

How it works#

x402 has no "charge them again". Every settlement is one signature over one amount, once. So a series is exactly that, N times: at checkout the buyer's wallet signs N EIP-3009 authorizations, each carrying its own future validAfter. USDC refuses an authorization before its date and after its settle window, on-chain, so a held period is inert until its day and worthless after its window closes. We submit each one when its window opens, through the same verify and settle path a one-off payment takes.

What that means for each side:

  • The seller can only ever collect exactly what was signed: the amount, the dates, the count. Nothing can be re-charged, raised or brought forward.
  • The buyer sees every date and amount in the wallet before signing, and can stop the rest at any time: softly (we drop the signatures we hold) or on-chain (cancelAuthorization on USDC, which no server can override).
  • Nobody holds funds. Each period settles from the buyer's own balance on its date. An empty wallet on the day means a failed period, retried until its window closes, then the series lapses.

Base only: nothing on Solana can be signed for next month.

Sell a series#

Add two fields to POST /v1/checkouts (or create_checkout over MCP):

{
  "title": "data37.com, monthly lease",
  "price_usd": "750.00",
  "schedule_interval_days": 30,
  "schedule_periods": 12,
  "fulfillment": "webhook"
}

price_usd is per period. The checkout reads back with a schedule block (interval_days, periods, price_usd, total_usd), the pay page states the whole commitment before anyone signs, and the 402 carries the terms under extra.schedule so an agent knows it is committing to twelve payments, not one.

Limits: 2 to 24 periods, 1 to 366 days apart. A series cannot be escrowed. The deployment must have SCHEDULES_ENABLED on.

What the buyer does#

In a browser with the wallet button on: the wallet asks for N signatures in a row, one per payment, each showing its date and amount. Signing costs no gas. The first payment settles on the spot; the page then links to the buyer's schedule.

With pay402 or any agent:

  1. POST /c/{id}/prepare with the payer address. The response carries period 0 at the top level as always, plus schedule[]: one typed data per held period, each with its own valid_after and valid_before.
  2. Sign them all.
  3. POST /c/{id}/pay with period 0 in the payment header and the rest in the body: {"schedule": [payload, payload, ...]}.

pay402 does this by itself. Its max_usd ceiling is held against the whole commitment (price times periods), so an agent that would not sign one $9,000 invoice cannot sign twelve $750 ones by accident. The purchase returns the series under schedule.

Every held period is verified before period 0 settles. A series with a wrong signer, a missing period, a period dated earlier than agreed or a window longer than allowed is refused with nothing charged.

The schedule#

The pay response carries schedule:

{
  "id": "sch_…",
  "url": "https://…/schedule/sch_…",
  "status": "active",
  "periods": 12,
  "periods_settled": 1,
  "paid_through": "2026-10-14T09:00:00",
  "next_charge_at": "2026-10-14T09:00:00",
  "payer": "0x…"
}

paid_through is the date the settled periods cover up to. It is the one field an entitlement should key on: a domain lease, a seat, an API plan is live while now < paid_through.

Statuses:

Status Means
active periods still held and due
completed every period settled
cancelled buyer, seller or system stopped it; held signatures dropped
lapsed a period's window closed unsettled; the rest were dropped with it

A series never continues past a hole. If period 4 could not settle inside its window, periods 5 to 12 are dropped: a subscription with a gap in it is not the one either side agreed to.

Cancelling#

The buyer, from GET /schedule/{schedule_id} (no account, the id is the capability):

  • Softly: sign the message the page gives (Cancel schedule sch_…, EIP-191 personal sign) and POST /schedule/{id}/cancel {"signature": "0x…"}. We drop every held authorization. After this the server holds nothing that could settle.
  • On-chain: the page lists one CancelAuthorization typed data per held period. Sign them and POST /schedule/{id}/cancel {"authorizations": [{"nonce", "signature": {"v","r","s"}}]}. We submit USDC cancelAuthorization for each from the relayer and pay the gas. This is the veto that needs no trust in us at all, and a buyer can also submit it themselves with any wallet.

The seller: POST /v1/schedules/{id}/cancel or the cancel_schedule MCP tool. What settled stays settled.

Voiding the checkout cancels every series behind it at the next sweep.

Webhooks#

checkout.paid fires for every settled period. A series is N sales, and a ledger built on checkout.paid sees each one, with the same checkout_id and a new payment_id.

Around it:

Event Means
schedule.started period 0 settled; the rest are held
schedule.charged a held period settled on its date
schedule.charge_failed a due period could not settle yet (empty wallet, chain down); retrying until its window closes. Sent once per period
schedule.lapsed a period's window closed unsettled; the series ended
schedule.cancelled buyer, seller or system stopped it
schedule.completed every period settled

Every payload carries data.schedule (the block above) and, where there is one, data.period, data.payment_id and data.receipt_url.

Rehearsing#

The test face of a series runs a compressed clock, like the escrow rehearsal: periods are seconds apart (SCHEDULE_TEST_INTERVAL_SECONDS, default 20) so an integrator sees started, charged and completed inside one session. Pay it with the bare {"payer": "0x…"} payload and nothing needs signing; cancel it with {"payer": "0x…"}.

Operating it#

  • SCHEDULES_ENABLED=true turns the feature on; off, the fields are refused at create and nothing else changes.
  • SCHEDULE_SWEEP_SECONDS (default 60) is the cadence of the thread that submits due periods. A missed tick delays a period, never loses it.
  • SCHEDULE_SETTLE_WINDOW_DAYS (default 7) is how long a period stays settleable after its date: the buyer's exposure per period, and the time a wallet that was empty on the day has to be topped up.
  • SCHEDULE_RETRY_SECONDS (default 3600) is the retry cadence for a period that failed to settle.
  • SCHEDULE_MAX_PERIODS (24) and SCHEDULE_MAX_INTERVAL_DAYS (366) bound what a seller may ask a buyer to sign.

Held signatures are dropped the moment they cannot be settled any more: settled, cancelled, expired or invalid. A cancelled series leaves nothing usable behind.

What this is not#

It is not a pull. The seller cannot change the amount, add a period or charge early. It is not custody: no funds are held anywhere between periods. And it is not a card subscription: a buyer who empties the wallet has, in effect, cancelled, and the seller learns it from schedule.charge_failed and then schedule.lapsed.