Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.fireblocks.com/llms.txt

Use this file to discover all available pages before exploring further.

Open-source or hosted? This page describes the open-source facilitator that you run yourself. Fireblocks also offers a fully managed, fully secured hosted x402 Facilitator — production-grade security, operational support, monitoring, and a managed endpoint, with no infrastructure for you to run or upgrade. Talk to us about early access.
This page covers everything beyond the merchant integration: the config file, the two auth surfaces, the management API, the CLI, optional Payment Instruction Integrity, production deployment, multiple-merchant hosting, and operator responsibilities. For the protocol overview and quick start, see the Overview. For the merchant-side integration and pricing, see Integration.

The config file

Everything non-runtime lives in config/facilitator.json. The file is loaded and validated at boot; changes require a server restart.
{
  "tenant_id": "acme",
  "default_configuration_id": "default",
  "assets": [
    {
      "asset_id": "USDC_BASE",
      "blockchain_id": "0318d40f-7709-4f10-b980-11f3abaf31ac",
      "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "decimals": 6,
      "chain_id": 8453,
      "eip712_name": "USD Coin",
      "eip712_version": "2",
      "transfer_mechanism": "eip-3009",
      "stable": true,
      "price_symbol": null
    }
  ],
  "configurations": [
    {
      "configuration_id": "default",
      "public_host": "https://pay.myservice.com",
      "fireblocks": {
        "api_key": "...",
        "api_secret_path": "./secrets/fireblocks.pem",
        "receiver_vault": "0",
        "base_url": "https://api.fireblocks.io",
        "deposit_address_cache": {
          "USDC_BASE": "0xMerchantVaultAddress"
        }
      },
      "api_keys": [
        {
          "key_id": "ak_...",
          "hash": "sha256:...",
          "scopes": ["process-payments"],
          "label": "agent"
        }
      ],
      "products": [
        {
          "product_id": "prod_...",
          "name": "Premium Data",
          "endpoint": "/premium",
          "scheme": "exact",
          "usd_price": 0.10,
          "pricing": [
            { "asset_id": "USDC_BASE", "amount": null }
          ],
          "description": null,
          "mime_type": "application/json",
          "category": null,
          "is_discoverable": false
        }
      ]
    }
  ]
}
A few things to know:
  • configurations[] — one facilitator process can host many merchants. Each configuration has its own Fireblocks credentials, API keys, and product catalog. Clients are routed to the right configuration by the Host header matching public_host.
  • asset_id is the Fireblocks-native identifier (for example, USDC_BASE, USDC_POLYGON, ETH_BASE). There is no separate tokens or blockchains registry — the asset entry carries everything the facilitator needs.
  • Assets set stable: true for USD-pegged stablecoins (no network call at request time) or price_symbol: "<coingecko-id>" for live-priced assets.
  • Products use pricing[] to list accepted assets, and optionally a usd_price to convert from. Legacy {asset_id, price} on a product is still accepted and normalized to a single-entry pricing[].
API key hashes are persisted; plaintext is returned once at creation and never again. If you lose a key, revoke it and mint a new one.

Auth model

