Skip to content

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

server.tsRepository API
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.

terminalRepository
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 service

ProviderOptions

ProviderOptions
19
  • providerId

    string · required

    Your provider id (a domain name works well). Lowercase letters, digits, dots and dashes.
  • name

    string · required

    Display name.
  • description

    string · optional

  • url

    string · 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.
  • signer

    ProviderSigner · required

    The provider key: signs the discovery document, every quote and every receipt. A viem account.
  • payTo

    address · optional

    Settlement recipient. Defaults to the signer address.
  • facilitatorUrl

    string · required

    The x402 facilitator, for example https://epp402-core.fly.dev/facilitator.
  • asset

    PaymentAsset · optional

    The payment asset. Defaults to the network's first deployed asset from the manifest.
  • services

    ServiceConfig[] · required

    What you sell.
  • quoteTtlSeconds

    number · optional

    How long a quote stays valid.
  • quotesPath

    string · optional

    Default /v1/quotes: POST to request, GET /:quoteId to read.
  • receiptsPath

    string · optional

    Default /v1/receipts: GET /:receiptId, POST /verify.
  • stores

    { quotes?, receipts? } · optional

    Persistence for quotes and receipts. Memory by default.
  • registryUrl

    string · 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.
  • facilitatorTimeoutMs

    number · optional

    Budget for facilitator calls.

ServiceConfig

One entry per purchasable endpoint. The list price is advertised; the quote function prices a specific request.

ServiceConfig
14
  • id

    string · required

    Unique within the provider, for example inference.standard.
  • name

    string · required

  • description

    string · optional

  • category

    ai-inference | market-data | compute | storage | api | data-processing | agent-services | other · required

    The protocol's closed list; the registry indexes by it.
  • endpoint

    string · required

    Path of the paid endpoint, relative to url. The gate is mounted here.
  • method

    GET | POST | PUT | PATCH | DELETE · optional

    Default POST.
  • price

    string | { 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.required

    boolean · 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.ttlSeconds

    number · optional

    Quote lifetime for this service.
  • maxTimeoutSeconds

    number · optional

    How long an authorization stays valid, as advertised in the 402.
  • input, output

    JSON Schema · optional

    Shapes of the parameters and the result, published in the discovery document.
  • capabilities, tags, docsUrl

    string[], string[], url · optional

    Discovery metadata.

What the gate does

The payment middleware runs on every configured endpoint, before your handler.

  1. 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. 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. 3

    Verifies the payment

    Through the facilitator's /verify: the EIP-3009 authorization, the amount, the recipient, the window.
  4. 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. 5

    Runs your handler

    Only now. A handler error or a 4xx/5xx response cancels: nothing is settled.
  6. 6

    Settles

    Through the facilitator's /settle, after the handler, so a failed handler never costs the payer.
  7. 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.

Provider
9
  • 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.
  • config

    ResolvedProvider

    The options after defaults: asset, paths, services with resolved prices.
  • quotes, receipts

    QuoteStore, ReceiptStore

    The stores in use.
  • runtime

    PaymentRuntime

    The x402 resource server and facilitator client.
Endpoints the provider serves
6
  • /.well-known/epp402.json

    GET

    The signed discovery document.
  • /v1/quotes

    POST

    Request a quote: { serviceId, params?, quantity?, buyer?, sessionId? }.
  • /v1/quotes/:quoteId

    GET

    Read a quote this provider issued.
  • /v1/receipts/:receiptId

    GET

    Read a receipt this provider issued.
  • /v1/receipts/verify

    POST

    Verify a receipt against this provider's records and signature.
  • your endpoints

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

terminalRepository
# 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 --onchain

Programmatically, 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.

examples/
5
  • paid-api

    port 4021

    The smallest provider: one echo endpoint.
  • inference-provider

    port 4031

    Deterministic text analysis priced per request; an optional OpenAI-compatible passthrough.
  • market-data-provider

    port 4032

    Live perpetual market data from Hyperliquid's public API, sold per query.
  • compute-provider

    port 4033

    Metered CPU jobs: a hash search and a prime sieve, priced per million units, quotes required.
  • ai-agent

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