Developers · provider SDK
One configuration, every protocol endpoint.
createProvider turns a list of services into a signed discovery document, a quote endpoint, receipt lookup and an x402 gate mounted in front of your handlers. Your code is the handler and nothing else.
A minimal provider
import { serve } from "@hono/node-server";
import { privateKeyToAccount } from "viem/accounts";
import { createProvider } from "@epp402/server";
const provider = createProvider({
providerId: "inference.example.com",
name: "Example Inference",
url: "https://inference.example.com",
network: "elysium-testnet",
signer: privateKeyToAccount(process.env.PROVIDER_PRIVATE_KEY as `0x${string}`),
facilitatorUrl: "https://epp402-core.fly.dev/facilitator",
registryUrl: "https://epp402-core.fly.dev",
services: [
{
id: "inference.standard",
name: "Standard inference",
category: "ai-inference",
endpoint: "/v1/inference",
method: "POST",
price: "0.01", // list price in EPPUSD; the quote below prices each request
quote: {
price: (req) => 10_000n + 5n * BigInt(String((req.params as { text?: string }).text ?? "").length / 100),
},
},
],
});
// Discovery, quotes, receipts and the x402 gate are already mounted; add your handler.
const app = provider.app();
app.post("/v1/inference", async (c) => c.json(await infer(await c.req.json())));
serve({ fetch: app.fetch, port: 4031 }, () => provider.publish());The CLI writes this file for you: epp402 init scaffolds a provider with its discovery document, a sample service and an .env.example. The CLI page has every flag.
npx @epp402/cli init my-provider --provider-id my-provider.example.com --url https://my-provider.example.com
cd my-provider && cp .env.example .env # set PROVIDER_PRIVATE_KEY
pnpm install && pnpm dev
epp402 inspect http://127.0.0.1:4021 # discovery document, signature check, a 402 probe per serviceProviderOptions
providerIdstring · required
Your provider id (a domain name works well). Lowercase letters, digits, dots and dashes.namestring · required
Display name.descriptionstring · optional
urlstring · required
The public origin the discovery document is served from. The registry refuses documents whose origin differs.contact{ email?, url? } · optional
network"local" | "elysium-testnet" | NetworkDefinition · required
The network you settle on.signerProviderSigner · required
The provider key: signs the discovery document, every quote and every receipt. A viem account.payToaddress · optional
Settlement recipient. Defaults to the signer address.facilitatorUrlstring · required
The x402 facilitator, for example https://epp402-core.fly.dev/facilitator.assetPaymentAsset · optional
The payment asset. Defaults to the network's first deployed asset from the manifest.servicesServiceConfig[] · required
What you sell.quoteTtlSecondsnumber · optional
How long a quote stays valid.quotesPathstring · optional
Default /v1/quotes: POST to request, GET /:quoteId to read.receiptsPathstring · optional
Default /v1/receipts: GET /:receiptId, POST /verify.stores{ quotes?, receipts? } · optional
Persistence for quotes and receipts. Memory by default.registryUrlstring · optional
Where publish() announces your discovery URL.onSettled(event) => void · optional
Called after each settlement with the quote, receipt, settle response, payer and resource.log(event, data) => void · optional
Structured events.facilitatorTimeoutMsnumber · optional
Budget for facilitator calls.
ServiceConfig
One entry per purchasable endpoint. The list price is advertised; the quote function prices a specific request.
idstring · required
Unique within the provider, for example inference.standard.namestring · required
descriptionstring · optional
categoryai-inference | market-data | compute | storage | api | data-processing | agent-services | other · required
The protocol's closed list; the registry indexes by it.endpointstring · required
Path of the paid endpoint, relative to url. The gate is mounted here.methodGET | POST | PUT | PATCH | DELETE · optional
Default POST.pricestring | { minor: string } · required
List price per unit: "0.01" in the asset's units, or minor units.pricing{ model?, unit? } · optional
fixed, per_request, per_token, per_second, per_unit, metered or volume, and the unit name.quote.requiredboolean · optional
Refuse payments that carry no signed quote (HTTP 428 quote_required).quote.price(request) => bigint | string · optional
Price a quote request in minor units from its params. Throw EppError("quote_params_invalid") for bad input.quote.ttlSecondsnumber · optional
Quote lifetime for this service.maxTimeoutSecondsnumber · optional
How long an authorization stays valid, as advertised in the 402.input, outputJSON Schema · optional
Shapes of the parameters and the result, published in the discovery document.capabilities, tags, docsUrlstring[], string[], url · optional
Discovery metadata.
What the gate does
The payment middleware runs on every configured endpoint, before your handler.
- 1
Resolves the quote
From EPP-QUOTE, or from the payment's epp402 extension, or by minting an implicit list-price quote. A service with quote.required answers 428 without one. - 2
Answers 402 from the quote
accepts[0] is exactly the quote's x402 block; extensions.epp402.info names provider, quote, session and intent. - 3
Verifies the payment
Through the facilitator's /verify: the EIP-3009 authorization, the amount, the recipient, the window. - 4
Verifies a signed intent
When EPP-INTENT decodes as a Payment Intent: its signature, that it names this quote and matches its terms, and that it describes exactly the authorization in PAYMENT-SIGNATURE. Otherwise 400 or 409 before anything runs. - 5
Runs your handler
Only now. A handler error or a 4xx/5xx response cancels: nothing is settled. - 6
Settles
Through the facilitator's /settle, after the handler, so a failed handler never costs the payer. - 7
Issues the receipt
Signed over the quote, the payer, the amount, the transaction hash and keccak256 of the response body; returned in EPP-RECEIPT beside PAYMENT-RESPONSE.
The Provider object
What createProvider returns.
app()() => Hono
A ready app with routes() and payment() mounted; add your handlers to it.routes()() => Hono
Discovery, quotes and receipts routes, to mount at the root of your own app.payment()() => MiddlewareHandler
The x402 gate for the configured endpoints.document()({ refresh? }?) => Promise<DiscoveryDocument>
The signed discovery document.quote()({ serviceId, params?, quantity?, buyer?, sessionId? }) => Promise<Quote>
Issue a quote programmatically.publish()() => Promise<{ registryUrl, status, body } | undefined>
Announce the discovery URL to registryUrl.configResolvedProvider
The options after defaults: asset, paths, services with resolved prices.quotes, receiptsQuoteStore, ReceiptStore
The stores in use.runtimePaymentRuntime
The x402 resource server and facilitator client.
/.well-known/epp402.jsonGET
The signed discovery document./v1/quotesPOST
Request a quote: { serviceId, params?, quantity?, buyer?, sessionId? }./v1/quotes/:quoteIdGET
Read a quote this provider issued./v1/receipts/:receiptIdGET
Read a receipt this provider issued./v1/receipts/verifyPOST
Verify a receipt against this provider's records and signature.your endpointsas configured
Behind the gate: 402 without payment, your handler with it.
Being found
Discovery is decentralised: your document is the source of truth. The registry indexes it; registration on-chain is optional.
# Index it in the registry (the crawler fetches your document from your origin and verifies it):
epp402 publish --url https://my-provider.example.com --registry https://epp402-core.fly.dev
# Also register in EPP402Registry, so agents can check the document hash against the chain
# (PROVIDER_PRIVATE_KEY must be the document's provider address and hold a little gas):
epp402 publish --url https://my-provider.example.com --onchainProgrammatically, provider.publish() posts your URL to registryUrl; registerOnChain from @epp402/server writes or updates your registration. The ecosystem page lists what the registry has verified.
Example providers
Four providers in the repository, each real: they run jobs, read live data, and settle on the local stack or the testnet.
paid-apiport 4021
The smallest provider: one echo endpoint.inference-providerport 4031
Deterministic text analysis priced per request; an optional OpenAI-compatible passthrough.market-data-providerport 4032
Live perpetual market data from Hyperliquid's public API, sold per query.compute-providerport 4033
Metered CPU jobs: a hash search and a prime sieve, priced per million units, quotes required.ai-agentno port
An agent that buys market data, inference and compute in one session and writes every object it received to a file.
Run one with pnpm --filter @epp402/example-<name> dev. Each needs PROVIDER_PRIVATE_KEY and a network with a deployed payment asset.