Providers and API keys

How independent services share one facilitator: registration, API keys, payTo binding, tenant isolation and key rotation.

On this page

A provider is anyone who sells over z402: one payTo address, a set of priced routes, and a server that answers 402. In x402 terms it is the resource server. A provider does not run payment infrastructure. It connects to a hosted facilitator with an API key.

The facilitator is one rail that many providers share. Each provider keeps its own address, its own prices, its own key and its own ledger, and nothing in one provider's state is visible to another. The operator who runs the facilitator is a provider like any other. This is protocol v2.0, the multi-provider rail.

Who is who

RoleWhoWhat it holds
AgentThe buyer.A wallet, and a session key per provider.
ProviderA seller, with one payTo.Its payTo, a view-only key for it, an API key, its prices.
FacilitatorThe hosted rail.Each provider's view-only key and a ledger per provider. It needs no spending key.
OperatorWhoever runs the facilitator.The facilitator's configuration, and its own provider: its payTo and its token.
402, voucher402, voucherAPI key 1API key 2AgentProvider 1Provider 2FacilitatorRuntime of provider 1Runtime of provider 2lightwalletd
402, voucher402, voucherAPI key 1API key 2AgentProvider 1Provider 2FacilitatorRuntime of provider 1Runtime of provider 2lightwalletd

An agent pays each provider on its own terms. It opens a separate session, with a separate key and a separate deposit, for every payTo it meets.

What a provider owns

ItemWhat it isWhere it lives
payToThe shielded-only unified address its deposits go to.In every 402 the provider sends.
UIVKUnified incoming viewing key. Whoever holds it can see the payments made to an address, but cannot spend them.The view-only key of payTo. It lets the facilitator read the provider's deposits.Given to the facilitator once, at registration. Stored in registry.json, mode 0600.
API keyA bearer secret that starts with z402_. It picks the provider's runtime.Shown to the provider once. The facilitator keeps only its sha256.
Provider id16 hex characters: the start of the sha256 of z402-provider-v1, the network and the payTo.Derived. It names the provider's ledger and catalog entries.
PricesOne per route, from 1 zat to windowZat.In the provider's own routes.
LedgerThe provider's sessions, deposits and vouchers.ledgers/<providerId>.jsonl, in the providers directory.

The provider id depends on the network and the payTo alone, so it does not change when the key is rotated. The ledger and the catalog entries stay with the provider.

Registering a provider

A provider registers once. It proves it owns its payTo, and gets an API key.

POST/providersRegistration tokenSend the operator's registration token as Authorization: Bearer …, when the facilitator sets Z402_REGISTRATION_TOKEN (an empty one counts as unset). Without one the route is open, except on mainnet, where it answers 403 registration_disabled unless Z402_REGISTRATION_OPEN=1.

Registers a provider, or rotates the key of one that is registered. The route is open until the operator sets Z402_REGISTRATION_TOKEN, except on mainnet, where it is closed until the operator sets the token or Z402_REGISTRATION_OPEN=1. With a token set, it needs that token as a bearer. It answers 201 for a new provider and 200 for a rotated key.

payTostringRequired
The provider's shielded-only unified address, valid for the facilitator's network, and at most 1,024 characters.
uivkstringRequired
The unified incoming viewing key of payTo. It has to own the address.
networkstring
Optional. If given, it must be the facilitator's network, such as zcash:regtest.
POST /providers with payTo and uivkowns(payTo), key in the environmentownedByKey, hasTransparentopen the provider's runtimewrite the registry201 with providerId and apiKeyProviderFacilitatorzdecryptProviderFacilitatorzdecrypt
POST/providerswith payToand uivkowns(payTo), keyin the environmentownedByKey,hasTransparentopen theprovider'sruntimewrite theregistry201 withproviderIdand apiKeyProviderFacilitatorzdecryptProviderFacilitatorzdecrypt

