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

Accept USDC payments in a Hono app, on Node, Cloudflare Workers or Bun.

Hono takes USDC payments through @fianto/hono's two handlers: checkout() creates a one-off checkout session for your pay button, and webhooks() verifies the order.paid event you fulfil from. Both pass Hono's untouched c.req.raw to the SDK and use only fetch, Request, Response and Web Crypto. On Node the code reads process.env; on Cloudflare Workers you build the handlers inside the route from c.env, and Fianto's CI does not yet test Workers or Bun.

On this page
Which runtimes can run Fianto with Hono?How do you add USDC checkout to a Hono app on Node?How do you accept USDC on Cloudflare Workers with Hono?Does the Hono code change on Bun?What do the Fianto Hono handlers do for you?What does a one-off USDC payment cost through Hono?How do you check the Hono webhook route before a real payment?SourcesQuestions
TL;DR
  • @fianto/hono 0.3.0 exports checkout() and webhooks(), lowercase, for hono 4 or later.
  • On Workers there is no process.env: pass new Fianto({ appId, appSecret }) and the webhook secret from c.env, inside the route.
  • Fianto CI tests the SDK on Node.js 22 and 24; Workers, Bun and Deno are designed for but not tested yet.

Which runtimes can run Fianto with Hono?

Fianto's Hono handlers are written for any runtime with the Fetch API, but only Node is tested. The Fianto docs put it plainly: "CI tests the SDK on Node.js 22 and 24 and smoke-tests the built @fianto/sdk on Node.js 20.3.0; Workers, Bun and Deno are not tested yet." (Fianto docs: Hono, read 15 October 2026.) Plan for that: run one real payment end to end on the runtime you deploy to before you take customers there.

NodeCloudflare WorkersBun
Where secrets come fromprocess.env, loaded with node --env-file=.env (Node 20.6+)c.env bindings, only inside a requestprocess.env; Bun reads .env files itself
How the app starts@hono/node-server serve()export default appexport default app
Where to build the handlersModule levelInside each routeModule level
Tested in Fianto CI (15 Oct 2026)Yes, Node 22 and 24NoNo

One detail from the SDK source shows Workers was considered in the code, not only in the docs. The SDK sends every API request with redirect: 'manual', because its comments note that redirect: 'error' throws while the request is being built on workerd, the Workers runtime, which would fail every call there. The same comments record how workerd passes a redirect response through. Workers has been looked at closely; it is just not in the test matrix.

How do you add USDC checkout to a Hono app on Node?

On Node you add USDC checkout with two app.post routes and @hono/node-server to listen. Install @fianto/hono, @fianto/sdk and hono; the packages need Node 20.3 or later. The handlers read FIANTO_APP_ID, FIANTO_APP_SECRET and FIANTO_WEBHOOK_SECRET from the environment. Price and order_id are decided here, on your server, from your own data; the browser only says which item.

src/index.ts
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { checkout, webhooks } from '@fianto/hono';
import { orders } from './orders.js'; // your own data layer

const ITEMS: Record<string, { amount: string; description: string }> = {
  report: { amount: '25.00', description: 'Quarterly market report (PDF)' },
};

const app = new Hono();

app.post(
  '/api/checkout',
  checkout({
    createSession: async (request) => {
      const body = (await request.json().catch(() => null)) as { item?: string } | null;
      const item = body?.item === undefined ? undefined : ITEMS[body.item];
      if (item === undefined) return new Response('Unknown item', { status: 400 });
      const order = await orders.open(item); // your row, with its own id

      return {
        mode: 'payment',
        order_id: order.id,
        amount: item.amount,
        description: item.description,
        success_url: 'https://reports.example/orders/confirming',
        cancel_url: 'https://reports.example/',
      };
    },
    onError: (error) => console.error('fianto checkout failed', error),
  }),
);

app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    onEvent: async (event) => {
      if (event.type === 'order.paid') {
        // One transaction: record event.id (skip if seen), mark the order paid, deliver.
        await orders.markPaid(event.id, event.data.order_id);
      }
    },
    onVerificationError: (error) => console.warn('fianto webhook rejected:', error.reason),
  }),
);

