Skip to content

Developers · agent SDK

Epp402Agent, method by method.

One class drives the buyer side of the protocol. Every table here is read from the exported types in @epp402/client; nothing is promised that the code does not do.

Construct an agent

A signer, a network, and optionally a policy and a registry.

agent.tsRepository API
import { privateKeyToAccount } from "viem/accounts";
import { Epp402Agent } from "@epp402/client";

const agent = new Epp402Agent({
  signer: privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`),
  network: "elysium-testnet",
  registryUrl: "https://epp402-core.fly.dev", // optional: publishes sessions, quotes, intents and receipts
});

const { document } = await agent.discover("https://inference.example.com");
const asset = document.assets[0]!.address;

const session = await agent.openSession({ asset, spendingLimit: "25000000" }); // 25 EPPUSD
const quote = await agent.requestQuote(document, "inference.standard", { text: "Summarise Q3." }, { session });

const paid = await agent.pay(quote, { session, body: { text: "Summarise Q3." } });
console.log(paid.settlement.txHash, paid.receipt?.receiptId, paid.verification?.ok);

await agent.completeSession(session);
AgentOptions
9
  • signer

    AgentSigner · required

    An address and signTypedData: a viem LocalAccount works as is. Signs intents and session attestations, and the x402 authorizations.
  • network

    "local" | "elysium-testnet" | NetworkDefinition · required

    The chain the agent pays on. A provider that does not settle there is refused at discover.
  • policy

    SpendingPolicy · optional

    Limits enforced before anything is signed. Without one only x402's own spend controls apply.
  • agent

    string · optional

    A label recorded on sessions, so the explorer can show which program paid.
  • sessionStore

    SessionStore · optional

    Where sessions live. Defaults to memory; a Postgres store is in @epp402/session.
  • registryUrl

    string · optional

    Publishes signed session snapshots on every change, and the quote, intent and verified receipt after each payment.
  • fetch

    FetchLike · optional

    A fetch to use for every request, for tests and proxies.
  • log

    (event, data) => void · optional

    Structured events: discover, quote, session.open, intent.signed, paid, registry.publish_failed, …
  • now

    () => number · optional

    A clock in Unix seconds, for tests.

The lifecycle

Methods in the order a purchase uses them.

Methods
9
  • discover

    (providerUrl: string) => Promise<Discovered>

    Fetches and validates /.well-known/epp402.json. Discovered has document, discoveryUrl and signatureValid (true, false, or null when unsigned).
  • searchServices

    (filter?: { category?, q?, limit? }) => Promise<unknown[]>

    Queries the registry's /v1/services. Needs registryUrl.
  • openSession

    ({ asset, spendingLimit, ttlSeconds?, metadata?, network? }) => Promise<Session>

    A budget in minor units of asset. ttlSeconds defaults to 3600.
  • requestQuote

    (document, serviceId, params?, { quantity?, session? }?) => Promise<Quote>

    POSTs the provider's quotes endpoint with your address and session, verifies the provider's signature, attaches the quote to the session.
  • attachQuote

    (session, quote) => Promise<Session>

    Attach a quote obtained elsewhere.
  • pay

    (quote, options?: PayOptions) => Promise<PaymentResult>

    The payment, described below. Throws PaymentFailed.
  • completeSession

    (session, acceptPartial = false) => Promise<Session>

    Moves the session to completed. With acceptPartial, a partially settled session completes too.
  • intent

    (intentId) => PaymentIntent | undefined

    An intent this agent created, with its current status.
  • verifyReceipt

    (receipt, options?) => Promise<ReceiptVerification>

    The same verification pay runs, for a receipt you obtained elsewhere.

What pay does

One call, nine steps, each one visible in the result.

  1. 1

    Checks the quote

    Unexpired, signed by the provider named in the discovery document, priced in the agent's network and asset.
  2. 2

    Applies the policy

    Per-transaction, per-session and per-day limits, allowed providers and assets, the approval threshold. A refusal throws PaymentFailed with policy_violation or approval_required before anything is signed.
  3. 3

    Builds the intent

    A Payment Intent for the quote, recorded on the session as authorized.
  4. 4

    Sends the request

    With EPP-QUOTE, EPP-SESSION and EPP-INTENT (the intent id). The provider answers 402 with PAYMENT-REQUIRED built from the quote.
  5. 5

    Signs the authorization

    The x402 client refuses requirements that differ from the quote, then signs the EIP-3009 TransferWithAuthorization.
  6. 6

    Binds and signs the intent

    The intent takes the authorization's nonce and window and is signed by the agent key.
  7. 7

    Retries with the payment

    PAYMENT-SIGNATURE carries the x402 payload; EPP-INTENT now carries the signed intent, which the provider verifies before fulfilling.
  8. 8

    Reads the settlement and receipt

    PAYMENT-RESPONSE gives the transaction; EPP-RECEIPT gives the provider's signed receipt, verified against the quote, the payer, the amount, the hash and, with options.verify, the chain.
  9. 9

    Records and publishes

    The session records the settlement and receipt; with registryUrl the quote, intent and verified receipt are posted to the registry.
PayOptions
7
  • session

    Session | string · optional

    The session the payment belongs to.
  • body

    unknown · optional

    Request body for the paid call; objects are JSON-encoded with content-type application/json.
  • headers

    Record<string, string> · optional

    Extra request headers.
  • method

    string · optional

    Override the service's method.
  • url

    string · optional

    Override the service's endpoint URL.
  • approved

    boolean · optional

    A human approved an amount above the policy's approvalThreshold.
  • verify

    { publicClient?, settlementContract? } · optional

    Also check the receipt against the chain: the transaction, the transfer, and the relay's commerce reference.
PaymentResult
8
  • response

    Response

    The paid response, body unread.
  • quote

    Quote

    The quote that was paid.
  • intent

    PaymentIntent

    Signed, bound to the authorization, status settled.
  • settlement

    Settlement

    Built from PAYMENT-RESPONSE: transaction hash, payer, amount, status confirmed.
  • x402

    SettleResponse

    The facilitator's response as decoded from the header.
  • receipt

    Receipt | undefined

    The provider's signed receipt, when it sent one.
  • verification

    ReceiptVerification | undefined

    ok, signatureValid, schemaValid, chain, problems.
  • session

    Session | undefined

    The session after recording the settlement and receipt.

When it fails

pay throws PaymentFailed, an EppError with the protocol's code, the HTTP response when there was one, and the facilitator's x402 response when settlement was attempted.

errors.tsRepository API
import { PaymentFailed } from "@epp402/client";

try {
  await agent.pay(quote, { session, body });
} catch (error) {
  if (error instanceof PaymentFailed) {
    // error.code: payment_invalid | settlement_failed | policy_violation | approval_required | …
    console.error(error.code, error.message, error.x402?.errorReason);
  } else throw error;
}
Codes pay can throw
8
  • quote_expired

    before signing

    The quote's expiresAt has passed.
  • quote_invalid_signature

    before signing

    The quote does not recover to the provider's address.
  • quote_mismatch

    at the 402

    The provider's PAYMENT-REQUIRED differs from the quote it named.
  • policy_violation

    before signing

    A limit, provider or asset rule refused the payment.
  • approval_required

    before signing

    Above approvalThreshold and approved was not passed.
  • session_limit_exceeded

    before signing

    spent + authorized + amount would exceed the session's limit.
  • payment_invalid

    at the request

    The provider refused the payment (400) or the request failed.
  • settlement_failed

    at the request

    The provider answered 402 again: the facilitator could not settle. x402.errorReason says why.

Spending policy

A buyer-side document the agent checks before it signs. It never leaves the agent.

policy.tsRepository API
import type { SpendingPolicy } from "@epp402/protocol";

// Enforced in the agent before anything is signed; never leaves the buyer.
const policy: SpendingPolicy = {
  version: "1.0.0",
  name: "research-agent",
  network: "eip155:99801",
  asset: "0x…EPPUSD",
  maxPerTransaction: "2000000", //   2 EPPUSD
  maxPerSession: "25000000", //     25 EPPUSD
  maxPerDay: "100000000", //       100 EPPUSD
  authorizationTtlSeconds: 300,
  approvalThreshold: "10000000", // above 10 EPPUSD, pay(quote, { approved: true }) is required
};

const agent = new Epp402Agent({ signer, network: "elysium-testnet", policy });
SpendingPolicy
11
  • version

    "1.0.0" · required

  • name

    string · optional

    A label.
  • network

    eip155:<chainId> · required

    Quotes on other networks are refused.
  • asset

    address · required

    The payment asset the policy covers.
  • maxPerTransaction

    amount · optional

    Ceiling for one quote.
  • maxPerSession

    amount · optional

    Ceiling for a session's total.
  • maxPerDay

    amount · optional

    Rolling 24-hour ceiling, kept in the agent's ledger.
  • allowedProviders

    (providerId | address)[] · optional

    When set, only these providers may be paid.
  • allowedAssets

    { network, address }[] · optional

    Additional assets the agent may pay in.
  • authorizationTtlSeconds

    integer ≥ 30 · optional

    How long a signed authorization stays valid.
  • approvalThreshold

    amount · optional

    Above this, pay needs approved: true.

Amounts are decimal strings in the asset's minor units (EPPUSD has 6 decimals, so 1 EPPUSD is 1000000).

Verifying a receipt

The same function the explorer's verifier and the CLI use.

verify.tsRepository API
import { createPublicClient, http } from "viem";
import { verifyReceipt } from "@epp402/receipts";

const publicClient = createPublicClient({ transport: http("https://testnet-rpc.elysium.kinetiq.xyz") });
const result = await verifyReceipt(receipt, {
  publicClient,
  expectedProvider: document.provider.address,
  expectedRecipient: quote.payTo,
  settlementContract: "0x…EPP402Settlement", // from contracts/deployments/99801.json
});
// result.ok, result.signatureValid, result.chain?.transferMatches, result.problems

Without a publicClient only the schema and the signature are checked. With one, the transaction receipt is read, the asset's Transfer event must match the payer, the recipient and the amount, and when a settlement contract is given its Settled event must carry the commerce reference of the receipt's quote, intent and session. Try it on the receipt verifier.