These are the steps for a new provider. A rotation skips the runtime.

  1. Check the body

    payTo must pass the address check for the network, uivk must have the shape of a viewing key for it, and network, if present, must be the facilitator's. A body that fails gets 400, and the message names the field.
  2. Prove ownership

    zdecrypt owns must answer ownedByKey: true and hasTransparent: false. The viewing key goes in the environment, never in argv. A key that does not own the address, or an address with a transparent receiver, gets 403 not_owner.
  3. Open the runtime

    For a new payTo, the facilitator locks and replays the provider's ledger, polls the chain once and starts its watcher. Registrations run one at a time.
  4. Write the registry

    Only now is the registry written: to a temporary file, synced, renamed over registry.json, and the directory synced. A crash therefore never leaves an entry without a runtime.
  5. Issue the key

    The facilitator answers with the key. It is shown once.
201 Created
{
  "providerId": "3f9a0c1d5e7b2a46",
  "network": "zcash:regtest",
  "payTo": "uregtest1...",
  "apiKey": "z402_..."
}

The answer carries Cache-Control: no-store, and it never echoes the viewing key.

# Leave out the authorization header when the facilitator has no registration token
curl -sS https://facilitator.example.com/providers \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $Z402_REGISTRATION_TOKEN" \
  -d '{"payTo":"uregtest1...","uivk":"uivkregtest1..."}'

The script reads network, uivk and orchardOnlyAddress from a view-only keys file, and sends only the address, the viewing key and the network. It opens the key file, with mode 0600, before it sends the request, so a key that is issued always has somewhere to go. It refuses to replace an existing key file without --force, takes --registration-token-file when the operator set a token, and never prints the key, the viewing key or the token.

Refusals have the body {"error": "<code>", "message": "<text>"}:

StatusCodeWhen
400invalid_requestA field is missing or invalid. The message names it. A payTo of more than 1,024 characters is one.
401unauthorizedA registration token is set and was not presented.
403not_ownerThe viewing key does not own payTo, or payTo has a transparent receiver.
403provider_limitThe limit, Z402_MAX_PROVIDERS, is reached.
403registration_disabledThe facilitator was started without a providers directory, as embedded and test facilitators are, or it runs on mainnet with no registration token and no Z402_REGISTRATION_OPEN=1.
409operatorpayTo is the operator's own address, which is never registered.
500internal_errorThe registration failed. Nothing was changed.

API keys

A key is z402_ followed by 43 base64url characters, which is 32 random bytes. That is a valid RFC 6750 bearer token. The facilitator stores only its sha256, and looks a key up by that hash. The key is never logged: access-log lines name the provider id.

A provider sends its key as Authorization: Bearer <apiKey>.

RouteWhat it needs
GET /supported, GET /discovery/resources, GET /discovery/searchNothing. A key that is presented must still be a real one, so a wrong key is found out at once.
POST /providersThe registration token, when the operator has set one. Without one it is open, except on mainnet, where it is closed unless the operator opened it with Z402_REGISTRATION_OPEN=1.
GET /providers/me, POST /verify, POST /settle, POST /discovery/resourcesThe provider's API key. A request without a valid one gets 401 with WWW-Authenticate: Bearer.
GET /providers/me
{
  "providerId": "3f9a0c1d5e7b2a46",
  "network": "zcash:regtest",
  "payTo": "uregtest1..."
}

A resource server calls GET /providers/me once, when it starts, to prove that its key and its configured payTo belong together.

Tenant isolation

The key picks the provider's runtime. A runtime is everything that serves one payTo, and nothing in it is shared with another provider:

Per providerShared by all providers
A chain view that decrypts with the provider's viewing key and keeps only outputs to its payTo.The lightwalletd client, and one tip it reads, cached for 1 second.
The ledger file, its lock and its header, which names the provider's payTo.The zdecrypt program, run at most 8 times at once across all providers.
Sessions, deposits and credit.The start-up self-test and network checks, which run once.
The deposit resolver (4 lookups at a time, 16 waiting) and the watcher.The HTTP server, the registry and the catalog.
The x402 facilitator and its scheme, which knows one payTo.

