2026-09-19
Designing a USDC Recurring Billing API

A USDC recurring billing system is not a crypto version of Stripe Billing, and the difference matters. Card networks let a merchant initiate a charge after collecting a mandate from a bank. A self-custodial wallet can't safely become a standing debit order just because a customer connected it once, because no bank in the middle agrees to honor that mandate on your behalf.
That constraint is worth keeping. If your subscription system can move customer funds indefinitely with no fresh, inspectable authorization, you've recreated the worst part of traditional payments, opaque pull authority, with more complicated failure modes and none of the regulatory backstops.
For SaaS and usage-based products, the design starts with a hard question: what exactly has the customer authorized, on which network, from which wallet, for how much, and until when? Klap doesn't have a subscriptions product. It has charges, deterministic addresses, splits and events. This post is about building recurring billing on top of those primitives.
A recurring billing system needs an authorization model
"Recurring" can describe several different payment mechanics, and treating them as interchangeable causes billing disputes and unsafe wallet permissions later.
The simplest model is a scheduled payment request. At renewal, your backend creates a new USDC charge for the invoice amount and the customer sends the transaction from their wallet. This keeps consent clear and works with any ordinary wallet. The trade-off is that the customer has to come back and approve every invoice.
A second model uses pre-authorized token allowances. The customer lets a contract or spender move up to a set amount of USDC, and your billing service submits periodic transfers within that limit. This cuts renewal friction, but it changes the security model. An unlimited approval is a standing withdrawal permission that stays valid until someone remembers to revoke it.
A third model uses smart accounts or session keys: the customer authorizes a scoped, on-chain rule, such as $29 USDC every 30 days, capped at 12 payments, payable only to one recipient. This comes closest to a card mandate, but support depends on the customer's wallet and account-abstraction stack, and it adds smart-contract surface area your team has to audit and operate.
For most products a hybrid works best: push-based renewal requests by default, with an opt-in, tightly scoped authorization for customers on compatible smart wallets. Wallets differ in what recurring behavior they support, so don't design as if they all match.
Model subscriptions as state, not a cron job
A cron task that fires a charge once a month can't explain a payment that is late or underpaid, or one confirmed after your invoice deadline. It just fires again next month and hopes.
Split your subscription record into commercial state and payment state, and keep the two apart. Commercial state says whether the customer has an active plan and what they owe. Payment state says what happened to a specific charge, and it comes straight from Klap, not from your own guess:
Your invoice: scheduled -> charge_created -> paid|vexpired / underpaid (needs review)Klap charge: pending -> confirmed|vexpired / underpaid / partially_paidSettlement: pending -> completed / failed
At minimum, store the subscription ID, the customer's wallet, the network, your pricing rule, the billing interval and the charge ID for each attempt. Add an idempotencyKey per attempt so a retried request can never double-charge. Use externalRef on the charge to carry your own subscription or invoice ID through. Klap echoes it back on the charge and on every webhook event, so you're never reconciling by amount and timestamp alone.
For allowance- or mandate-based collection, add states of your own for authorization creation, authorization expiry, and revocation. Your customers need visibility into these states and your support team needs an audit trail for them, so don't leave them for later.
Use deterministic addresses for attribution
A recurring system has to reliably match money to an invoice, every time, without asking the customer to paste a transaction hash. A shared deposit address plus a memo field is fragile on EVM chains, because a plain USDC transfer doesn't carry an off-chain invoice ID with it.
Klap predicts a unique payment address for every charge, derived deterministically from the charge's own ID before it's funded. When USDC lands there, the address tells you which invoice got paid, with no separate matching step and nothing to reconcile by hand. The mechanics are worth understanding if you're going to lean on this for every renewal in your billing cycle.
Events are your billing control plane
Polling an endpoint every few seconds is fine for a prototype, but it can't carry entitlement, accounting, or customer notifications at renewal scale.
Consume webhooks for the durable path (provisioning access, issuing receipts, updating invoices) and use server-sent events when a checkout is actually open and needs to reflect a status change immediately. Verify every webhook signature and persist the delivery ID. Process events idempotently:
const event = klap.webhooks.constructEvent(request.rawBody,request.headers['x-klappay-signature'],process.env.KLAP_WEBHOOK_SECRET,)if (event.event === 'charge.confirmed') {await markInvoicePaidOnce(event.data.id, event.id)await extendSubscriptionAccess(event.data.externalRef)}
event.data is the full charge, so event.data.id is the charge itself and event.data.externalRef is the subscription ID you set when you created it, so no separate lookup is required. event.id is the webhook delivery ID, and it's what makes markInvoicePaidOnce mean what its name says: a retried delivery of the same charge.confirmed event should never extend access twice or double-issue a receipt.
Don't stop at charge.confirmed. It tells you a qualifying transfer landed on-chain. charge.settled tells you the payout to your own wallet completed, a separate step tracked independently as settlementStatus. A stale, late-arriving charge.partially_paid event should never roll a charge backward if a charge.confirmed event already advanced it.
Price in dollars, settle in USDC, define the exceptions
USDC tracks the dollar, which makes subscription pricing simpler than with a volatile asset, but billing decisions remain. On Klap, a charge's amount is always denominated in USD, and acceptedPayments is the separate list of which USDC (or USDT) network you'll take it on. For most USDC-priced SaaS products that shape is what you want: predictable amounts and no price feed of your own to maintain.
Network fees need a direct policy too. On a plain wallet flow, the customer's wallet covers gas. In a sponsored smart-account flow, your platform might cover it to improve conversion, and Klap's feePayer setting lets you decide who bears Klap's own processing fee independently of that gas question. Sponsorship can make a $5 subscription practical, but it turns gas into an operating cost you have to forecast and opens a new door for abuse. Neither choice is right for everyone, so settle on one before you ship.
Also decide, ahead of time, what happens to partial payments, late payments and wrong-network payments. A charge that comes in underpaid should land in review, not get waved through as confirmed because the amount was close enough. Automation should be strict where money changes hands.
Custody is not a shortcut
Plenty of crypto billing products avoid their own operational work by pooling customer funds into a processor-controlled wallet and paying merchants out later from an internal balance. That's easier for the processor to build, and worse settlement architecture for you.
Klap detects and routes payments without ever taking custody of merchant or customer funds. The transfer reaches your recipients directly. For a marketplace or revenue-share subscription, native recipient splits can route each renewal to the right participants without passing through a pooled balance you'd have to reconcile and redistribute later.
You still protect API credentials, verify webhook signatures, and reconcile on-chain records with your own database. What changes is that ownership stays legible: the funds are where the transaction says they are, not where a dashboard claims they are.
Build the renewal path before the checkout path
Teams tend to start with a polished checkout screen and postpone retries, expired authorizations and cancellation timing. Subscription revenue is won or lost in those paths.
Define your renewal state machine and your customer-facing authorization terms first. Then build deterministic charge creation and idempotent event handling, and find a way to test both without spending real funds. Klap's sandbox can simulate a confirmed, underpaid, expired, or failed-settlement charge on demand, so you can exercise every renewal edge case before a real customer hits one. Only after that should you spend time polishing the wallet prompt.
For the rest of a SaaS integration around it (plans, upgrades, access on charge.confirmed), see crypto payment gateway for SaaS.
A recurring payment system earns trust by being boring under failure. Start at docs.klappay.com when you're ready to build it.