No Fianto fee during the beta. Any fee comes with at least 30 days notice. See pricing
Developers9 min read

How to test a USDC integration when there is no test mode.

Fianto has no test mode, no test keys and no hosted devnet: its API settles real USDC on Solana mainnet. You still test almost everything without spending money, by unit-testing your webhook route with signed sample events, replaying any event type with fianto trigger, and asking for a real signed test.event. The checkout itself you test once with a real payment of 0.01 USDC, the lowest price allowed, sent from a second wallet of your own back to yourself.

On this page
Why Fianto has no sandboxWhich tool tests which partUnit-test the webhook route with signed samplesReplay every event type with the CLIRun one real payment for 0.01 USDCCan you test with devnet USDC instead?A pre-launch checklistSourcesQuestions
TL;DR
  • Webhook logic is testable offline: signWebhook and sampleEvent from @fianto/sdk/webhooks sign a sample exactly as Fianto does.
  • fianto trigger <type> --forward-to <local url> posts a signed sample of any of the 14 event types to your route, with a fresh event id each run and no API call.
  • The payment path needs mainnet: a one-off 0.01 USDC payment from your own second wallet costs a Solana network fee under $0.01, and the 0.01 USDC lands back in your receiving wallet.

Why Fianto has no sandbox

A Fianto deployment runs on exactly one Solana cluster, and the hosted one at api.fianto.xyz has run on mainnet since 6 October 2026. A test mode would need a second deployment on devnet with its own merchants, plans and wallets, and Fianto does not run one. The docs say it in their first line on the subject: "Fianto has no test mode and no test keys." Every secret starts with fian_sk_live_: there is no test key to switch to.

That sounds worse than it is. Most integration bugs live in your webhook route: a missed event type, a double grant because an event arrived twice, a body parsed before verification. None of those need money to find. What does need mainnet is the part you do not write, the hosted checkout and the wallet, and one real payment covers it.

Which tool tests which part

ToolWhat it provesNeedsCost
A unit test with signWebhookYour handler verifies, routes and dedupes each event typeNothing but your test runnerFree
fianto trigger <type> --forward-toYour running route accepts real-shaped, signed samplesThe webhook secret onlyFree, no API call
fianto trigger test.eventFianto can reach your public URL and your signature check passesApp id and secret, a verified webhook URLFree, 10 an hour per application
fianto events tail --forward-toYour local route handles your real eventsApp id, secret and webhook secretFree
One real 0.01 USDC paymentSession creation, the checkout, the wallet and order.paid end to endAn approved account, a second wallet with USDC and a little SOLA network fee under $0.01
One real 0.01 USDC subscriptionPlan creation, the subscribe transaction and subscription.createdThe same, plus SOL for account rent0.01 USDC network fee today, plus non-refundable wallet setup rent

Work down the table. The first two rows make no API call, so they work before your merchant account is approved. test.event and events tail call the API, which answers 401 to every key until approval.

Unit-test the webhook route with signed samples

@fianto/sdk/webhooks exports sampleEvent(type), a realistic event of any type with obviously fake ids such as fian_sub_sample, and signWebhook({ event, secret }), which signs it the way Fianto signs a delivery and returns the body and headers. Feed both to your route handler as a plain Request. This test assumes the Next.js route from accepting USDC in a Next.js app, with its data layer and the SDK client stubbed:

test/webhook-route.test.ts
import { beforeAll, expect, it, vi } from 'vitest';
import { sampleEvent, signWebhook } from '@fianto/sdk/webhooks';

// The route re-reads each subscription through the SDK and saves it: stub both.
vi.mock('@/lib/billing', () => ({ saveSubscription: vi.fn(async () => undefined) }));
vi.mock('@fianto/sdk', async () => {
  const { sampleEvent } = await import('@fianto/sdk/webhooks');
  return {
    Fianto: class {
      subscriptions = { retrieve: async () => sampleEvent('subscription.created').data };
    },
  };
});

// Any whsec_ key of 16 bytes or more; this one never leaves the test.
const SECRET = `whsec_${Buffer.alloc(32, 7).toString('base64')}`;
let POST: (request: Request) => Promise<Response>;

beforeAll(async () => {
  process.env.FIANTO_WEBHOOK_SECRET = SECRET; // set before the route module loads
  ({ POST } = await import('../app/api/webhooks/fianto/route'));
});