Four rules keep one provider's traffic out of another's:

  • The key binds the payTo. The scheme of a runtime refuses any paymentRequirements.payTo other than its own, with invalid_payment_requirements. The answer is still HTTP 200, as every verify and settle answer is.
  • The voucher binds the payTo. The signed message contains it, so a voucher made for one provider does not verify at another.
  • Credit is kept per provider. A deposit is consumed once per provider, under the network, the payTo and the txid. A transaction funds at most one session per payTo, and each provider credits only the outputs to its own address.
  • Deposits decrypt only for their provider. A deposit is read with that provider's viewing key, so a provider's runtime cannot read another's deposits.

A ledger that does not belong to its runtime is refused when the facilitator starts. A new ledger begins with a header that names its network and payTo, and a start that finds another network or address in it stops.

Failure is the one thing the providers share. A failed ledger write, or a lost ledger lock, in any runtime exits the whole process, and the restart replays every ledger. Those are disk or deployment faults that would hit every provider, so the facilitator does not try to keep serving some of them.

The operator provider

The provider whose address and viewing key the facilitator is configured with is the operator. It is not registered:

  • Its API key is Z402_FACILITATOR_TOKEN.
  • Its ledger stays at Z402_LEDGER.
  • Registering its payTo is refused with 409 operator.

A facilitator started without a token, as embedded test facilitators are, treats a request with no Authorization header as the operator's. A registered provider always needs its key.

In the starter deployment, the operator's address and token serve apps/api and three starter services: the LLM, embeddings and drop box services. One deposit window funds all of them, because an agent keeps one session per payTo. URL to markdown is registered as an independent provider with its own keys, so an agent pays it with a deposit of its own.

Prices and terms

A provider prices each of its routes. Protocol v2.0 accepts any canonical amount from 1 zat to windowZat, and the facilitator takes the price from the paymentRequirements of each call. The routes of one provider share one session, and each voucher must advance the total by the price of its own call. The service kit refuses to start a route whose price is outside that range.

ServiceOne call buysPrice
apps/api GET /chunkOne chunk500 zat
LLM inferenceOne output chunk200 zat
URL to markdownOne page1,000 zat
Text embeddingsAbout 1,000 tokens100 zat
Encrypted drop boxAn upload, or 10 more blocks of storage500 zat

The trust terms are not per provider. The window, the credit ratio, the cap and the expiry margin are the same for every provider on the rail, and they are advertised in /supported. Per-provider terms are future work.

Rotating a key

Registering a payTo that is already registered, with a key that owns it, rotates its API key. The answer is 200, with the same shape as the first registration and a new key.

  • The facilitator writes the new hash before it swaps the key in memory, so no request sees the new hash stored with the old key still working.
  • The old key stops working at once, on every route. GET /providers/me answers 401 from that moment.
  • The provider's ledger, its catalog entries and its provider id stay as they were.

This is also how a lost key is recovered: register again. Rotating a key does not count as a new provider, so the provider limit does not apply.

Connecting a resource server

A resource server uses its key in two ways. It sends the key as a bearer token on every call to the facilitator, and it checks the key and its payTo at start with checkProvider from @z402-net/server. That calls GET /providers/me. When it fails it throws a ProviderCheckError, and callers branch on its fields, never on its message. The message never contains the key.

Outcomefieldretryable
A connection error, a timeout, a body that breaks off, 5xx, 408 or 429facilitatorUrltrue
404, which is a facilitator without the route, any other status, or an unreadable 200facilitatorUrlfalse
401 or 403facilitatorTokenfalse
200, but for another payTo or networkpayTofalse
Before any request: a URL that is not http or https or carries credentials, or a token that is not a bearer tokenfacilitatorUrl or facilitatorTokenfalse
check-provider.ts
import { checkProvider, ProviderCheckError } from "@z402-net/server";

