On this page
- One product per tier with a monthly or yearly USDC price; one subscription session per sign-up, carrying your account id in customer_reference so every event names the account.
- Gate keys on the current_period_end each event carries. A failed renewal is retried until about a day after the period ends, then the subscription ends.
- There is no usage billing, trial or plan switching: sell overage as one-off payments and move tiers with a new subscription plus a cancel.
How do API and RPC providers bill in USDC today?
RPC providers already sell plans for USDC on Solana, usually as a payment the customer approves in their wallet or a wallet they connect for autopay. QuickNode announced on 5 December 2024 that "users can now pay for their monthly memberships using USDC on Solana", with the customer approving the transaction in a wallet at checkout (QuickNode blog). Helius invoices renewals in USDC on Solana with a payment link by default, and offers an optional autopay built on the Solana Foundation's Subscriptions program; when the wallet is short there is no automatic retry, and an unpaid project is "suspended after 3 days" (Helius billing docs, checked 15 October 2026).
Fianto, which publishes this post, runs the pull model on the same program: the customer approves a cap per period once, and each renewal is collected without a new signature. The rest of this post is the build on Fianto, from the dashboard to the key check in your API gateway; the questions to ask before choosing a gateway are in USDC payment gateway for SaaS. Neither provider above is a Fianto customer; they are here because their pages show how API billing in USDC works in practice.
How do you set up API tiers as USDC prices?
Set up each API tier as its own product with a recurring USDC price for each billing period you offer, created in the dashboard; the v1 API can read products and prices but not create them. One product per tier is our recommendation because the product name is what the checkout shows and what product_ carries on every event, so "Scale" reads better than one shared product name with three prices. An invented provider, Plinthwave RPC, might sell:
| Tier | Price | Billing | Customer pays per charge today |
|---|---|---|---|
| Starter | 19.00 USDC | Monthly (every 30 days) | 19.01 USDC |
| Pro | 49.00 USDC | Monthly (every 30 days) | 49.01 USDC |
| Scale | 199.00 USDC | Monthly (every 30 days) | 199.01 USDC |
| Scale, yearly | 1,990.00 USDC | Yearly (every 365 days) | 1,990.01 USDC |
The extra 0.01 USDC is the network fee the customer pays with each charge today, on plans created since 6 October 2026; there is no Fianto fee during the beta, and after it the customer pays about 0.1 to 0.5% on top. You receive the full price and pay nothing. A price runs from 0.01 to 1,000,000 USDC and can never be edited: to change a tier's price, add a new price and point new sign-ups at its fian_ id.
How do you start a subscription from your API console?
Start a subscription from your console by creating a checkout session on your server with mode: 'subscription' and the tier's price id, then sending the signed-in user to the returned URL. Two fields do the bookkeeping: customer_ comes back as customer. on every subscription event, and metadata (up to 20 keys) is copied onto the subscription.
import { Fianto } from '@fianto/sdk';
const fianto = new Fianto(); // reads FIANTO_APP_ID and FIANTO_APP_SECRET
const TIERS = {
starter: process.env.FIANTO_PRICE_STARTER,
pro: process.env.FIANTO_PRICE_PRO,
scale: process.env.FIANTO_PRICE_SCALE,
};
export async function startPlan(accountId: string, tier: keyof typeof TIERS, signupId: string) {
const priceId = TIERS[tier];
if (priceId === undefined) throw new Error(`no price configured for ${tier}`);
const session = await fianto.checkoutSessions.create(
{
mode: 'subscription',
order_id: `signup_${signupId}`, // one per sign-up attempt, never reused
price_id: priceId,
customer_reference: accountId, // returns as customer.reference on every event
metadata: { tier },
success_url: 'https://console.plinthwave.example/billing/welcome',
cancel_url: 'https://console.plinthwave.example/billing',
},
{ idempotencyKey: `signup:${signupId}` },
);
return session.url; // send the customer here
}Use a fresh order_ for each sign-up: once a subscription session with that id has completed, creating another answers 409 order_. Calling again while the first session is still open returns that session with url: null, so keep the URL from the first call. Do not issue anything on the success page: the customer can close it after signing, and the payment only counts once Solana finalizes it.
Which webhook events issue, limit and revoke an API key?
Four subscription events are enough to drive an API key, and each one carries current_, the moment the paid period runs out. Store that date as the key's paid-until, and let your API gateway compare against it on each request. Events then only move the date; they are not the only thing standing between an unpaid account and your RPC nodes.
import { Webhooks } from '@fianto/nextjs';
import { billing } from '@/lib/billing'; // your own data layer
export const POST = Webhooks({
secret: process.env.FIANTO_WEBHOOK_SECRET,
// Each billing call records event.id in the same database transaction and skips ids it has seen.
onSubscriptionCreated: async (event) => {
const sub = event.data;
await billing.issueKey(event.id, {
account: sub.customer.reference,
subscription: sub.id,
tier: sub.metadata.tier,
paidUntil: sub.current_period_end,
});
},
onSubscriptionRenewed: async (event) => {
await billing.extend(event.id, event.data.id, event.data.current_period_end);
},
onSubscriptionPastDue: async (event) => {
await billing.markPastDue(event.id, event.data.id, event.data.failure?.next_attempt_at);
},
onSubscriptionEnded: async (event) => {
await billing.revokeKey(event.id, event.data.id, event.data.end_reason);
},
});The same callbacks exist in @fianto/express and @fianto/hono. Deliveries can repeat and arrive out of order, which is why every write is keyed on event. and why a renewal only ever moves paid-until forward. Endpoint verification, signatures and the full event table are in grant and revoke access from webhooks; the API reference is subscriptions in the Fianto docs.
How long should an API key keep working after a failed renewal?
An API key should keep working for at most about a day after a failed renewal, because that is how long Fianto keeps trying before the subscription ends. The attempts are fixed and measured from the period boundary:
| Attempt | When | Event if it fails |
|---|---|---|
| 1 | Boundary + 2 minutes | subscription. and subscription. |
| 2 | Boundary + 1 hour | subscription. |
| 3 | Boundary + 6 hours | subscription. |
| 4 | Boundary + 24 hours | subscription., then subscription. with end_ |
Only wallet-side failures use up an attempt: insufficient funds, a removed approval, a frozen or a closed account. A success on any retry sends subscription. and the subscription is active again. The period is never billed later, so a key you keep open during the retries is a day you may not be paid for.
Our rule of thumb for RPC: keep the key at full limits until the second attempt, then drop it to a free-tier rate limit until subscription. or subscription. arrives. Fianto sends the customer no email, so the past-due event is your cue to tell them their wallet needs USDC before the next attempt in failure.. The customer side is explained in failed renewals and retries.
What happens when a customer upgrades, buys credits or leaves?
Upgrading a tier
There is no plan switching or proration. An upgrade is a new subscription session for the higher tier, then POST /v1/subscriptions/{id}/cancel on the old one. With at: 'now' the old plan stops at once and the customer has paid for days they will not use; with at: 'period_ both plans are live until the old period ends. If you owe a refund, you send USDC back from your own wallet: there is no refund API. A merchant cancel only stops Fianto from charging; it is final and changes nothing on Solana.
Selling extra requests
A subscription charges the same amount every period, so overage cannot be billed after the fact (paying per request is compared in x402 vs subscriptions for APIs). Sell credit packs up front as one-off sessions with mode: 'payment' and add the credits when order. arrives with your order id. One-off payments carry no network fee and no rent.
Leaving
The customer cancels in the portal at fianto.xyz/my. You get subscription. first and subscription. when the paid period runs out, so the key keeps working until the date they paid for. For what the Subscriptions program enforces underneath, see Solana subscriptions, and for the SaaS pitch, USDC billing for SaaS, API and RPC plans.
Checkout sessions, webhooks, the SDKs for Next.js, Express and Hono, and the CLI in one place.
Open the developer overviewWhat should you test before the first paying customer?
- Get the merchant account approved. Until then, API keys answer 401.
- Add the webhook URL to your application and answer the verification challenge. One application has one endpoint.
- Post a signed sample to your local route with
npx @fianto/cli trigger subscription.and check that a key is issued. Each trigger gets a fresh event id, so to test a repeat delivery, sign one payload file withcreated --forward-to http://localhost:3000/api/webhooks/fianto fianto signand post it twice: the second post must change nothing. - Create a subscription session for each tier so its plan is on chain before a customer arrives.
- Subscribe to Starter from a wallet of your own with real USDC, then cancel it in the portal and watch the key stay live until the period end.
There is no test mode, so step five costs one real Starter period plus the fees and rent. That is cheaper than finding a key bug with a paying account. The other test tools are in testing a USDC integration when there is no test mode.