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 (
cancelAuthorizationon 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:
POST /c/{id}/preparewith the payer address. The response carries period 0 at the top level as always, plusschedule[]: one typed data per held period, each with its ownvalid_afterandvalid_before.- Sign them all.
POST /c/{id}/paywith 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) andPOST /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
CancelAuthorizationtyped data per held period. Sign them andPOST /schedule/{id}/cancel {"authorizations": [{"nonce", "signature": {"v","r","s"}}]}. We submit USDCcancelAuthorizationfor 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=trueturns 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) andSCHEDULE_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.