serve({ fetch: app.fetch, port: 3000 });

Keep body-reading middleware off both routes. Hono hands the SDK c.req.raw, the original Request, and the webhook signature is checked over its exact bytes. Fulfil from order.paid only: the checkout page shows the payer success at confirmed, but Fianto marks the order PAID and sends order.paid only at finalized. What those two words mean on Solana is in Solana payment finality.

How do you accept USDC on Cloudflare Workers with Hono?

On Cloudflare Workers you accept USDC with the same handlers, built inside each route so they can read c.env. Hono's Workers guide says you can bind environment values and "access them in c.env" (Hono: Cloudflare Workers, read 15 October 2026). Workers has no process.env, so the default new Fianto() finds no credentials. A handler created at module level runs before any binding exists, which is why the factory call moves inside the route.

src/index.ts
import { Hono } from 'hono';
import { Fianto } from '@fianto/sdk';
import { checkout, webhooks } from '@fianto/hono';

interface Bindings {
  FIANTO_APP_ID: string;
  FIANTO_APP_SECRET: string;
  FIANTO_WEBHOOK_SECRET: string;
}

const app = new Hono<{ Bindings: Bindings }>();

app.post('/api/checkout', (c) =>
  checkout<{ Bindings: Bindings }>({
    fianto: new Fianto({ appId: c.env.FIANTO_APP_ID, appSecret: c.env.FIANTO_APP_SECRET }),
    createSession: async () => ({
      mode: 'payment',
      order_id: `report_${crypto.randomUUID()}`, // better: your own order's id
      amount: '25.00',
      description: 'Quarterly market report (PDF)',
      success_url: 'https://reports.example/orders/confirming',
      cancel_url: 'https://reports.example/',
    }),
    onError: (error) => console.error('fianto checkout failed', error),
  })(c),
);

app.post('/webhooks/fianto', (c) =>
  webhooks({
    secret: c.env.FIANTO_WEBHOOK_SECRET,
    onEvent: async (event) => {
      if (event.type === 'order.paid') {
        console.log('paid', event.data.order_id, event.id); // write to D1 or KV here
      }
    },
  })(c),
);

export default app;

Set the three values as Worker secrets for production, and in a .dev.vars file for local runs, which Hono's guide describes. Two failures point straight at a missed binding. The webhook route answers 400 invalid_webhook with reason invalid_secret when no secret reached it. The checkout route answers 500, and onError logs "Missing appId: pass it to new Fianto({ appId }) or set FIANTO_APP_ID." For a self-hosted or local Fianto backend, also pass baseUrl to new Fianto().

Keys, sessions, signed webhooks and the CLI, in one place.

See the developer overview

Does the Hono code change on Bun?

On Bun the Node version of the app needs only a different start line. Bun's docs say it "reads your .env files automatically" and that Bun.env and import.meta.env are "aliases of process.env" (Bun: environment variables, read 15 October 2026), so the default new Fianto() and process.env.FIANTO_WEBHOOK_SECRET find their values. Drop @hono/node-server and end the file with export default app, which Hono's Bun guide uses. Bun is not in Fianto's CI, so treat your first real payment there as the test.

What do the Fianto Hono handlers do for you?