const deliver = (body: string, headers: Record<string, string>) =>
  POST(new Request('http://localhost/api/webhooks/fianto', { method: 'POST', headers, body }));

it('accepts a signed subscription.created', async () => {
  const event = sampleEvent('subscription.created', { id: `evt_${'a'.repeat(32)}` });
  const { body, headers } = await signWebhook({ event, secret: SECRET });
  expect((await deliver(body, headers)).status).toBe(200);
});

it('refuses a body changed after signing', async () => {
  const event = sampleEvent('subscription.ended', { id: `evt_${'b'.repeat(32)}` });
  const { body, headers } = await signWebhook({ event, secret: SECRET });
  expect((await deliver(body.replace('sample', 'forged'), headers)).status).toBe(400);
});

Add one test per event type you handle, and one that delivers the same event twice and checks you granted access once. Delivery is at least once, so the second case is not hypothetical. Why the tampered body fails, and what the signature covers, is in verifying Standard Webhooks signatures. A grant keyed by subscription and period, as in accepting USDC for an AI app, passes the duplicate test by design.

Replay every event type with the CLI

The @fianto/cli package installs a fianto command. With --forward-to, trigger signs a local sample and posts it to your running dev server; it makes no API call and needs only the webhook secret. Without --forward-to it sends nothing at all, except for test.event, and any other type exits with code 2.

terminal
# A signed sample to your local route. Each run gets a fresh event id.
npx @fianto/cli trigger subscription.past_due \
  --forward-to http://localhost:3000/api/webhooks/fianto \
  --secret "$FIANTO_WEBHOOK_SECRET"

# A real, signed test.event from Fianto to your verified public URL.
npx @fianto/cli trigger test.event

# Your application's real events, re-signed and posted to your machine.
npx @fianto/cli events tail \
  --forward-to http://localhost:3000/api/webhooks/fianto \
  --since 1h
  • trigger --forward-to accepts only localhost, 127.0.0.0/8 or [::1] unless you add --allow-remote, because a signed sample at a public URL looks like a real delivery. It prints → 200 subscription.past_due (local sample) and exits 1 on any status that is not 2xx.
  • trigger test.event needs FIANTO_APP_ID and FIANTO_APP_SECRET exported in the shell or passed as flags (the CLI does not read .env files) and a verified webhook URL. Its data is { "message": "This is a test event from Fianto." }. Ten an hour per application, shared with the dashboard's test button.
  • events tail polls every 2 seconds by default and forwards each event once per run, with no retry. It is a development aid; real deliveries still go to your verified URL.

Fianto never sends a business event such as order.paid or subscription.renewed on request. Those come only from real payments, which is the next step.

Run one real payment for 0.01 USDC

A real payment on mainnet is the only way to see the hosted checkout, a real wallet signature and a real order.paid. Keep it small: 0.01 USDC is the lowest price Fianto accepts. Pay from a second wallet you control, so the money comes back to you.

  1. Get the merchant account approved. Until then the API refuses your keys.
  2. Fund a second wallet with a little USDC and some SOL. The checkout tells you the exact SOL the wallet needs; for a one-off payment that is the wallet's own rent-exempt minimum (650,240 lamports when measured on 15 October 2026), the network fee and a small margin.
  3. Create a mode: payment session with amount: '0.01' and a description, through your own checkout route or the SDK.
  4. Open it, connect the second wallet and sign. The page shows success at confirmed; order.paid and checkout.session.completed arrive at finalized.
  5. Check that your route stored the order once, then find the payment under Payments in the dashboard and the transfer in your receiving wallet.

The whole run costs the second wallet a Solana network fee under $0.01. The 0.01 USDC price lands in your receiving wallet, and there is no Fianto fee during the beta; after it, the customer pays about 0.1 to 0.5% on top, and the merchant still pays nothing.

Testing a subscription for real

A subscription test costs a little more and teaches less, because the first renewal comes 30 days and two minutes after the start and nothing on mainnet fast-forwards it. Create a monthly price of 0.01 USDC in the dashboard, then a subscription session for it. The first session creates the price's plan on Solana, so the page shows the plan as preparing for a moment. The subscribe transaction then charges the first period: 0.01 USDC to you plus a network fee of 0.01 USDC today to Fianto's treasury, on plans created since 6 October 2026.