const facilitatorUrl = "https://facilitator.example.com";
const token = process.env["Z402_FACILITATOR_TOKEN"]; // the API key
const payTo = process.env["Z402_PAY_TO"] ?? "";

for (;;) {
  try {
    await checkProvider({ facilitatorUrl, token, payTo, network: "zcash:regtest" });
    console.log("the key and the payTo belong together");
    break;
  } catch (e) {
    if (!(e instanceof ProviderCheckError)) throw e;
    // Branch on the fields. A key or an address that is wrong will not get better by waiting.
    if (!e.retryable) throw new Error(`fix ${e.field}: ${e.message}`);
    console.log(`the facilitator is not ready (${e.field}), trying again in 2 s`);
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}

The demo API calls checkProvider at start and exits on any error. The service kit waits for a facilitator that is starting or down, and fails at once on anything it cannot fix by waiting. Sell an API and Build with the service kit show both.

Running registration in the open

On mainnet, open registration is off. With no token and no Z402_REGISTRATION_OPEN=1, the facilitator still starts and serves the operator and the providers already registered, and POST /providers answers 403 registration_disabled. The start prints a warning that says so.

SettingDefaultWhat it does
Z402_PROVIDERS_DIR.data/providers.<network>Where the registry, the provider ledgers and the catalog live. The directory has mode 0700.
Z402_MAX_PROVIDERS32How many providers can register, the operator not counted. 0 turns away new providers. A registered provider can still rotate its key.
Z402_REGISTRATION_TOKENnoneA bearer token that POST /providers then requires. It must be one token of letters, digits and -._~+/, and at least 32 characters, for example openssl rand -hex 32. A shorter one stops the start.
Z402_REGISTRATION_OPENunset1, or unset. Only mainnet reads it: 1 lets anyone register there, with the warning above. A token always gates registration, so 1 beside a token changes nothing.

At start the facilitator opens the operator's runtime, then loads the registry and asks zdecrypt again whether each stored viewing key still owns its payTo. A malformed registry, or one for another network, stops the start. So does an entry whose key no longer owns its address, and that message names the provider id. Every provider's ledger is then opened in parallel and polled once. The facilitator prints its usual ready line, then a second line with the number of registered providers.

Nothing removes a provider while the facilitator runs, and an idle provider keeps its slot. To free a slot or to drop an abusive provider, the operator stops the facilitator, removes the entry from registry.json and starts it again. Run the facilitator gives the steps, and the facilitator's README has the whole procedure under "Removing a provider": packages/facilitator/README.md.

References

  • docs/superpowers/specs/2026-10-28-z402-multi-provider-design.md: the multi-provider rail, sections 3, 4.1, 4.5, 4.6 and 7.
  • docs/spec/protocol.md: the wire contract. Its v2.0 revision adds section 16, providers and API keys, and changes sections 2, 5.2, 10 and 15.
  • packages/facilitator/src/providers/register.ts: the registration and rotation flow, and its refusals.
  • packages/facilitator/src/providers/registry.ts: the registry file and its atomic write.
  • packages/facilitator/src/providers/apiKey.ts: the key format and its hash.
  • packages/facilitator/src/providers/id.ts: the provider id.
  • packages/facilitator/src/runtime.ts: one provider's runtime.
  • packages/facilitator/src/app.ts: the routes, the key lookup and the start-up order.
  • packages/facilitator/src/config.ts: Z402_PROVIDERS_DIR, Z402_MAX_PROVIDERS, Z402_REGISTRATION_TOKEN and Z402_REGISTRATION_OPEN.
  • packages/facilitator/README.md: the environment, registration, and removing a provider.
  • packages/server/src/provider.ts: checkProvider and ProviderCheckError.
  • scripts/register-provider.mjs: registration from a view-only keys file.
  • docs/superpowers/specs/2026-10-27-z402-services-design.md: the starter services, with URL to markdown as provider 2.
  • RFC 6750, bearer tokens: the token format of every key.