> ## 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.

# Operating and Production

> Operate and deploy the Fireblocks x402 Facilitator: config file, auth model, management API, CLI, Payment Instruction Integrity, production deployment, and operator risk.

<Note>
  **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](https://www.fireblocks.com/#request-demo).
</Note>

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](/docs/x402-facilitator-overview). For the merchant-side integration and pricing, see [Integration](/docs/x402-facilitator-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.

```jsonc theme={"system"}
{
  "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.

| Path                                                         | Auth                | Purpose                                                                  |
| ------------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------ |
| `/api/admin/*`                                               | JWT (HS256 or JWKS) | Operators. Per-route scope check.                                        |
| `/api/payments/*`                                            | Persistent API key  | Machine clients (merchant servers, agents). Scoped to one configuration. |
| `/api/discovery/*`, `/api/health`, `/api/payments/supported` | Public              | —                                                                        |

### Management API (JWT)

Admin tokens are short-lived JWTs. In development, mint them locally from the HS256 secret scaffolded by `npm run setup`:

```bash theme={"system"}
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:

```bash theme={"system"}
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

| Scope            | Routes                                                                                                                                              |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin:read`     | `GET /facilitator`, `GET /assets`, `GET /products`, `GET /tokens`, `GET /fireblocks`                                                                |
| `admin:write`    | `POST` / `DELETE` on assets, products, tokens, and `fireblocks test`                                                                                |
| `payments:read`  | `GET /payments`, `GET /payments/:id`                                                                                                                |
| `payments:write` | `POST /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`:

```bash theme={"system"}
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.

```bash theme={"system"}
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.

<Note>
  Adding and editing products over HTTP is not yet supported. Use the `x402 products` CLI. The HTTP endpoints above are read-only on purpose.
</Note>

### API tokens

The primary write endpoints for issuing machine credentials.

`POST /api/admin/tokens` mints a key:

```bash theme={"system"}
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"}'
```

```json theme={"system"}
{
  "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>`.

```json theme={"system"}
[
  {
    "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`.

<Info>
  **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.
</Info>

## 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:

```bash theme={"system"}
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.

```bash theme={"system"}
# 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.

```bash theme={"system"}
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

```bash theme={"system"}
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](https://x402.org) 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:

```json theme={"system"}
{
  "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:

```jsonc theme={"system"}
{
  "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.

```bash theme={"system"}
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](https://x402.org); 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.

| Variable                     | Default                     | Notes                                                                         |
| ---------------------------- | --------------------------- | ----------------------------------------------------------------------------- |
| `PORT`                       | `3000`                      | Server listen port                                                            |
| `CONFIG_PATH`                | `./config/facilitator.json` | Where to load config                                                          |
| `PAYMENT_STORE`              | `sqlite`                    | `memory`, `sqlite`, or `postgres`                                             |
| `DB_PATH`                    | `./data/facilitator.db`     | sqlite only                                                                   |
| `POSTGRES_URL`               | —                           | Required when `PAYMENT_STORE=postgres`                                        |
| `ALLOWED_ORIGINS`            | `http://localhost:$PORT`    | Comma-separated CORS allowlist                                                |
| `X402_ADMIN_JWT_SECRET`      | —                           | Inline HS256 secret; takes precedence over the file-based default             |
| `X402_ADMIN_JWT_SECRET_FILE` | `./secrets/jwt-hs256.key`   | File-based HS256 secret; scaffolded by `npm run setup`                        |
| `X402_ADMIN_JWT_JWKS_URL`    | —                           | JWKS endpoint for production (RS256 or ES256); overrides HS256 when set       |
| `X402_ADMIN_JWT_ISSUER`      | —                           | Optional `iss` claim to require on every admin JWT                            |
| `X402_ADMIN_JWT_AUDIENCE`    | —                           | Optional `aud` claim to require on every admin JWT                            |
| `X402_URL`                   | `http://localhost:3000`     | Read by the `x402` CLI                                                        |
| `X402_ADMIN_TOKEN`           | —                           | Bearer JWT for the `x402` CLI                                                 |
| `X402_ALLOW_MAINNET`         | —                           | Must be `true` to register or run with mainnet assets. Default-deny.          |
| `X402_RECONCILE_ON_BOOT`     | `true`                      | Set to `false` when running many processing instances against a shared store. |
| `COINGECKO_API_KEY`          | —                           | Optional; enables CoinGecko Pro. Free tier works unauthenticated.             |
| `NODE_ENV`                   | `development`               | Standard 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.
* **Import** — `POST /api/admin/assets` (and `x402 assets import`) returns **403** when the hydrated asset is mainnet and the flag is off.

To opt in:

```bash theme={"system"}
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

```bash theme={"system"}
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:

```jsonc theme={"system"}
{
  "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`](https://github.com/fireblocks/x402-facilitator/blob/main/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.

<Warning>
  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.
</Warning>

To report a suspected security issue in the facilitator, email [security@fireblocks.com](mailto:security@fireblocks.com) directly — do not file a public issue.

## License

The facilitator is licensed under Apache License 2.0. See [`LICENSE`](https://github.com/fireblocks/x402-facilitator/blob/main/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`](https://github.com/fireblocks/x402-facilitator/blob/main/DISCLAIMER.md).