The wallet also pays account rent in SOL, read live at subscribe time and shown on the checkout. As of 15 October 2026 the subscription account took about 0.00144 SOL, returned when the wallet reclaims it after the subscription ends, and the one-time wallet setup account about 0.00119 SOL, not returned. To end the test, cancel from your server with fianto.subscriptions.cancel(id, { at: 'now' }) and watch subscription.ended arrive with end_reason: MERCHANT_CANCELED. That cancel changes nothing on Solana, so to get the rent back the wallet uses "Cancel on Solana" in the portal at fianto.xyz/my, then reclaims it once the paid period has ended on chain, up to 30 days later for a monthly test.

Signed webhooks, the CLI and the SDKs on one page.

Open the developer overview

Can you test with devnet USDC instead?

No: devnet USDC cannot pay a Fianto checkout, because the hosted API at api.fianto.xyz settles on mainnet and a devnet token is a different token from its mainnet namesake. Circle's address list gives the devnet USDC mint as 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU and the mainnet mint as EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (checked on 15 October 2026). A mint is the on-chain account that defines a token, and a checkout accepts only the one its cluster uses.

So the test plan stays the one above: signed samples for every event type, one test.event for the network path, and one small real payment for the checkout. Every CLI flag and exit code is on the docs page for the Fianto CLI.

A pre-launch checklist

  1. Each event type you handle has a unit test, plus a duplicate-delivery test.
  2. A trigger --forward-to run returns 200 for every type, and a run with the wrong secret returns 400.
  3. A real test.event reached your production URL and shows in fianto events list --type test.event.
  4. Your handler answers within 10 seconds; anything slower counts as a failed attempt and is retried, up to 8 attempts over about 28 hours.
  5. One real 0.01 USDC payment produced exactly one fulfilment.
  6. Your success page says "confirming", not "paid": the browser's result is not proof of payment.

If the one real payment shows money in your wallet but no paid order, the order can still turn paid at finalized; how a merchant knows a crypto payment arrived walks through those cases.

Sources
Questions

Questions about testing USDC payments.

Does Fianto have a test mode or sandbox?

No. There are no test keys and no hosted devnet; the hosted API settles real USDC on Solana mainnet. You test webhook handling with signed samples and a test.event, and the payment path with one small real payment.

How much does a real test payment cost?

A one-off payment of 0.01 USDC from your own second wallet costs that wallet a Solana network fee under $0.01, and the 0.01 USDC lands in your receiving wallet. There is no Fianto fee during the beta.

Can I trigger order.paid on my production endpoint?

No. Fianto sends only test.event on request. fianto trigger order.paid works with --forward-to and posts a signed local sample to your machine.

Can I pay a Fianto checkout with devnet USDC from a faucet?

No. The hosted Fianto API settles on Solana mainnet, and devnet USDC is a different token with its own mint, so a Fianto checkout cannot accept it.

How do I test a renewal without waiting 30 days?

Post signed subscription.renewed, subscription.payment_failed and subscription.past_due samples to your route with fianto trigger --forward-to. A real renewal on mainnet comes only after the 30-day period.

Read next.

Developers9 min read

Accept USDC in a Next.js app, and gate pages by subscription status

To accept USDC in a Next.js App Router app, install @fianto/nextjs, export Checkout() from a route that creates the session and Webhooks() from a route that receives signed events, then render FiantoButton from @fianto/react. Grant and revoke access from the webhook, never from the browser: store each subscription's status (ACTIVE, PAST_DUE or ENDED) and let your pages read it.

Developers9 min read

Verify Standard Webhooks signatures for payment events

Verifying a webhook signature means recomputing an HMAC-SHA256 over the exact bytes you received and comparing it, in constant time, with the one in the request. Under the Standard Webhooks format that Fianto uses, the signed content is webhook-id.webhook-timestamp.body, the key is the base64 part of your whsec_ secret, and the result arrives as v1,<base64> in the webhook-signature header. Check the timestamp too, then dedupe on the id.

Use casesDevelopers10 min read

Paid Discord roles with USDC: a bot, a checkout and two webhook events

To sell a Discord role for USDC, run a bot that holds the Manage Roles permission, let a /join command open a Fianto subscription checkout with the member's Discord user ID as the customer reference, and add or remove the role when Fianto's subscription.created and subscription.ended webhooks arrive. Fianto has no Discord integration of its own: it collects the USDC into your wallet and tells your server what happened, and your bot does the Discord part.

Test the route, then pay once.

Samples for each event type, one real 0.01 USDC payment for the rest.

New posts are announced on X and Discord.