A facilitator verifies and settles payments on-chain. By default, faremeter uses the Corbits facilitator (https://facilitator.corbits.dev). If you need to run your own, use @faremeter/facilitator.
pnpm add @faremeter/facilitator hono
When to run your own
- You need to support custom payment schemes not available on the Corbits facilitator.
- You want full control over payment verification and settlement.
- You are running in an environment that cannot reach the Corbits facilitator.
For most development and production use cases, the Corbits facilitator is sufficient.
adaptSettleResponseV2ToV1Legacy is deprecated. Use adaptSettleResponseV2ToV1 for spec-compliant v1 output. See the Facilitator API reference for details on version adapters.
Basic setup
import { Hono } from "hono"
import { createFacilitatorRoutes } from "@faremeter/facilitator"
const handlers = [
// Chain-specific handlers
]
const app = new Hono()
app.route("/", createFacilitatorRoutes({ handlers }))
export { app }
createFacilitatorRoutes returns a Hono router with the standard facilitator endpoints: /accepts, /verify, /settle, and /supported.
Creating handlers
Each chain has its own createFacilitatorHandler function that returns a FacilitatorHandler. Install the payment package for the chain you want to support.
Solana handler
pnpm add @faremeter/payment-solana @solana/kit @solana/web3.js @solana-program/node-helpers
import { createFacilitatorHandler } from "@faremeter/payment-solana/exact"
import { createSolanaRpc } from "@solana/kit"
import { getKeypairFromFile } from "@solana-program/node-helpers"
import { PublicKey } from "@solana/web3.js"
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!)
const feePayerKeypair = await getKeypairFromFile(process.env.SOLANA_PAYER_KEYPAIR!)
const mint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") // USDC
const solanaHandler = await createFacilitatorHandler(
"devnet",
rpc,
feePayerKeypair,
mint,
)
Signature: createFacilitatorHandler(network, rpc, feePayerKeypair, mint, config?)
The optional config parameter accepts:
| Option | Type | Default | Description |
|---|
maxRetries | number | 30 | Maximum retries for transaction submission. |
retryDelayMs | number | 1000 | Delay between retries in milliseconds. |
maxPriorityFee | number | 100000 | Maximum priority fee in lamports. |
maxTransactionAge | number | 150 | Maximum age of a transaction (in slots) before it expires. |
features.enableSettlementAccounts | boolean | — | Enable settlement account support. |
features.enableDuplicateCheck | boolean | — | Prevent duplicate transaction submission. |
hooks | FacilitatorHooks[] | — | Lifecycle hooks for afterVerify and afterSettle. |
const solanaHandler = await createFacilitatorHandler(
"devnet",
rpc,
feePayerKeypair,
mint,
{
maxRetries: 30,
retryDelayMs: 1000,
features: { enableDuplicateCheck: true },
hooks: [
{
afterSettle: async ({ response, requirements, payment, logger }) => {
logger.info("Settlement complete", { transaction: response.transaction })
},
},
],
},
)
EVM handler
pnpm add @faremeter/payment-evm viem
import { createFacilitatorHandler } from "@faremeter/payment-evm/exact"
const evmHandler = await createFacilitatorHandler(
{ id: 84532, name: "base-sepolia", rpcUrls: { default: { http: [process.env.EVM_RPC_URL!] } } },
process.env.EVM_PRIVATE_KEY!,
"USDC",
)
Signature: createFacilitatorHandler(chain, privateKey, assetNameOrInfo, opts?)
| Parameter | Type | Description |
|---|
chain | ChainInfoWithRPC | Object with id, name, and rpcUrls: { default: { http: [url] } }. |
privateKey | string | Hex-encoded private key for transaction submission. |
assetNameOrInfo | AssetNameOrContractInfo | Asset name (e.g., "USDC") or contract info object. |
opts | object | Optional. network (KnownX402Network) and transport (viem Transport). |
Multi-chain configuration
Wire handlers for each chain together and pass them to createFacilitatorRoutes:
import { Hono } from "hono"
import { createFacilitatorRoutes } from "@faremeter/facilitator"
import { createFacilitatorHandler as createSolanaHandler } from "@faremeter/payment-solana/exact"
import { createFacilitatorHandler as createEvmHandler } from "@faremeter/payment-evm/exact"
import { createSolanaRpc } from "@solana/kit"
import { getKeypairFromFile } from "@solana-program/node-helpers"
import { PublicKey } from "@solana/web3.js"
// Solana
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!)
const feePayerKeypair = await getKeypairFromFile(process.env.SOLANA_PAYER_KEYPAIR!)
const solanaMint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
// EVM
const chain = { id: 84532, name: "base-sepolia", rpcUrls: { default: { http: [process.env.EVM_RPC_URL!] } } }
const handlers = [
await createSolanaHandler("devnet", rpc, feePayerKeypair, solanaMint),
await createEvmHandler(chain, process.env.EVM_PRIVATE_KEY!, "USDC"),
]
const app = new Hono()
app.route("/", createFacilitatorRoutes({ handlers }))
export { app }
Each handler implements the FacilitatorHandler interface:
import type { FacilitatorHandler, GetRequirementsArgs } from "@faremeter/types/facilitator"
interface FacilitatorHandler {
capabilities?: HandlerCapabilities
getSupported?: () => Promise<x402SupportedKind>[]
getSigners?: () => Promise<Record<string, string[]>>
getRequirements: (args: GetRequirementsArgs) => Promise<x402PaymentRequirements[]>
handleVerify?: (requirements, payment) => Promise<x402VerifyResponse | null>
handleSettle: (requirements, payment) => Promise<x402SettleResponse | null>
}
Handlers return null for payment schemes they do not support, allowing the facilitator to route to the correct handler.
The optional capabilities field declares what schemes, networks, and assets a handler supports. The middleware uses this to route ResourcePricing entries to the right handler without calling it. While optional on the interface, capabilities is effectively required for in-process usage — the middleware skips handlers that don’t declare capabilities when routing pricing entries.
Using handlers in-process
The same handlers you pass to createFacilitatorRoutes can be passed directly to the middleware via x402Handlers, eliminating the need for a separate facilitator process. See the Middleware Overview for details.
If you have an existing remote facilitator and want to use it through the in-process interface, wrap it with createHTTPFacilitatorHandler:
import { createHTTPFacilitatorHandler } from "@faremeter/middleware/http-handler"
const handler = createHTTPFacilitatorHandler("https://facilitator.corbits.dev", {
capabilities: { networks: ["solana:devnet"], assets: ["USDC"] },
})
Lifecycle hooks
Hooks let you run custom logic after the facilitator verifies or settles a payment. Use them for logging, analytics, webhooks, or downstream side effects.
Pass an array of hook objects to the handler’s hooks option. Each object can define afterVerify, afterSettle, or both:
const handler = await createFacilitatorHandler(
"devnet",
rpc,
feePayerKeypair,
mint,
{
hooks: [
{
afterVerify: async ({ requirements, response, payment, logger }) => {
logger.info("Payment verified", {
scheme: requirements.scheme,
isValid: response.isValid,
})
},
afterSettle: async ({ response, requirements, payment, logger }) => {
logger.info("Payment settled", { transaction: response.transaction })
// Send a webhook, update a database, trigger fulfillment, etc.
},
},
],
},
)
Hook parameters
Both hooks receive a context object:
| Parameter | Available in | Description |
|---|
requirements | afterVerify, afterSettle | The matched payment requirements. |
payment | afterVerify, afterSettle | The decoded payment payload from the client. |
logger | afterVerify, afterSettle | A structured logger instance from @faremeter/logs. |
response | afterVerify, afterSettle | The operation result. In afterVerify: { isValid, payer?, invalidReason? }. In afterSettle: { success, transaction, network, payer?, errorReason?, extensions? }. |
Multiple hooks
You can pass multiple hook objects. They run in order:
hooks: [
{ afterSettle: async (ctx) => { /* logging */ } },
{ afterSettle: async (ctx) => { /* webhook */ } },
]
Hook behavior
Hooks can optionally replace the response by returning a new value. If a hook returns a non-undefined value, that value becomes the response passed to subsequent hooks and returned to the client.
Hook errors are not caught by the facilitator — a throwing hook will cause the request to fail with a 500 error, even though the payment may have already been verified or settled on-chain. Ensure your hooks handle their own errors internally if you want settlement to proceed regardless.
Settlement accounts
The enableSettlementAccounts feature (Solana only) enables an intermediate settlement account flow. When enabled, payments are first settled into a facilitator-controlled settlement account rather than directly to the merchant’s payTo address. This is useful when the facilitator needs to hold funds temporarily before distributing them — for example, to support refunds, escrow patterns, or fee splitting.
const solanaHandler = await createFacilitatorHandler(
"devnet",
rpc,
feePayerKeypair,
mint,
{
features: { enableSettlementAccounts: true },
},
)
Settlement accounts are an advanced feature. For most integrations, leave this disabled and payments will settle directly to the payTo address.
Environment configuration
Facilitator handlers typically need:
- RPC endpoint URLs for each chain
- Private keys for transaction submission (the facilitator submits transactions on behalf of clients)
- Token contract addresses
Configure these via environment variables:
SOLANA_RPC_URL=https://api.devnet.solana.com
SOLANA_PAYER_KEYPAIR=~/.config/solana/facilitator.json
EVM_RPC_URL=https://sepolia.base.org
EVM_PRIVATE_KEY=0x...
Facilitator endpoints
| Endpoint | Method | Purpose |
|---|
/accepts | POST | Enriches payment requirements with facilitator-specific fields. |
/verify | POST | Verifies a payment without settling. |
/settle | POST | Verifies and settles a payment on-chain. Returns transaction hash. |
/supported | GET | Lists supported schemes, networks, and assets. |
Further reading