Getting started
This takes a merchant from a fresh account to a live, paid endpoint: link the Canton party that gets paid, create an API key, gate an Express route with @chainsafe/x402-express, and take your first payment. The facilitator does the on-ledger work — you write a few lines of middleware. The overview explains what the facilitator is; this page is the hands-on part. Building the paying side instead? Skip to the buyer side below.
The merchant side: gating an endpoint
Before you start
- A Canton party to receive payouts — you link it in step 1, and it becomes your payout party.
- A Node.js + Express server — the API you want to charge for.
- Your facilitator's base URL,
FACILITATOR_URL. The integration panel on the merchant dashboard's API keys page shows it, with the other values the server below reads, as a.envyou can copy.
Set up your account
Sign in and link your payout party
Sign in to the merchant dashboard with your Canton key. If you signed in another way, link a Canton key on the dashboard's Account page. The key's party is your
payTo: the payer transfers Canton Coin straight to it, and the facilitator holds no balance. A direct transfer needs aTransferPreapprovalon the party — see How you get paid.Create an API key
On the merchant dashboard's API keys page, create a key. It is shown once — copy it now. Your server sends it as
Authorization: Bearerto the facilitator on every verify and settle. The full key is never stored — Your API key is the facilitator credential covers what is kept, rotation, and revocation.
Wire up your server
Install the SDK
Three packages: the Express middleware, the facilitator client it calls, and the shared types and helpers.
bashnpm install @chainsafe/x402-express @chainsafe/x402-client @chainsafe/x402-coreFind your network and asset
Call
GET $FACILITATOR_URL/v2/supportedonce — theexact-cantonentry in itskindsarray carries thenetworkid this facilitator settles on andextra.asset.instrumentId.admin, the DSO party that admins Amulet. You pass both to the middleware below. The integration panel already has them, asNETWORK_IDandAMULET_ADMIN. The API reference describes every field.Gate a route
Point a
FacilitatorClientat the facilitator with your API key, then wrap the route you want to charge for withpaymentRequired(). Withsettle: truethe middleware verifies and settles in one shot; a paid, verified request then reaches your handler with the payer onreq.x402.Raise your server's max header size
The
X-PAYMENTheader runs to tens of KB — past Node's ~16 KB default — so create the server with a largermaxHeaderSize, or the paid retry dies with431 Request Header Fields Too Largebefore your handler ever runs.
Putting it together
import http from "node:http";
import express from "express";
import { FacilitatorClient } from "@chainsafe/x402-client";
import { paymentRequired } from "@chainsafe/x402-express";
import { amuletAsset } from "@chainsafe/x402-core";
// The .env the dashboard's integration panel gives you (API keys page).
const NETWORK = process.env.NETWORK_ID!; // from GET /v2/supported (step 4)
const DSO_PARTY = process.env.AMULET_ADMIN!; // from GET /v2/supported (step 4)
const PAY_TO = process.env.MERCHANT_PARTY!; // your payout party (step 1)
const API_KEY = process.env.FACILITATOR_API_KEY!; // your API key (step 2)
const facilitator = new FacilitatorClient(process.env.FACILITATOR_URL!, { apiKey: API_KEY });
const app = express();
// Gate one route behind a 0.05 CC payment, settled on Canton.
app.use(
"/paid",
paymentRequired({
facilitator,
settle: true,
requirements: {
network: NETWORK,
payTo: PAY_TO,
asset: amuletAsset(DSO_PARTY),
amount: "0.05",
description: "Access to /paid",
},
}),
);
app.get("/paid", (req, res) => {
// Paid + verified — req.x402 carries the payer and the settle result.
res.json({ ok: true, payer: req.x402!.payer });
});
// Raise maxHeaderSize (step 6) or the paid retry 431s before the handler runs.
http
.createServer({ maxHeaderSize: 4 * 1024 * 1024 }, app)
.listen(3000, () => console.log("listening on :3000"));Its settings are the integration panel's .env: FACILITATOR_URL, FACILITATOR_API_KEY, NETWORK_ID, MERCHANT_PARTY and AMULET_ADMIN.
Take your first payment
Pay the route and confirm it
Call your gated route from a payer wired up with the payer SDK (the buyer side below shows how). Once it settles, the payment shows up on the merchant dashboard's Settlements page with its on-ledger update id.
The buyer side: paying for one
If your service is the one calling a paid API, you pay from the other side with @chainsafe/x402-server-sdk. Given your Canton party key and a wallet-sdk ledger connection, it prepares and Ed25519-signs a Canton Coin transfer and wraps fetch so a 402 is paid and the request retried transparently — you call the URL and get the paid response back.
Before you start
- A funded payer party — its
partyIdand Ed25519 keypair (publicKey/privateKey), holding enough Canton Coin to cover the price. - A connection to your participant's Ledger API — its URL plus OAuth2 client credentials (LocalNet uses a self-signed token instead).
- The Amulet Token Standard registry URL — your validator's scan-proxy for Amulet, where the payer fetches the
TransferFactory. - The DSO party (the Amulet asset admin) —
extra.asset.instrumentId.adminfromGET $FACILITATOR_URL/v2/supportedon the facilitator the API you are paying uses.
Steps
Install the payer SDK
The payer SDK and the Canton wallet-sdk it signs with.
bashnpm install @chainsafe/x402-server-sdk @canton-network/wallet-sdkConnect and build the payer
Pick the network preset (
devnetConfig,mainnetConfig, orlocalnetConfig), open a wallet-sdk connection withSDK.create(), then construct aCantonX402Payerfrom your party key and the Amulet registry. The payer only pays requirements on its ownnetwork, so that must be thenetworkthatGET $FACILITATOR_URL/v2/supportedreturns; passnetworkto the preset to override its default.Wrap fetch and call the endpoint
createX402Fetch(payer)returns afetchthat pays402s for you. Call the gated URL through it and read the paid response — no 402 handling in your own code.
Putting it together
import { SDK } from "@canton-network/wallet-sdk";
import { CantonX402Payer, createX402Fetch, devnetConfig } from "@chainsafe/x402-server-sdk";
const net = devnetConfig({ ledgerClientUrl: process.env.LEDGER_CLIENT_URL! });
const registryUrl = process.env.AMULET_REGISTRY_URL!; // Amulet Token Standard registry
const DSO_PARTY = "DSO::1220be58c2…"; // from GET /v2/supported
// OAuth2 for your participant's Ledger API (localnet uses method: "self_signed").
const auth = {
method: "client_credentials",
configUrl: process.env.CANTON_OAUTH_CONFIG_URL!,
credentials: {
clientId: process.env.CANTON_OAUTH_CLIENT_ID!,
clientSecret: process.env.CANTON_OAUTH_CLIENT_SECRET!,
scope: net.auth.scope,
audience: net.auth.audience,
},
} as const;
const sdk = await SDK.create({
auth,
ledgerClientUrl: net.ledgerClientUrl,
token: { auth, registries: [registryUrl] },
});
const payer = new CantonX402Payer({
sdk,
key: {
partyId: process.env.PAYER_PARTY_ID!,
publicKey: process.env.PAYER_PUBLIC_KEY!,
privateKey: process.env.PAYER_PRIVATE_KEY!,
},
network: net.network,
registries: [{ instrumentId: { id: "Amulet", admin: DSO_PARTY }, registryUrl }],
});
// A fetch that pays 402s automatically.
const pay = createX402Fetch(payer);
// Call the gated endpoint — the 402 is paid and the request retried for you.
const res = await pay("https://api.example.com/paid");
const data = await res.json();
console.log(data);The examples/mortgage pair in the SDK repo is a full runnable payer + merchant, with the complete credential wiring in its .env.example.
Next
The payment flow walks every request end to end, and the API reference has the full request and response spec for /v2/verify and /v2/settle.