The two handlers carry the parts of a payment integration that are easy to get wrong by hand. An AI coding agent given the Fianto agent skill builds the same two routes with @fianto/hono. Each line below is behaviour in the SDK source, not a recommendation.

  • Same-site check. checkout() accepts a POST only with Sec-Fetch-Site: same-origin, or an Origin in allowedOrigins (by default the request URL's own origin). Anything else is 403 forbidden_origin.
  • Popup sessions. It always creates the session with ui_mode popup and answers { id, url } for the pay button.
  • Double clicks. When the order_id already has an open session with the same terms, it reissues that session's link instead of failing. Different terms are refused with 409 order_session_mismatch.
  • Safe errors. Only an allowlisted set of error codes reaches the browser; everything else, wrong credentials included, is a generic 500 internal_error, with the original passed to onError.
  • Verification. webhooks() checks headers and timestamp (300 s tolerance by default) before reading the body, caps the body at 1 MiB, and answers the endpoint.verification challenge itself.
  • Retries. A callback that throws makes the route answer 500, so Fianto delivers the event again.

What the handlers cannot do is make your callback idempotent. Delivery is at least once and unordered, so record event.id in the same transaction as the fulfilment and skip ids you have seen. The signature scheme itself, and how to check it without the SDK, is covered in verifying Standard Webhooks signatures.

What does a one-off USDC payment cost through Hono?

A one-off USDC payment costs the merchant nothing, whichever runtime serves the route. The amount you return from createSession is what lands in your wallet, in the payer's one transaction, with nobody holding it in between. During the beta there is no Fianto fee; after it, the payer pays about 0.1 to 0.5% on top. One-off payments carry no USDC network fee and need no account rent. The payer signs one Solana transaction with a fee under $0.01, and the checkout tells them the exact SOL their wallet needs.

The amount is a decimal string from 0.01 to 1,000,000 USDC with at most 6 decimals, so '25.00' works and a JSON number does not. Your receiving wallet must already hold a USDC token account, or creating a session fails with 422 merchant_token_account_missing. Sending that wallet any amount of USDC once creates it. The wider picture, from wallet to first payment, is in how to accept USDC on Solana.

How do you check the Hono webhook route before a real payment?

You check the Hono webhook route by sending it a signed sample from the Fianto CLI, which makes no API call and moves no money.

  1. Export the secret in the shell that runs the CLI: export FIANTO_WEBHOOK_SECRET=whsec_.... The CLI does not read .env files.
  2. Start the app: port 3000 in the Node example, 8787 for the Workers dev server that Hono's guide uses.
  3. Run npx @fianto/cli trigger order.paid --forward-to http://localhost:3000/webhooks/fianto. Without --forward-to, nothing is sent.
  4. Expect "→ 200 order.paid (local sample)" in the terminal, and your onEvent branch to run for the sample order. Each run is a new event id.

There is no test mode or sandbox: Fianto runs on Solana mainnet. Once samples pass, make one real 0.01 USDC payment on the runtime you deploy. The full routine is in testing a USDC integration without a test mode, and the same app written for Next.js is accepting USDC in a Next.js app.

Sources
Questions

Questions about USDC payments with Hono.

Does @fianto/hono work on Cloudflare Workers?

It is designed to: the handlers use only fetch, Request, Response and Web Crypto, and need no Node polyfills. As of 15 October 2026 Fianto CI does not test Workers, so run a real payment there before launch.

Why do my Fianto handlers fail on Workers when they work on Node?

Workers has no process.env, so the defaults find no app id, app secret or webhook secret. Build the handlers inside the route and pass the values from c.env.

Can a Hono app sell subscriptions too?

Yes. Return mode: 'subscription' with a recurring price_id from createSession, and handle the subscription events instead of order.paid, which never fires for a subscription.

Can I use Deno with @fianto/hono?

The handlers are written for any Fetch API runtime, Deno included, but Fianto does not test Deno yet.

Which tokens can a Hono app accept through Fianto?

USDC on Solana only. Fianto accepts no other token, no other chain and no cards.

Read next.

Developers8 min read

Add USDC checkout with an AI coding agent: the Fianto agent skill

Run npx skills add fianto-xyz/skills in your project, then ask your coding agent to add Fianto checkout: the skill gives it the real API, the official @fianto/* packages and the rules that keep money safe. It cannot create your merchant account, your API keys or your webhook secret, and it is told to ask you before anything irreversible, such as cancelling a real subscription or moving USDC.

Developers9 min read

Idempotency keys and order ids: how a payments API avoids charging twice

An idempotency key is a unique value a client sends with a write so that a retry returns the first result instead of doing the work again; it protects one HTTP request. An order id protects the business event: Fianto refuses a second checkout for an order that is already paid with 409 order_already_paid. Fianto requires an Idempotency-Key on every v1 POST, keeps it for 24 hours, and marks a replay with Idempotent-Replayed: true.

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.

Build the handler where the secrets are.

Module level on Node and Bun, inside the route on Workers.

New posts are announced on X and Discord.