The facilitator exposes two independent auth surfaces. They are never mixed.
PathAuthPurpose
/api/admin/*JWT (HS256 or JWKS)Operators. Per-route scope check.
/api/payments/*Persistent API keyMachine clients (merchant servers, agents). Scoped to one configuration.
/api/discovery/*, /api/health, /api/payments/supportedPublic

Management API (JWT)

Admin tokens are short-lived JWTs. In development, mint them locally from the HS256 secret scaffolded by npm run setup:
npm run setup:admin-token -- --preset full       # all four admin scopes
npm run setup:admin-token -- --preset readonly   # admin:read + payments:read
npm run setup:admin-token -- --preset payments-ops
npm run setup:admin-token -- --scopes "payments:read admin:read"
Export as X402_ADMIN_TOKEN and the x402 CLI plus any direct curl picks it up:
export X402_ADMIN_TOKEN=<paste token>
x402 payments list
curl -H "authorization: Bearer $X402_ADMIN_TOKEN" http://localhost:3000/api/admin/facilitator
In production, point X402_ADMIN_JWT_JWKS_URL at your identity provider’s JWKS endpoint. Your IdP issues tokens carrying tenant_id, sub, scope (space-delimited), and optional configuration_ids.

Payment processing (API keys)

Payment API tokens are opaque bearer strings minted via x402 keys create or POST /api/admin/tokens. Only the SHA-256 hash is persisted in the config file; the plaintext is shown once at creation and never again. Keys are scoped to a single configuration — a key minted for merchant-a cannot settle payments for merchant-b. Scopes you will actually use:
  • process-payments — call /api/payments/{create,verify,settle}
  • api:read — read-only access (reserved for future public read routes)
  • * — wildcard

Management API

The management API is the operator-facing surface. Every route is JWT-authenticated and gated by an explicit scope.

Admin scopes

ScopeRoutes
admin:readGET /facilitator, GET /assets, GET /products, GET /tokens, GET /fireblocks
admin:writePOST / DELETE on assets, products, tokens, and fireblocks test
payments:readGET /payments, GET /payments/:id
payments:writePOST /payments/:id/mark-failed, POST /payments/:id/refund, POST /payments/:id/sync, POST /payments/sync-all, POST /payments/sweep-expired
*Wildcard — passes every admin scope check

Targeting a configuration

When a tenant holds multiple configurations, admin requests pick one with the X-Configuration-ID header or ?configuration=<id> query parameter. If omitted, the request targets default_configuration_id:
curl -H "Authorization: Bearer $X402_ADMIN_TOKEN" \
     -H "X-Configuration-ID: merchant-b" \
     http://localhost:3000/api/admin/products
The authenticated user’s configurationIds grant must include the chosen configuration, otherwise the request returns 403.

Inspecting configuration

GET /api/admin/facilitator returns the configuration block the principal is scoped to, with fireblocks.api_key redacted.

Assets

GET /api/admin/assets and GET /api/admin/assets/:assetId list or fetch one configured asset. POST /api/admin/assets registers an asset. The facilitator calls Fireblocks’ listAssets to fill address / decimals / chain_id; you supply the x402-specific fields in the body.
curl -X POST https://facilitator.example.com/api/admin/assets \
  -H "Authorization: Bearer $X402_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "asset_id": "USDC_BASE",
    "transfer_mechanism": "eip-3009",
    "eip712_name": "USD Coin",
    "eip712_version": "2",
    "stable": true
  }'
POST /api/admin/assets/sync re-fetches Fireblocks-owned fields for every asset in scope. Body: { "apply": false } for a dry-run diff report; true to write. Returns per-asset diffs[] and any errors[].

Products

GET /api/admin/products and GET /api/admin/products/:productId list or fetch products with their asset joined.
Adding and editing products over HTTP is not yet supported. Use the x402 products CLI. The HTTP endpoints above are read-only on purpose.

API tokens

The primary write endpoints for issuing machine credentials. POST /api/admin/tokens mints a key:
curl -X POST https://facilitator.example.com/api/admin/tokens \
  -H "Authorization: Bearer $X402_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scopes":["process-payments"],"label":"agent-primary"}'
{
  "token": "x402_ak_AbCd1234_plaintextOnlyReturnedOnce",
  "keyId": "ak_AbCd1234",
  "label": "agent-primary",
  "scopes": ["process-payments"]
}
GET /api/admin/tokens lists keys (hashes omitted). DELETE /api/admin/tokens/:keyId revokes a key, returning 204 on success or 404 if the key does not exist.

Inspecting payments

GET /api/admin/payments and GET /api/admin/payments/:paymentId are read-only payment inspection. Scope: payments:read. Query parameters on list: ?status=<status>, ?limit=<n>, ?offset=<n>.
[
  {
    "paymentId": "pay_abc",
    "tenantId": "acme",
    "configurationId": "default",
    "productId": "prod_7Lm3nOp",
    "assetId": "USDC_BASE",
    "amount": 0.1,
    "amountBaseUnits": 100000,
    "recipientAddress": "0x…",
    "fromAddress": "0x…",
    "status": "completed",
    "transferMechanism": "eip-3009",
    "transactionHash": "0x…",
    "blockNumber": 12345678
  }
]

Managing payments

POST /api/admin/payments/:paymentId/mark-failed forces a stuck row into failed with an operator-supplied reason. No on-chain action — for rows where the happy-path transitions never closed out (server crash mid-settle, settlement poll timed out, and similar). Scope: payments:write. POST /api/admin/payments/:paymentId/refund refunds a payment whose on-chain leg landed (status is completed or settled). Submits a CONTRACT_CALL to the token’s transfer(to, amount) function via Fireblocks. Status transitions: refunding → refunded, or refund_failed with the Fireblocks error recorded. Returns 409 when the payment is not refundable, 502 when the on-chain refund fails. Scope: payments:write. POST /api/admin/payments/:paymentId/sync reconciles one payment against Fireblocks. Reads the persisted fireblocksTxId, asks Fireblocks for the transaction’s current state, and transitions the row:
  • completed with transactionHash and blockNumber on Fireblocks COMPLETED.
  • failed on Fireblocks FAILED, CANCELLED, BLOCKED, or REJECTED.
  • No-op while the transaction is still in-flight.
POST /api/admin/payments/sync-all runs the same reconcile pass across every settling row in the caller’s scope. The same pass fires at boot for every configuration (processing-role only); opt out with X402_RECONCILE_ON_BOOT=false when running many processing instances against a shared store. POST /api/admin/payments/sweep-expired bulk-expires every pending payment whose expiresAt is in the past. Idempotent per row — safe to run on a schedule.

Payment statuses

Payments transition through: pending → verified → settling → completed, plus the terminal states settled, refunding, refunded, refund_failed, expired, and failed.
Reconciler-ownership invariant. Once a fireblocksTxId is attached to a row, the settle route never preempts it into failed on transient errors. The underlying transaction may still land, so the row stays settling and the reconciler decides terminality from Fireblocks’s own truth. Rows without a fireblocksTxId still follow the normal settle path.

CLI reference

The facilitator ships two CLI surfaces with a clean split:
  1. x402 — a remote HTTP client for /api/admin/*. Pure API client, safe to run from anywhere with X402_ADMIN_TOKEN set. Targets X402_URL (default http://localhost:3000).
  2. npm run setup* — local bootstrap scripts. Touch the filesystem (scaffold config, mint JWTs from the on-disk secret, import a legacy SQLite database). Must run on the same host as the server.

x402 remote CLI

Install once per developer:
npm run build && npm link       # puts `x402` on your PATH
Every command that operates on a configuration takes -c, --configuration <id> (or env CONFIGURATION=<id>); omitted means the server default.
# Config (read-only)
x402 config show
x402 config validate
x402 config configurations

# Fireblocks (read-only + operational test)
x402 fireblocks show
x402 fireblocks test [--chain-id 8453] [--create-missing]

# API keys
x402 keys list [--json]
x402 keys create --scopes process-payments [--label agent]
x402 keys revoke <keyId>

# Assets
x402 assets list [--json]
x402 assets show <assetId>
x402 assets import <assetId> \
    --transfer-mechanism eip-3009 \
    --eip712-name "<domain name>" --eip712-version <n> \
    [--stable] [--price-symbol <coingecko-id>] [--force]
x402 assets sync [--apply]
x402 assets remove <assetId>

# Products
x402 products list [--json]
x402 products show <productId>
x402 products add --name Premium --endpoint /premium --asset USDC_BASE --usd-price 0.10
x402 products add --name Premium --endpoint /premium --asset USDC_BASE --price 100000
x402 products remove <productId>

# Payments — read
x402 payments list [--status failed] [--limit 20] [--json]
x402 payments get <paymentId>

# Payments — state changes
x402 payments mark-failed <paymentId> --reason "..."
x402 payments refund <paymentId>
x402 payments sync <paymentId>
x402 payments sync-all
x402 payments sweep-expired
Every command accepts --url <base> and --token <jwt> to override the env defaults.

Local setup scripts

These run through npm run. They never hit HTTP.
npm run setup                      # scaffold config/facilitator.json + secrets/jwt-hs256.key
npm run setup -- --force           # rotate both (destructive)

npm run setup:admin-token -- --preset full
npm run setup:admin-token -- --preset readonly
npm run setup:admin-token -- --preset payments-ops
npm run setup:admin-token -- --preset wildcard
npm run setup:admin-token -- --scopes "payments:read admin:read" --ttl 30m

npm run setup:migrate -- --db ./data/legacy.db [--force]
All config edits go through atomic tmpfile-plus-rename, so it is safe to run x402 products add while the server is running (nodemon reloads).

End-to-end test harness

npm run e2e
Assumes the facilitator, the example merchant, and the EOA are already set up. Fires each configured transfer mechanism (eip-3009, permit2, erc7710) in sequence against the test client, polls the admin API until each payment row reaches completed, and prints a pass/fail summary with block explorer links. Exits 0 if every mechanism passed. Idempotent — safe to re-run.

Payment Instruction Integrity

Optional. When enabled on a configuration, the facilitator signs every /api/payments/create response with an ES256 keypair and attaches the envelope two ways:
  1. As an integrity top-level field on the JSON body.
  2. As an X-402-Integrity response header (mirrored by @x402/express).
Wallets that implement the PII extension fetch the did:web document referenced by the envelope, verify the signature, and refuse to sign any payment whose body was altered between the facilitator and the wallet.

What the facilitator signs

The envelope covers a payment-critical slice of the 402 body — specifically {x402Version, accepts} — plus the envelope’s own iat and exp. resource.url, error, and extensions are intentionally excluded because the merchant SDK rewrites resource.url to its own public origin before emitting the 402; signing it would invalidate every response. The payment data the wallet actually commits to (amount, asset, payTo, network, scheme — all in accepts[]) is signed. Canonical bytes:
SHA-256( JCS({x402Version, accepts}) || "\n" || iat || "\n" || exp )
JCS is RFC 8785 JSON Canonicalization.

Envelope shape

Base64url-encoded JSON:
{
  "v": 1,
  "did": "did:web:api.example.com",
  "kid": "key-1",
  "alg": "ES256",
  "iat": 1776521334,
  "exp": 1776521634,
  "sig": "<base64url P1363 signature>"
}

Enabling it

npm run setup scaffolds ./secrets/integrity-p256.pem (a P-256 keypair) for you. Flip integrity on per configuration:
{
  "configuration_id": "default",
  "public_host": "http://localhost:3000",
  "integrity": {
    "enabled": true,
    "private_key_path": "./secrets/integrity-p256.pem",
    "did": "did:web:localhost%3A3000",
    "kid": "key-1",
    "alg": "ES256",
    "ttl_seconds": 300,
    "serve_did_document": true
  }
}
When integrity.serve_did_document: true, the facilitator serves the DID document at GET /.well-known/did.json (resolved by Host header against public_host). If you would rather host the did.json yourself, leave serve_did_document: false and publish the public key at whatever URL did:web:<domain> resolves to.

Wallet-side verification

The bundled test client has a VERIFY_INTEGRITY=true flag that decodes the envelope, checks iat and exp, resolves did:web:<domain>, verifies the ES256 signature, and aborts the flow if verification fails.
VERIFY_INTEGRITY=true MECHANISM=eip3009 CHAIN=11155111 npm run dev
Add REQUIRE_INTEGRITY=true to also reject quotes that do not carry an envelope at all.

Scope and limitations

What integrity signs: {x402Version, accepts, iat, exp}. What it does not sign: resource.url, error, extensions — these are mutable by the merchant SDK. Supported algorithm: ES256 (P-256). Not yet supported: ES256K, EdDSA, did:webvh, multiple keys per DID, on-chain registry binding. The PII specification is a draft; the envelope format follows it, but the multi-accept canonical form is a documented extension.

Running in production

Environment variables

All are optional unless noted.
VariableDefaultNotes
PORT3000Server listen port
CONFIG_PATH./config/facilitator.jsonWhere to load config
PAYMENT_STOREsqlitememory, sqlite, or postgres
DB_PATH./data/facilitator.dbsqlite only
POSTGRES_URLRequired when PAYMENT_STORE=postgres
ALLOWED_ORIGINShttp://localhost:$PORTComma-separated CORS allowlist
X402_ADMIN_JWT_SECRETInline HS256 secret; takes precedence over the file-based default
X402_ADMIN_JWT_SECRET_FILE./secrets/jwt-hs256.keyFile-based HS256 secret; scaffolded by npm run setup
X402_ADMIN_JWT_JWKS_URLJWKS endpoint for production (RS256 or ES256); overrides HS256 when set
X402_ADMIN_JWT_ISSUEROptional iss claim to require on every admin JWT
X402_ADMIN_JWT_AUDIENCEOptional aud claim to require on every admin JWT
X402_URLhttp://localhost:3000Read by the x402 CLI
X402_ADMIN_TOKENBearer JWT for the x402 CLI
X402_ALLOW_MAINNETMust be true to register or run with mainnet assets. Default-deny.
X402_RECONCILE_ON_BOOTtrueSet to false when running many processing instances against a shared store.
COINGECKO_API_KEYOptional; enables CoinGecko Pro. Free tier works unauthenticated.
NODE_ENVdevelopmentStandard Node environment
Fireblocks credentials do not live in environment variables — they belong in config/facilitator.json.

Network policy

The facilitator tracks every asset’s network via an is_testnet field (populated at import time from Fireblocks’ blockchain.onchain.test). By default, mainnet is denied:
  • Boot — the server refuses to start if any configured asset has is_testnet: false and X402_ALLOW_MAINNET is not set to true. The error lists the offending (asset_id, chain_id) pairs.
  • ImportPOST /api/admin/assets (and x402 assets import) returns 403 when the hydrated asset is mainnet and the flag is off.
To opt in:
X402_ALLOW_MAINNET=true npm start
The server banner prints network policy: mainnet OK (X402_ALLOW_MAINNET=true) or testnet-only (default) at startup, so there is no ambiguity.

Build and run compiled

npm run build
NODE_ENV=production \
X402_ROLE=processing \
X402_ADMIN_JWT_JWKS_URL=https://auth.yourco.example.com/.well-known/jwks.json \
PAYMENT_STORE=postgres \
POSTGRES_URL=postgres://<USER>:<PASSWORD>@host:5432/x402 \
npm start
For management in production, point X402_ADMIN_JWT_JWKS_URL at your JWT issuer’s public keys. HS256 works but asymmetric or JWKS is the right choice at scale. Without either, the admin API rejects everything.

Role split

The same binary runs in three profiles, selected by X402_ROLE:
  • processing — handles /api/payments/* and the reconciler. Run multiple of these behind a load balancer.
  • management — handles /api/admin/*. Smaller surface, separate scaling.
  • all (default) — both. Right for single-instance and development deployments.
When you run many processing instances against a shared payment store, set X402_RECONCILE_ON_BOOT=false on all but one so only one instance leads the boot-time reconcile pass.

Multiple merchants on one facilitator

Payment-service providers who operate the facilitator on behalf of many merchants use one configuration per merchant:
{
  "tenant_id": "acme-payments-provider",
  "default_configuration_id": "merchant-a",
  "configurations": [
    {
      "configuration_id": "merchant-a",
      "public_host": "https://pay.merchant-a.com",
      "fireblocks": { /* merchant A's vault */ },
      "products": [ /* … */ ],
      "api_keys": [ /* … */ ]
    },
    {
      "configuration_id": "merchant-b",
      "public_host": "https://pay.merchant-b.com",
      "fireblocks": { /* merchant B's vault */ },
      "products": [ /* … */ ],
      "api_keys": [ /* … */ ]
    }
  ]
}
Requests are routed to the right configuration by matching the request’s Host header against each configuration’s public_host. API keys are bound to the configuration that issued them — a key from merchant-a cannot settle payments for merchant-b. For a single-merchant deployment, leave the default single-configuration layout in place.

Operator responsibilities and risk

Running an x402 facilitator means initiating real on-chain value transfers from a Fireblocks vault you control. Before deploying, read the full operator-facing disclaimer:
  • DISCLAIMER.md — covers irreversibility of on-chain transfers, the absence of a third-party security audit at publication, third-party contract risk (Permit2, the MetaMask Delegation Framework, ERC-20 tokens including USDC and USDT blacklisting and pause functions), the non-advisory nature of the code, jurisdictional and data-protection considerations (including GDPR and UK GDPR), the Fireblocks trademark policy, the absence of any fiduciary or custodial relationship, and the independence of the x402 protocol specification.
On-chain transfers initiated by the facilitator are irreversible. Bugs in your integration, misconfigured pricing or destination addresses, or unexpected upstream contract behavior may result in permanent loss of funds. Test thoroughly on testnets before enabling mainnet, and review every configuration change.
To report a suspected security issue in the facilitator, email security@fireblocks.com directly — do not file a public issue.

License

The facilitator is licensed under Apache License 2.0. See LICENSE for the full legal text. The license does not grant rights to use the Fireblocks name, logos, or other trademarks — see the trademark clause in DISCLAIMER.md.