npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

uvd-x402-sdk

v2.99.1

Published

x402 Payment SDK - Gasless crypto payments across 29 networks via Ultravioleta facilitator. Supports EVM (including Scroll, SKALE Base, Robinhood Chain, Arc mainnet + testnet), Solana, Fogo, Stellar, NEAR, Algorand, Sui, XRP Ledger, and native Hedera main

Readme

uvd-x402-sdk

Gasless crypto payments across 29 blockchain networks using the x402 protocol.

Users sign a message or transaction, and the Ultravioleta facilitator handles on-chain settlement. No gas fees for users.

New in v2.94.0: EURC on Arc mainnet and testnet, six-decimal euro amounts and token-specific signatures. Gas remains USDC. Funded EURC payments confirmed on Arc mainnet (x402 v1 and v2, 2026-09-22); Arc testnet funded acceptance pending. Arc EURC guide.

New in v2.95.0: Hedera mainnet and testnet accept native USDC payments only, with offline signing and buyer/merchant helpers. HBAR funds sponsor network fees and is rejected as payment. Hedera guide. Arc USDC/EURC remains supported on mainnet and testnet.

Features

  • 29 Networks: EVM (17 including Arc, Scroll, SKALE Base, Robinhood Chain), Solana, Fogo, Stellar, NEAR, Algorand, Sui, XRP Ledger, Hedera (mainnet + testnet)
  • Multi-Stablecoin: USDC, EURC, AUSD, PYUSD, USDT, USDG (Robinhood Chain)
  • x402 v1 & v2: Protocol auto-detection; native Hedera supports v2/exact only
  • Gasless: Facilitator pays all network fees
  • Buyer Policy: Per-payment and cumulative budgets, payee allowlist and offer expiry, evaluated against the offer in hand before signing — six closed refusal codes in a fixed order
  • Type-Safe: Full TypeScript support
  • React & Wagmi: First-class integrations
  • Signing Wallet Adapters: EnvKeyAdapter (server/CLI), OWSWalletAdapter (Open Wallet Standard), or bring your own
  • ERC-8128 Signed Requests: Authenticate HTTP requests with a wallet (RFC 9421 + EIP-191) — no API keys
  • ERC-8004 Trustless Agents: On-chain reputation and identity across 23 networks (21 EVM + 2 Solana)
  • Escrow & Refunds: Hold payments with dispute resolution
  • Advanced Escrow: Full escrow lifecycle (authorize, release, refund, charge) with SigningWalletAdapter support
  • Escrow Pre-Auth: Sign-on-assignment X-Payment-Auth builder (buildEscrowPreAuth) — vector-pinned parity with the Python SDK and Execution Market
  • Commerce Scheme: 'commerce' scheme alias for marketplace integrations (identical to 'escrow' on-chain)
  • /accepts Negotiation: Discover facilitator capabilities before constructing payments
  • Bazaar Discovery: Register and discover paid resources across the x402 network
  • Live Traffic Stream: Subscribe to GET /events (SSE) for settlements as they happen — lossy live hint, not a ledger
  • Facilitator Info: Query version, supported networks, blacklist, and health

Installation

npm install uvd-x402-sdk

Peer Dependencies

# EVM (included by default)
npm install ethers@^6

# Solana/Fogo
npm install @solana/web3.js @solana/spl-token

# Stellar
npm install @stellar/stellar-sdk @stellar/freighter-api

# NEAR
npm install @near-wallet-selector/core @near-wallet-selector/my-near-wallet

# Algorand
npm install algosdk lute-connect

# Sui
npm install @mysten/sui

# XRPL (XRP Ledger)
npm install xrpl

Quick Start

Server + Client (Private Key)

The fastest way to get up and running. No browser wallet needed — works in Node.js, scripts, and agents.

.env

RECEIVING_ADDRESS=0xYourWalletAddress
PRIVATE_KEY=0xYourPrivateKey

Server (Hono)

npm install hono @hono/node-server uvd-x402-sdk dotenv
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { createHonoMiddleware } from 'uvd-x402-sdk';
import 'dotenv/config';

const app = new Hono();
const receiver = process.env.RECEIVING_ADDRESS as string;

// x402 payment middleware — handles 402, verify, and settle automatically
const paywall = createHonoMiddleware({
  accepts: [{
    network: 'skale-base',
    asset: '0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20',
    amount: '1000000', // $1.00 USDC.e (6 decimals)
    payTo: receiver,
    extra: {
      name: 'Bridged USDC (SKALE Bridge)',
      version: '2',
    },
  }],
});

app.get('/api/free', (c) => c.json({ message: 'This endpoint is free!' }));

app.get('/api/premium', paywall, (c) => {
  return c.json({ message: 'Payment verified and settled!', timestamp: new Date().toISOString() });
});

serve({ fetch: app.fetch, port: 3000 });
console.log('Server running on http://localhost:3000');

Client (Private Key)

npm install uvd-x402-sdk ethers dotenv
import { X402Client } from 'uvd-x402-sdk';
import 'dotenv/config';

const client = new X402Client({ defaultChain: 'skale-base' });
await client.connectWithPrivateKey(process.env.PRIVATE_KEY as string);

const result = await client.createPayment({
  recipient: process.env.RECEIVING_ADDRESS as string,
  amount: '1.00',
});

const response = await fetch('http://localhost:3000/api/premium', {
  headers: { 'X-PAYMENT': result.paymentHeader },
});

const data = await response.json();
console.log('Response:', data);

This example uses SKALE Base (zero gas costs). Replace network, asset, and extra to use any supported chain — see Supported Networks.

EVM Chains (Browser Wallet)

import { X402Client } from 'uvd-x402-sdk';

const client = new X402Client({ defaultChain: 'base' });
const address = await client.connect('base');

const result = await client.createPayment({
  recipient: '0x...',
  amount: '10.00',
});

await fetch('/api/purchase', {
  headers: { 'X-PAYMENT': result.paymentHeader },
});

Solana

import { SVMProvider } from 'uvd-x402-sdk/solana';
import { getChainByName } from 'uvd-x402-sdk';

const svm = new SVMProvider();
const address = await svm.connect();
const chainConfig = getChainByName('solana')!;

const payload = await svm.signPayment({
  recipient: '5Y32Dk6weq1LrMRdujpJyDbTN3SjwXGoQS9QN39WQ9Cq',
  amount: '10.00',
}, chainConfig);

const header = svm.encodePaymentHeader(payload, chainConfig);

Algorand

import { AlgorandProvider } from 'uvd-x402-sdk/algorand';
import { getChainByName } from 'uvd-x402-sdk';

const algorand = new AlgorandProvider();
const address = await algorand.connect(); // Lute or Pera wallet
const chainConfig = getChainByName('algorand')!;

const payload = await algorand.signPayment({
  recipient: 'NCDSNUQ2QLXDMJXRALAW4CRUSSKG4IS37MVOFDQQPC45SE4EBZO42U6ZX4',
  amount: '10.00',
}, chainConfig);

const header = algorand.encodePaymentHeader(payload, chainConfig);

Algorand uses atomic transaction groups:

  • Transaction 0: Fee payment (unsigned, facilitator signs)
  • Transaction 1: USDC ASA transfer (signed by user)

Sui

import { SuiProvider } from 'uvd-x402-sdk/sui';
import { getChainByName } from 'uvd-x402-sdk';

const sui = new SuiProvider();
const address = await sui.connect(); // Sui Wallet
const chainConfig = getChainByName('sui')!;

const payload = await sui.signPayment({
  recipient: '0x1234...', // 66-char Sui address
  amount: '10.00',
}, chainConfig);

const header = sui.encodePaymentHeader(payload, chainConfig);

Sui uses sponsored transactions:

  • User creates and signs a programmable transaction block
  • Facilitator sponsors gas (pays in SUI)
  • User pays zero gas fees

XRPL (XRP Ledger)

import { XRPLProvider } from 'uvd-x402-sdk/xrpl';
import { getChainByName } from 'uvd-x402-sdk';

// Seed-based signer (Node.js / server-side)
const xrpl = new XRPLProvider({ seed: process.env.XRPL_SEED });
const address = await xrpl.connect(); // classic r-address
const chainConfig = getChainByName('xrpl-mainnet')!;

// Build + FULLY sign the Payment off-chain. Returns JSON: { signedTxBlob }
const payload = await xrpl.signPayment({
  recipient: 'rfADKkVXBNqK3z72tVSS3LVzAR3psYkonp', // classic r-address
  amount: '10.00',
}, chainConfig);

const header = xrpl.encodePaymentHeader(payload);

XRPL uses the t54 "pre-signed Payment blob" scheme:

  • The client builds and FULLY signs a native XRP Payment off-chain (paying its own XRP fee)
  • Only one field is sent to the facilitator: { "signedTxBlob": "<hex tx blob>" }
  • The facilitator decodes the blob to re-derive payer/amount/destination and submits it
  • The Payment sets LastLedgerSequence, must NOT set tfPartialPayment, and must NOT use SendMax
  • All payment-level fields (destination, amount, InvoiceID, SourceTag, Memo) come from the requirements

Requires the optional peer dependency xrpl (npm install xrpl).

Stellar

import { StellarProvider } from 'uvd-x402-sdk/stellar';
import { getChainByName } from 'uvd-x402-sdk';

const stellar = new StellarProvider();
const address = await stellar.connect(); // Freighter wallet
const chainConfig = getChainByName('stellar')!;

const payload = await stellar.signPayment({
  recipient: 'GD3FWQ4QFSCO2F2KVXZPQWOC27CQHXHYCRCRRZBMWU3DNOZW2IIGOU54',
  amount: '10.00',
}, chainConfig);

const header = stellar.encodePaymentHeader(payload);

NEAR

Important: The SDK's NEARProvider.signPayment() only works with injected wallets (browser extensions). For browser-redirect wallets like MyNearWallet via @near-wallet-selector, you must use the popup flow below.

Option 1: Injected Wallet (Browser Extension)

import { NEARProvider } from 'uvd-x402-sdk/near';
import { getChainByName } from 'uvd-x402-sdk';

const near = new NEARProvider();
const accountId = await near.connect(); // MyNearWallet browser extension
const chainConfig = getChainByName('near')!;

const payload = await near.signPayment({
  recipient: 'merchant.near',
  amount: '10.00',
}, chainConfig);

const header = near.encodePaymentHeader(payload);

Option 2: Browser-Redirect Wallet (Popup Flow) - Recommended

MyNearWallet via @near-wallet-selector is a browser-redirect wallet that requires a popup flow. This is the recommended approach and works with the custom MyNearWallet deployment that supports NEP-366.

import { setupWalletSelector } from '@near-wallet-selector/core';
import { setupModal } from '@near-wallet-selector/modal-ui';
import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
import '@near-wallet-selector/modal-ui/styles.css';

// Configuration
const NEAR_CONFIG = {
  usdcContract: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
  recipientAccount: 'merchant.near',
  // Custom MyNearWallet with NEP-366 signDelegateAction support
  walletUrl: 'https://mynearwallet.ultravioletadao.xyz',
};

// Step 1: Initialize wallet selector
const selector = await setupWalletSelector({
  network: 'mainnet',
  modules: [
    setupMyNearWallet({ walletUrl: NEAR_CONFIG.walletUrl }),
  ],
});

const modal = setupModal(selector, {
  contractId: NEAR_CONFIG.usdcContract,
});

// Step 2: Connect wallet
modal.show(); // User selects wallet
// Wait for connection via selector.store.observable.subscribe()
const state = selector.store.getState();
const accountId = state.accounts[0].accountId;

// Step 3: Create payment with popup flow
async function createNearPayment(amount: string): Promise<string> {
  const amountRaw = Math.floor(parseFloat(amount) * 1_000_000); // 6 decimals

  // Get access key info and block height from RPC
  const [accessKeyInfo, blockInfo] = await Promise.all([
    getNearAccessKeyInfo(accountId),
    getNearBlockHeight(),
  ]);

  const nonce = accessKeyInfo.nonce + 1;
  const maxBlockHeight = blockInfo.blockHeight + 1000; // ~17 minutes

  // Build wallet URL for signDelegateAction
  const popupUrl = new URL(NEAR_CONFIG.walletUrl);
  popupUrl.pathname = '/sign-delegate-action';
  popupUrl.searchParams.set('receiverId', NEAR_CONFIG.usdcContract);
  popupUrl.searchParams.set('actions', JSON.stringify([{
    methodName: 'ft_transfer',
    args: {
      receiver_id: NEAR_CONFIG.recipientAccount,
      amount: amountRaw.toString(),
      memo: 'x402 payment',
    },
    gas: '30000000000000', // 30 TGas
    deposit: '1', // 1 yoctoNEAR
  }]));
  popupUrl.searchParams.set('callbackUrl', window.location.origin + '/near-callback');
  popupUrl.searchParams.set('meta', JSON.stringify({
    sender: accountId,
    nonce,
    maxBlockHeight,
    publicKey: accessKeyInfo.publicKey,
  }));

  // Open popup
  const popup = window.open(popupUrl.toString(), 'nearWallet', 'width=500,height=700');
  if (!popup) throw new Error('Popup blocked. Please allow popups.');

  // Wait for redirect with signedDelegateAction
  const signedDelegateAction = await new Promise<string>((resolve, reject) => {
    const checkInterval = setInterval(() => {
      if (popup.closed) {
        clearInterval(checkInterval);
        reject(new Error('Wallet popup closed'));
        return;
      }
      try {
        const url = popup.location.href;
        if (url.includes('signedDelegateAction=')) {
          clearInterval(checkInterval);
          popup.close();
          const params = new URLSearchParams(new URL(url).search);
          const errorCode = params.get('errorCode');
          if (errorCode) {
            reject(new Error(params.get('errorMessage') || errorCode));
            return;
          }
          resolve(params.get('signedDelegateAction')!);
        }
      } catch { /* cross-origin, keep waiting */ }
    }, 500);

    setTimeout(() => {
      clearInterval(checkInterval);
      if (!popup.closed) popup.close();
      reject(new Error('Popup timeout'));
    }, 300000); // 5 min timeout
  });

  // Return x402 payload
  return JSON.stringify({
    signedDelegateAction,
    network: 'near',
  });
}

// Helper: Get access key info from NEAR RPC
async function getNearAccessKeyInfo(accountId: string) {
  const rpcUrls = [
    'https://near.drpc.org',
    'https://rpc.mainnet.near.org',
  ];

  for (const rpcUrl of rpcUrls) {
    try {
      const response = await fetch(rpcUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          jsonrpc: '2.0',
          id: 'dontcare',
          method: 'query',
          params: {
            request_type: 'view_access_key_list',
            finality: 'final',
            account_id: accountId,
          },
        }),
      });
      const data = await response.json();
      if (data.error) continue;

      const fullAccessKey = data.result.keys.find(
        (k: any) => k.access_key.permission === 'FullAccess'
      );
      return {
        nonce: fullAccessKey.access_key.nonce,
        publicKey: fullAccessKey.public_key,
      };
    } catch { continue; }
  }
  throw new Error('Failed to get NEAR access key info');
}

// Helper: Get block height from NEAR RPC
async function getNearBlockHeight() {
  const rpcUrls = [
    'https://near.drpc.org',
    'https://rpc.mainnet.near.org',
  ];

  for (const rpcUrl of rpcUrls) {
    try {
      const response = await fetch(rpcUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          jsonrpc: '2.0',
          id: 'dontcare',
          method: 'block',
          params: { finality: 'final' },
        }),
      });
      const data = await response.json();
      if (data.error) continue;
      return { blockHeight: data.result.header.height };
    } catch { continue; }
  }
  throw new Error('Failed to get NEAR block height');
}

// Step 4: Encode payment header
function encodeNearPaymentHeader(payload: string): string {
  const parsed = JSON.parse(payload);
  const x402Payload = {
    x402Version: 1,
    scheme: 'exact',
    network: 'near',
    payload: {
      signedDelegateAction: parsed.signedDelegateAction,
    },
  };
  return btoa(JSON.stringify(x402Payload));
}

// Usage
const payload = await createNearPayment('10.00');
const header = encodeNearPaymentHeader(payload);
await fetch('/api/purchase', {
  headers: { 'X-PAYMENT': header },
});

See 402milly's full implementation for a production-ready example.

Wagmi/RainbowKit

import { useWalletClient } from 'wagmi';
import { createPaymentFromWalletClient } from 'uvd-x402-sdk/wagmi';

function PayButton() {
  const { data: walletClient } = useWalletClient();

  const handlePay = async () => {
    const paymentHeader = await createPaymentFromWalletClient(walletClient, {
      recipient: '0x...',
      amount: '10.00',
      chainName: 'base',
    });

    await fetch('/api/purchase', {
      headers: { 'X-PAYMENT': paymentHeader },
    });
  };

  return <button onClick={handlePay}>Pay $10</button>;
}

Signing Wallet Adapters

Low-level signing primitives for server-side agents, CLI tools, and Open Wallet Standard wallets. These adapters implement EIP-191, EIP-712, and EIP-3009 (gasless USDC transfers).

EnvKeyAdapter (Server / CLI / Agents)

Signs with a raw private key from the environment or constructor. Never use in browser contexts.

import { EnvKeyAdapter } from 'uvd-x402-sdk';

// Option 1: Reads process.env.WALLET_PRIVATE_KEY
const wallet = new EnvKeyAdapter();

// Option 2: Explicit key
const wallet = new EnvKeyAdapter(process.env.MY_AGENT_KEY!);

console.log(wallet.getAddress()); // 0x...

// Sign EIP-3009 gasless USDC transfer
const auth = await wallet.signEIP3009({
  to: '0xRecipient...',
  amountUsdc: 1.00,
  network: 'base',
});
// auth contains: from, to, value, nonce, v, r, s, signature

// Sign arbitrary message (EIP-191)
const sig = await wallet.signMessage('Hello x402');

// Sign EIP-712 typed data
const result = await wallet.signTypedData(JSON.stringify({
  domain: { name: 'MyApp', version: '1', chainId: 8453 },
  types: { Order: [{ name: 'id', type: 'uint256' }] },
  primaryType: 'Order',
  message: { id: 42 },
}));

OWSWalletAdapter (Open Wallet Standard)

Delegates signing to any wallet that implements the Open Wallet Standard. Works with browser wallets, agent vaults, and hardware-backed signers.

npm install @open-wallet-standard/core  # optional peer dependency
import { OWSWalletAdapter } from 'uvd-x402-sdk';

const wallet = new OWSWalletAdapter(owsWalletInstance);

const auth = await wallet.signEIP3009({
  to: '0xRecipient...',
  amountUsdc: 0.50,
  network: 'base',
});

Custom Adapter

Implement the SigningWalletAdapter interface for your own signer:

import type { SigningWalletAdapter, EIP3009Params, EIP3009Authorization } from 'uvd-x402-sdk';

class MyAdapter implements SigningWalletAdapter {
  getAddress(): string { /* ... */ }
  async signMessage(message: string): Promise<string> { /* ... */ }
  async signTypedData(typedData: string): Promise<{ signature: string; v: number; r: string; s: string }> { /* ... */ }
  async signEIP3009(params: EIP3009Params): Promise<EIP3009Authorization> { /* ... */ }
}

ERC-8128 Signed Requests

Authenticate HTTP requests with a wallet instead of an API key. The SDK builds the RFC 9421 signature base, signs it with EIP-191 personal_sign, and produces the Signature, Signature-Input, and (for bodies) Content-Digest headers. Used by APIs that only accept wallet signing, like Execution Market.

Wire format is pinned by golden vectors (src/erc8128.vectors.json): alg="eip191", keyid always lowercase (erc8128:{chainId}:{address}), params in the order created;expires;nonce;keyid;alg.

import { createSignedFetch, EnvKeyAdapter } from 'uvd-x402-sdk';

// Auto-signing fetch: fetches a fresh nonce and signs every request
const signedFetch = createSignedFetch({
  wallet: new EnvKeyAdapter(),           // or privateKey: process.env.KEY!
  apiBase: 'https://api.execution.market',
  chainId: 8453,                          // Base (default)
});

const resp = await signedFetch('/api/v1/tasks', {
  method: 'POST',
  body: JSON.stringify({ title: 'test' }),
});

For manual control, sign a single request (the nonce is single-use — fetch one per request):

import { fetchNonce, signRequestWithWallet, EnvKeyAdapter } from 'uvd-x402-sdk';

const wallet = new EnvKeyAdapter();
const nonce = await fetchNonce('https://api.execution.market');
const headers = await signRequestWithWallet(wallet, {
  method: 'POST',
  url: 'https://api.execution.market/api/v1/tasks',
  body: '{"title":"test"}',
  nonce,
});
// headers = { Signature, 'Signature-Input', 'Content-Digest' } — merge into your request

Also available: signRequest (raw private key) and signRequestWithSigner (callback-based, for browser wallets / out-of-process signers where the key never leaves the signer), plus buildSignatureBase / buildSignatureParams to reproduce the exact signed bytes externally.

Escrow Pre-Auth (sign-on-assignment)

Build and sign the EIP-3009 ReceiveWithAuthorization that locks a bounty in the x402r AuthCaptureEscrow, packed as the raw-JSON X-Payment-Auth wrapper the facilitator's /settle expects. Used by marketplaces on the escrow rail (e.g. Execution Market's universal escrow).

The EIP-3009 nonce is AuthCaptureEscrow.getHash(paymentInfo), which includes the receiver — the signature cryptographically commits to the chosen worker, so it can only be created AT ASSIGNMENT. The wire format is pinned by golden vectors (src/escrow-preauth.vectors.json) shared with the Python SDK and Execution Market's dashboard/mobile suites.

import { buildEscrowPreAuth, EnvKeyAdapter } from 'uvd-x402-sdk';

// Escrow config as published by the marketplace server
// (e.g. Execution Market's GET /api/v1/h2a/payment-config).
const paymentAuth = await buildEscrowPreAuth(new EnvKeyAdapter(), {
  networkConfig: config.escrow.networks.base,
  payerWallet: '0xPublisher...',
  workerWallet: '0xWorker...',        // escrow receiver — committed by the nonce
  bountyAtomic: '100000',             // $0.10 in 6-decimal USDC
  reviewDeadlineSec: taskDeadline,    // release window outlasts it
});
// Send as the X-Payment-Auth header (raw JSON, NOT base64).

Any SigningWalletAdapter works as the signer (only signTypedData is used); browser wallets can pass a minimal { signTypedData } wrapper. Validation fails loud instead of falling back: incomplete network config, unknown tier, bounty outside the on-chain deposit limit ($100), or a maxFeeBps below the operator's 1300 bps all throw before anything is signed. computeEscrowNonce is exported to reproduce AuthCaptureEscrow.getHash externally.

Multi-Stablecoin (EVM)

// Pay with EURC instead of USDC
const result = await client.createPayment({
  recipient: '0x...',
  amount: '10.00',
  tokenType: 'eurc', // 'usdc' | 'eurc' | 'ausd' | 'pyusd' | 'usdt'
});

// Check token availability
import { getSupportedTokens, isTokenSupported } from 'uvd-x402-sdk';

getSupportedTokens('ethereum'); // ['usdc', 'eurc', 'ausd', 'pyusd']
getSupportedTokens('base');     // ['usdc', 'eurc']
isTokenSupported('base', 'eurc'); // true

Validity window (validitySeconds)

An EIP-3009 authorization can only be settled until validBefore = now + validitySeconds. A seller that settles after handing over the resource needs the authorization alive long enough for that settlement to land.

| Where | Scope | Default | |---|---|---| | X402ClientConfig.validitySeconds | every payment this client signs | 300 | | PaymentInfo.validitySeconds | one payment; wins over the client | — | | maxTimeoutSeconds in the seller's 402 | client.fetch() signs the seller's declared window | — |

import { X402Client, DEFAULT_VALIDITY_SECONDS, MAX_VALIDITY_SECONDS } from 'uvd-x402-sdk';

// Every payment from this client stays settleable for 10 minutes...
const client = new X402Client({ defaultChain: 'avalanche', validitySeconds: 600 });

// ...except this one, which the seller settles in batches
const result = await client.createPayment({
  recipient: '0x...',
  amount: '1.00',
  validitySeconds: 1800,
});

DEFAULT_VALIDITY_SECONDS; // 300
MAX_VALIDITY_SECONDS;     // 3600
  • A value that is not a whole number between 1 and MAX_VALIDITY_SECONDS throws INVALID_CONFIG: from the constructor for the client config, and before anything is signed for a single payment.
  • client.fetch() signs the maxTimeoutSeconds the seller's 402 declares, clamped to 1–3600 s. When the 402 declares none, the client's window applies.
  • The facilitator keeps a 6 s clock-skew grace, so the payer really has validitySeconds - 6 seconds. It never rejects a window for being long.
  • Before 2.91.0 the window was 300 s on Base and 60 s on every other EVM network, and there was no way to change it.

AUSD on Solana (Token2022)

import { SVMProvider } from 'uvd-x402-sdk/solana';
import { getChainByName } from 'uvd-x402-sdk';

const svm = new SVMProvider();
const chainConfig = getChainByName('solana')!;

// AUSD uses Token2022 program
const payload = await svm.signPayment({
  recipient: '5Y32Dk...',
  amount: '10.00',
  token: 'ausd', // Token2022 AUSD
}, chainConfig);

const header = svm.encodePaymentHeader(payload, chainConfig);

Supported Networks

EVM (16)

| Network | Chain ID | Tokens | |---------|----------|--------| | Base | 8453 | USDC, EURC | | Ethereum | 1 | USDC, EURC, AUSD, PYUSD, USDT | | Polygon | 137 | USDC, AUSD | | Arbitrum | 42161 | USDC, AUSD, USDT | | Optimism | 10 | USDC, USDT | | Avalanche | 43114 | USDC, EURC, AUSD | | Celo | 42220 | USDC, USDT | | HyperEVM | 999 | USDC | | Unichain | 130 | USDC | | Monad | 143 | USDC, AUSD | | Scroll | 534352 | USDC | | SKALE Base | 1187947933 | USDC.e | | SKALE Base Sepolia | 324705682 | USDC.e | | Robinhood Chain | 4663 | USDG | | Robinhood Chain Testnet | 46630 | USDG | | Arc | 5042 | USDC, EURC | | Arc Testnet | 5042002 | USDC, EURC |

Arc mainnet and testnet: payment amounts use 6 decimals. USDC signs with the USDC / 2 domain; EURC (0xbEf5…21c1 mainnet, 0x89B5…D72a testnet) signs with EURC / 2 and its amounts are euros, not dollars (tokenType: 'eurc', usdPegged: false). Native gas uses 18 decimals on the same balance. Use arc / eip155:5042 for mainnet and arc-testnet / eip155:5042002 for testnet. See Arc usage and validation.

Robinhood Chain / USDG: Robinhood Chain has no USDC — the settlement stablecoin is Paxos USDG (Global Dollar, 6 decimals, EIP-3009). Its on-chain version() getter reverts, so the EIP-712 domain { name: "Global Dollar", version: "1" } can never be resolved on-chain. The SDK carries this domain in the chain config; when constructing PaymentRequirements yourself, send it in extra: { "name": "Global Dollar", "version": "1" }. Use tokenType: 'usdg' (or the default, which resolves to USDG on these networks).

SVM (2)

| Network | Tokens | Wallet | |---------|--------|--------| | Solana | USDC, AUSD | Phantom | | Fogo | USDC | Phantom |

Algorand

| Network | USDC ASA | Wallet | |---------|----------|--------| | Algorand | 31566704 | Lute, Pera |

Sui

| Network | Tokens | Wallet | |---------|--------|--------| | Sui | USDC, AUSD | Sui Wallet |

XRPL

XRP Ledger settles in native XRP (6 decimals / drops) using the t54 pre-signed Payment blob scheme. The client builds and fully signs the Payment off-chain and sends { signedTxBlob } to the facilitator. There is no stablecoin/token contract on XRPL. Use network ids xrpl-mainnet and xrpl-testnet. See XRPL (XRP Ledger) above for the uvd-x402-sdk/xrpl provider usage.

| Network | Asset | Network ID | Provider | |---------|-------|------------|----------| | XRP Ledger | XRP (native) | xrpl-mainnet | uvd-x402-sdk/xrpl | | XRP Ledger Testnet | XRP (native) | xrpl-testnet | uvd-x402-sdk/xrpl |

Other

| Network | Wallet | |---------|--------| | Stellar | Freighter | | NEAR | MyNearWallet |

Facilitator Addresses

The SDK includes built-in facilitator addresses. You don't need to configure them.

import { FACILITATOR_ADDRESSES, getFacilitatorAddress } from 'uvd-x402-sdk';

// Built-in addresses
FACILITATOR_ADDRESSES.evm;      // 0x103040545AC5031A11E8C03dd11324C7333a13C7
FACILITATOR_ADDRESSES.solana;   // F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq
FACILITATOR_ADDRESSES.algorand; // KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I
FACILITATOR_ADDRESSES.stellar;  // GCHPGXJT2WFFRFCA5TV4G4E3PMMXLNIDUH27PKDYA4QJ2XGYZWGFZNHB
FACILITATOR_ADDRESSES.near;     // uvd-facilitator.near
FACILITATOR_ADDRESSES.sui;      // 0xe7bbf2b13f7d72714760aa16e024fa1b35a978793f9893d0568a4fbf356a764a
FACILITATOR_ADDRESSES['xrpl-mainnet']; // rfADKkVXBNqK3z72tVSS3LVzAR3psYkonp

// Or get by chain name
getFacilitatorAddress('algorand'); // KIMS5H6...
getFacilitatorAddress('base', 'evm'); // 0x1030...
getFacilitatorAddress('sui'); // 0xe7bbf...
getFacilitatorAddress('xrpl-mainnet'); // rfADKk...

Backend

import {
  FacilitatorClient,
  create402Response,
  extractPaymentFromHeaders,
  buildPaymentRequirements,
} from 'uvd-x402-sdk/backend';

// Return 402 if no payment
app.post('/api/premium', async (req, res) => {
  const payment = extractPaymentFromHeaders(req.headers);

  if (!payment) {
    const { status, headers, body } = create402Response({
      amount: '1.00',
      recipient: process.env.RECIPIENT,
      resource: 'https://api.example.com/premium',
      chainName: 'base',
    });
    return res.status(status).set(headers).json(body);
  }

  // Verify first, then settle when you're ready to fulfill the request
  const client = new FacilitatorClient();
  const requirements = buildPaymentRequirements({
    amount: '1.00',
    recipient: process.env.RECIPIENT,
    resource: 'https://api.example.com/premium',
    chainName: 'base',
    x402Version: payment.x402Version,
  });

  const verifyResult = await client.verify(payment, requirements);
  if (!verifyResult.isValid) {
    return res.status(402).json({ error: verifyResult.invalidReason });
  }

  const settleResult = await client.settle(payment, requirements);
  if (!settleResult.success) {
    return res.status(500).json({ error: settleResult.error });
  }

  res.json({ data: 'premium content', txHash: settleResult.transactionHash });
});

createPaymentMiddleware() and createHonoMiddleware() verify and settle automatically by default (before-handler). Use settlementStrategy: 'manual' if you need to control when settlement happens (e.g., settle only after confirming you can fulfill the request).

React

import { X402Provider, useX402, usePayment } from 'uvd-x402-sdk/react';

function App() {
  return (
    <X402Provider config={{ defaultChain: 'base' }}>
      <PaymentPage />
    </X402Provider>
  );
}

function PaymentPage() {
  const { connect, isConnected, address } = useX402();
  const { pay, isPaying } = usePayment();

  if (!isConnected) {
    return <button onClick={() => connect('base')}>Connect</button>;
  }

  return (
    <button onClick={() => pay({ recipient: '0x...', amount: '10.00' })} disabled={isPaying}>
      {isPaying ? 'Processing...' : 'Pay $10'}
    </button>
  );
}

Error Handling

import { X402Error } from 'uvd-x402-sdk';

try {
  await client.createPayment(paymentInfo);
} catch (error) {
  if (error instanceof X402Error) {
    switch (error.code) {
      case 'WALLET_NOT_FOUND': // Install wallet
      case 'WALLET_CONNECTION_REJECTED': // User rejected
      case 'INSUFFICIENT_BALANCE': // Not enough USDC
      case 'SIGNATURE_REJECTED': // User cancelled
      case 'CHAIN_NOT_SUPPORTED': // Unsupported network
    }
  }
}

Buyer policy — what this buyer is allowed to sign, decided before it signs

A catalog listing is a claim somebody else made about their own price. The 402 that comes back from the actual request is the offer, and they can differ legitimately: a seller may have repriced, and the listing may be a copy of a copy. So the buying decision is made against the offer in hand, every time, before anything is signed. Same contract the facilitator fixed in Rust (x402-reqwest, release 2.25.0), so a buyer in either language refuses the same payments for the same stated reasons.

import { X402Client, PurchasePolicy, PolicyRefusedError } from 'uvd-x402-sdk';

const USDC_BASE = {
  network: 'base',
  address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
};

// create() DENIES any asset it was not given a ceiling for.
const policy = PurchasePolicy.create()
  .perPayment(USDC_BASE, 50_000n)      // 0.05 USDC, atomic units, bigint
  .cumulative(USDC_BASE, 1_000_000n)   // 1 USDC total, for this policy's life
  .onlyPay(['0xe4dc963c56979E0260fc146b87eE24F18220e545']);

const client = new X402Client({ defaultChain: 'base', policy });
await client.connectWithPrivateKey(process.env.PRIVATE_KEY!, 'base');

try {
  const res = await client.fetch('https://api.example.com/data', {
    // What the catalog advertised, if you read one. It is REPORTED, never a gate.
    advertised: { asset: USDC_BASE, amount: 10_000n },
    // Evaluating does not spend. This is where a settled payment gets recorded.
    onPaid: (approval) => client.policy.recordSpend(approval.asset, approval.amount),
  });
  const data = await res.json();
} catch (err) {
  if (err instanceof PolicyRefusedError) {
    switch (err.refusal.code) {
      case 'offer-expired':           // ask the seller for new terms
      case 'asset-not-budgeted':      // budget that asset
      case 'per-payment-limit':       // one payment is too big
      case 'cumulative-limit':        // the budget is spent
      case 'recipient-not-permitted': // not a payee you allowed
      case 'no-readable-offer':       // err.refusal.offered names the schemes
    }
  }
}

Nothing changes for a caller who never writes a policy. A client without one holds PurchasePolicy.permissive(): this SDK had no budget before 2.89.0 and switching one on silently would refuse payments consumers are making today. The asymmetry is deliberate — whoever sits down to write a policy gets the deny-by-default one. maxAmount is untouched and still runs before the policy.

The fields

| field | type | meaning | |---|---|---| | perPayment(asset, amount) | bigint, atomic units | most this policy pays in ONE payment of that asset | | cumulative(asset, amount) | bigint, atomic units | most it pays in that asset in TOTAL, for as long as it lives | | spent(asset) | bigint | what has been recorded; only recordSpend moves it | | onlyPay([...]) | addresses | permitted recipients, canonicalised by family | | allowUnlistedAssets() | boolean, false by default | whether an asset with no declared ceiling may be paid |

An asset is a { network, address } pair, and both halves matter: the same contract address on two networks is two different assets. Use the SDK chain name ('base'), not the CAIP-2 string — the client resolves a v2 challenge's eip155:8453 to it, so one written policy covers both 402 dialects.

The order of evaluation is part of the contract

The FIRST failing check is the one reported, because a caller branches on it. Reporting per-payment-limit for an expired offer to a payee nobody allowed would tell the caller to raise a ceiling when the real fix is to ask the seller for new terms.

no-readable-offer → offer-expired → recipient-not-permitted
                  → asset-not-budgeted → per-payment-limit → cumulative-limit

The six are a closed kebab-case vocabulary (PolicyRefusalCode), typed as a literal union so you branch without parsing English. There is no other: a refusal a caller cannot interpret is one it will paper over. Each carries the numbers that caused it — requested, allowed, spent, wouldTotal, asset, payTo, validUntil, now, offered[].

asset-not-budgeted runs before the ceilings on purpose. The ceilings are a map, and a map has no opinion about a key it does not hold — which is exactly how an unlisted token sails past a budget that looks complete. And the EVM signer would have signed it: it takes its EIP-712 domain from the seller's own extra, for a token and a network it has never seen.

The seven rules

  1. Evaluating does not spend. Signing can fail and a settlement can be refused; a limit that counted attempts would lock you out of money you never spent. recordSpend is a separate call, after the settlement resolved — use onPaid for it.
  2. A policy is never widened from inside an evaluation. No method raises a ceiling: every builder returns a NEW policy and leaves the receiver exactly as strict as it was.
  3. No human confirmation when the policy already covers the operation. A divergence from the listing is not, by itself, a refusal: an offer that costs more than the catalog said but sits inside an authorised policy is paid. Halting there would turn every ordinary reprice into a stop, and an agent that halts on ordinary commerce is one nobody can leave running. There is no confirmation hook on this path.
  4. A different asset is not the same price. No numbers are compared across assets. An asset with no declared ceiling is denied by default; the permissive mode has to be asked for by name.
  5. A network name's case never decides anything ('Base' and 'base' are one network written twice), and addresses are canonicalised by family, never with toLowerCase(). Hex is folded; base58 (Solana, XRPL) is compared exactly. Folding a base58 address does not produce the same address spelled differently — it produces a string that is not an address, so an allowlist written in the seller's own spelling would never match. And in the dangerous direction, two distinct base58 addresses can fold to the same lowercase string, letting in one nobody listed.
  6. validUntil is read from extensions["offer-receipt/1"].info.validUntil, in Unix seconds. Absent means no declared expiry. Unreadable means absent, never zero: "the seller said something we could not read" must not become "this offer expired in 1970".
  7. validUntil === now still stands — it is the last instant the offer is up. And an accepts with one unreadable entry keeps the readable ones and counts the others by scheme name, so a refusal says what the seller offered: offered: ["batch-settlement","agent-pay"]. Discovering a service keeps working even when buying it automatically does not.

Two more worth knowing: a copy of a policy spends from the same purse (a client is copied per request, and a per-copy total would make a cumulative limit meaningless), and a corrupt purse reports the ceiling, never zero — for money the safe direction is to refuse.

The scheme decides, and so does the asset

Two checks sit beside the policy on the buyer path, because approving a payment you cannot honestly present is not an approval:

Schemes. KNOWN_SCHEMES is the vocabulary shared with the Rust facilitator's closed Scheme enum and the Python SDK: exact, upto, escrow, commerce, fhe-transfer. CLIENT_PAYABLE_SCHEMES is the subset this buyer path can sign — exact alone, because the payload builder stamps scheme: 'exact' into everything it produces. Recognising a scheme is not being able to pay it: a well-formed escrow offer read as payable would be signed as exact, offering the seller a payment under a scheme it never asked for. Either way the entry is excluded and counted by its scheme name, which is what lets a refusal say where to go look.

A missing scheme is unreadable, not exact. Rust requires the field, and a buyer that guessed would sign under a scheme the seller never named. This is deliberately asymmetric with the seller side of this SDK, where a missing scheme reads as exact: a seller is lenient about what it accepts, a buyer is strict about what it signs.

Assets. The policy judges the offer's own asset, but the signature is built for whatever tokenType resolves to on that chain, at that token's decimals. With USDC on both sides they coincide. They do not have to — so an offer naming a different token is refused rather than silently re-pointed, because which token to pay with is your decision and guessing it from the seller's 402 is how a wallet signs for a token nobody chose. Pass the tokenType that matches the offer.

Deciding without a network stack

decideOnChallenge(policy, challenge, offer, { now }) runs all six steps and is callable directly, which is the point: a decision that can only be exercised by driving a real HTTP client is a decision nobody tests. It takes the challenge WHOLE — passing the offers alone is exactly what dropped a seller's validUntil on the floor in Rust for a full commit with every unit test green. now is passed rather than read, so a money decision can be pinned to an exact instant.

503 is not 402 — read the refusal before you re-sign

402 and 503 say opposite things, and the difference is the buyer's money:

| status | meaning | what the caller must do | |---|---|---| | 402 | the payment was rejected | sign a new authorization | | 503 | no verdict was reached | resend the same credential | | 502 upstream_rpc_unavailable | the node never answered; nothing was broadcast | resend the same credential | | 502 settlement_unconfirmed | broadcast, and it may be mined | send nothing — look up the hash |

Charging a 503 as a 402 makes the buyer sign and broadcast a second payment for money that was never refused — and the first authorization is still perfectly spendable. Every facilitator edge in this SDK therefore reports the refusal as data:

const result = await client.settle(payment, requirements);

if (!result.success) {
  if (result.retryable) {
    // Nothing was rejected. Do NOT ask for another signature.
    // result.status            -> 503
    // result.reason            -> 'holder_unknown' | 'forward_failed' | ...
    // result.retryAfterSeconds -> already clamped, never an hour
    // result.safeToReplay      -> true only if the facilitator proved nothing ran
  } else if (isSettlementUnconfirmed(result)) {
    // NOT a refusal: the transfer was broadcast and may already be mined.
    // Reconcile — never re-send.
    // result.transaction -> the hash, in that chain's own encoding
    // result.paymentId   -> the same id a successful settle would have printed
  } else {
    // A real refusal. result.errorReason says why.
  }
}

The same fields appear on verify(), verifyAndSettle(), every Erc8004Client write, and the gasless escrow calls. Erc8004LookupError carries them as getters.

Why reason matters: the five are not interchangeable

The facilitator serialises every EVM write through one process — they share a gas wallet whose nonce is allocated in memory. A task that does not hold that lease forwards the write; when it cannot, it answers 503 + Retry-After: 5 + a reason.

| reason | did the write run? | replay the same request? | |---|---|---| | holder_unknown | no | yes | | forwarding_disabled | no | yes | | forwarded_but_not_writer | no | yes | | body_unreadable | no | yes | | forward_failed | maybe | no |

forward_failed is emitted after the write was handed over: the holder may have executed it and the response been lost coming back. It is a timeout wearing a status code. The SDK replays the first four automatically and never that one — resolve it by reading state, with getIdentityByOwner (respecting its 404-vs-503 distinction) or getRegisterStatus. Re-POSTing an ambiguous mint is what once created five duplicate agents.

Automatic replay is bounded and configurable:

new FacilitatorClient({ retries: 0 });   // never replay; default is 2 extra attempts

Retry-After is honoured only up to MAX_RETRY_AFTER_SECONDS (15). A misconfigured facilitator answering Retry-After: 3600 would otherwise hang the request for an hour.

The two 502s mean opposite things — branch on the body, not the status

POST /settle answers 502 for two situations that call for opposite moves:

| body error | Retry-After | did the money move? | retry? | |---|---|---|---| | upstream_rpc_unavailable | 30 | no, nothing was broadcast | yes | | settlement_unconfirmed | absent | maybe — it may be mined | never |

settlement_unconfirmed is emitted when the transaction went out and no receipt ever came back. Retrying it re-signs a new authorization with a fresh nonce, which the chain accepts as a second, perfectly valid payment for the same purchase — the buyer pays twice, in exactly the case the facilitator emits this error to prevent. authorizationState does not stop it: the second authorization is genuinely new.

So the status alone cannot decide. This SDK reads the body:

import { isSettlementUnconfirmed, SETTLEMENT_UNCONFIRMED } from 'uvd-x402-sdk';

const result = await client.settle(payment, requirements);

if (!result.success && isSettlementUnconfirmed(result)) {
  // result.retryable  -> false. Do not resend, and do not ask for a signature.
  // result.transaction -> what to look up on chain
  // result.paymentId   -> matches the id a successful settle prints, so a
  //                       payment later found confirmed reconciles cleanly
  await reconcileOnChain(result.transaction);
}

result.transaction is not always 0x-prefixed — Algorand prints base32 and Solana base58. Pass it through verbatim; reformatting it makes it unpasteable in an explorer, and pasting it is the entire remedy on offer.

The same reading applies to Erc8004LookupError (POST /register goes through the same EVM path, so a mint can come back unconfirmed too) and to every gasless escrow call. An explicit retryable: false in a facilitator body always wins over the status — but only ever downgrades: a body claiming retryable: true on a 402 will not make this SDK resend a genuinely refused credential. The one exception is named, not read from a flag: 409 authorization_in_flight is retryable (see Portable facilitator receipts).

Three independent signals stop a retry, because the cost of missing one is a second payment: the explicit retryable: false, the named settlement_unconfirmed, and — the general rule — any 5xx body carrying a transaction hash at all, under any of transaction, transaction.hash, txHash, tx_hash or transaction_hash. A hash in a failure means the facilitator got as far as broadcasting, whatever it called the error, so that last rule holds for codes that do not exist yet. It is the Python SDK's anti-double-settle guard, adopted here.

Middleware

createPaymentMiddleware and createHonoMiddleware answer 503 with a Retry-After header — not 402, not 500 — whenever the facilitator reached no verdict, and keep answering 402 for genuine rejections.

An unconfirmed settlement is the one 5xx that goes out as 500, with no Retry-After: "stop" is the correct instruction when the transfer may already be mining. The body carries transaction, paymentId and retryable: false, so the buyer's client can reconcile instead of paying again.

An X-PAYMENT the facilitator already admitted for another request is answered 409 (authorization_already_settled, receipt_request_conflict: it was used, the handler does not run) or 503 + Retry-After while it is authorization_in_flight — never 402 and never 500. Both middlewares add PAYMENT-RESPONSE to an existing Access-Control-Expose-Headers and no-store to an existing Cache-Control instead of replacing them, and in 'manual' mode a settle() after the handler already answered no longer touches the sent response.

ERC-8004 Trustless Agents

Build verifiable on-chain reputation for AI agents and services. Supports 23 networks (21 EVM + 2 Solana).

Name Base as 'base'. The old 'base-mainnet' spelling is rejected by the facilitator (400 Invalid network); the SDK now rewrites it for you, but new code should use 'base'.

On EVM networks, agent IDs are sequential numbers. On Solana, agent IDs are base58 pubkey strings. The AgentId type (number | string) handles both.

EVM Networks (21)

ethereum, base, polygon, arbitrum, optimism, celo, bsc, monad, avalanche, scroll, skale-base, arc, ethereum-sepolia, base-sepolia, polygon-amoy, arbitrum-sepolia, optimism-sepolia, celo-sepolia, avalanche-fuji, skale-base-sepolia, arc-testnet

Arc (arc, arc-testnet) since 2.98.0, with the canonical registries on both; see ERC-8004 on Arc.

Solana Networks (2)

solana, solana-devnet

Usage

import { Erc8004Client, AgentId } from 'uvd-x402-sdk/backend';

const erc8004 = new Erc8004Client();

// EVM: agent ID is a number
const identity = await erc8004.getIdentity('ethereum', 42);
console.log(identity.agentUri);

// Solana: agent ID is a base58 pubkey string
const solIdentity = await erc8004.getIdentity('solana', '8oo4dC4JvBLwy5...');
console.log(solIdentity.agentUri);

// Look up agent by wallet owner address
const byOwner = await erc8004.getIdentityByOwner('base-mainnet', '0xOwnerAddress...');
console.log(byOwner.agentId, byOwner.identity.agentUri);

// Get agent reputation
const reputation = await erc8004.getReputation('ethereum', 42);
console.log(`Score: ${reputation.summary.summaryValue}`);

// Submit feedback after payment
const result = await erc8004.submitFeedback({
  x402Version: 1,
  network: 'ethereum',
  feedback: {
    agentId: 42,
    value: 95,
    valueDecimals: 0,
    tag1: 'quality',
    proof: settleResponse.proofOfPayment,
  },
});

// Respond to feedback (agents only)
// sealHash is required for Solana, optional for EVM
await erc8004.appendResponse('ethereum', 42, 1, 'Thank you for your feedback!');

Ratings the chain attributes to the rater

submitFeedback() above works, but the registry records msg.sender as the author -- and on that route msg.sender is the facilitator. It is why 87,2% of the reputation on Base (1.384 of 1.587 feedbacks) is attributed to one wallet, which can also revoke it.

EIP-7702 fixes it without touching the registry: the rater delegates their own EOA to a FeedbackDelegate, and the transaction is sent to the rater's address, so the registry sees the rater while the facilitator still pays the gas.

import { Erc8004Client, supportsRelayedFeedback } from 'uvd-x402-sdk/backend';

const erc8004 = new Erc8004Client();

if (!supportsRelayedFeedback('base')) {
  // fall back to submitFeedback(); the facilitator is the author there
}

const prep = await erc8004.prepareRelayedFeedback({
  x402Version: 1,
  network: 'base',
  feedback: {
    agentId: 18896,
    value: 95,
    tag1: 'quality',
    rater: raterAddress, // who the chain will record as the author
  },
});

// 1. Sign with the RATER's key. WHICH value you sign depends on HOW you sign
//    it — get this wrong and you produce a well-formed signature that
//    authorises nobody, and the only symptom is `relay_bad_signature`.
//
//    `prep.digest` already carries the EIP-191 envelope. A raw key signs it as
//    a prehash; a wallet's personal_sign would add the envelope a SECOND time,
//    so wallets sign `prep.signingPayload` instead.
const signature = await account.sign({ hash: prep.digest! });   // raw key
// ...or, from a browser wallet:
//   const signature = await walletClient.signMessage({
//     account, message: { raw: prep.signingPayload! },
//   });

// 2. Only the first time this rater rates: point their EOA at the delegate.
const authorization = prep.delegated
  ? undefined
  : {
      chainId: prep.chainId, // 0 is EIP-7702's wildcard: valid on every chain
      address: prep.delegate!,
      nonce: prep.accountNonce!,
      ...(await signAuthorization(prep.chainId, prep.delegate!, prep.accountNonce!)),
    };

const result = await erc8004.submitRelayedFeedback({
  x402Version: 1,
  network: 'base',
  feedback: { agentId: 18896, value: 95, tag1: 'quality', rater: raterAddress },
  deadline: prep.deadline!, // short by design; past it, refused
  nonce: prep.nonce!,
  signature,
  authorization,
});

Pass the same feedback parameters, deadline and nonce back to submitRelayedFeedback(). They are not redundant: the facilitator rebuilds the registry calldata from them and refuses to relay anything the rater's signature does not cover.

Available on the ten networks in RELAYED_FEEDBACK_NETWORKS -- the nine mainnets with a deployed FeedbackDelegate (base, ethereum, polygon, arbitrum, optimism, celo, bsc, monad, arc) plus base-sepolia. Avalanche is not one of them and is not waiting to become one: its C-Chain rejects the transaction type itself (-32000 transaction type not supported), so anchor the rating on a chain that supports EIP-7702 -- the payment stays where it was made. arc-testnet serves ERC-8004 reads but not this rail: no delegate is deployed there, and prepare answers 400.

Requires facilitator v1.93.0+ for the mainnets (2.38.0+ for arc); base-sepolia since v1.74.0.

The same thing on Solana, without a delegate

Solana reaches the same place by a shorter road, so it is a different pair of calls and a different network list. The program's give_feedback instruction already declares account 0 as [signer, writable] client (feedback author), and a Solana transaction carries several signatures natively: the rater signs as client, the facilitator co-signs as fee payer. Nothing is delegated because nothing has to be.

solana never goes in RELAYED_FEEDBACK_NETWORKS. That list drives /feedback/evm/* and names chains with a deployed FeedbackDelegate; a Solana rating sent there is a 400, and the entry would assert a delegate that was never deployed and is not missing. Use SOLANA_FEEDBACK_NETWORKS / supportsSolanaFeedback().

import { Erc8004Client, supportsSolanaFeedback } from 'uvd-x402-sdk/backend';
import { Transaction } from '@solana/web3.js';

const erc8004 = new Erc8004Client();

if (!supportsSolanaFeedback('solana')) {
  // fall back to submitFeedback(); the facilitator is the author there
}

const prep = await erc8004.prepareSolanaFeedback({
  x402Version: 1,
  network: 'solana',
  feedback: {
    agentId: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgHkv', // the agent asset
    rater: raterPubkey,   // base58: who the chain will record as the author
    value: 87,
    valueDecimals: 0,
    score: 95,            // WITHOUT THIS the rating counts for nothing
    tag1: 'quality',
  },
});

// Sign the message as it came. Re-encoding it changes bytes the facilitator
// will compare, and it refuses to co-sign anything it did not build. It is a
// LEGACY transaction: `Transaction.from()`, not `VersionedTransaction`.
const tx = Transaction.from(Buffer.from(prep.transaction!, 'base64'));
tx.partialSign(raterKeypair);   // ...or a wallet: await wallet.signTransaction(tx)

const result = await erc8004.submitSolanaFeedback({
  x402Version: 1,
  network: 'solana',
  feedback: { /* exactly what went to prepare, rater included */ },
  transaction: tx.serialize({ requireAllSignatures: false }).toString('base64'),
});

prep.feePayer is the facilitator and prep.rater is the rater — that split is the whole point. Submit before prep.lastValidBlockHeight: past it the network drops the transaction, nothing is written and nothing is charged, and a resend needs a fresh prepare() because the blockhash the rater signed over is gone.

Set score. It is optional on the wire and the ATOM Engine ignores an unscored feedback: the transaction succeeds, the record lands on the agent, and reputation stays at zero (had_impact=false) — and it is not retroactive.

Available on solana and solana-devnet. Requires facilitator v2.16.0+.

/accepts Negotiation

Discover what the facilitator can settle before constructing payment authorizations. Used by Faremeter middleware and clients.

import { FacilitatorClient } from 'uvd-x402-sdk/backend';

const client = new FacilitatorClient();

// Ask facilitator what it can settle
const enriched = await client.accepts([
  {
    scheme: 'exact',
    network: 'base-mainnet',
    maxAmountRequired: '1000000',
    resource: 'https://api.example.com/data',
    payTo: '0xMerchant...',
  },
]);
// enriched[0].extra now has feePayer, tokens, escrow config

Escrow & Refunds

Hold payments in escrow with dispute resolution.

import { EscrowClient } from 'uvd-x402-sdk/backend';

const escrow = new EscrowClient();

// Create escrow payment
const escrowPayment = await escrow.createEscrow({
  paymentHeader: req.headers['x-payment'],
  requirements: paymentRequirements,
  escrowDuration: 86400, // 24 hours
});

// Release after service delivery
await escrow.release(escrowPayment.id);

// Or request refund if service failed
await escrow.requestRefund({
  escrowId: escrowPayment.id,
  reason: 'Service not delivered',
});

// Query on-chain escrow state
const state = await escrow.getEscrowState({
  network: 'base-mainnet',
  payer: '0xPayer...',
  recipient: '0xRecipient...',
  nonce: '0x1234...',
});

Advanced Escrow (AdvancedEscrowClient)

Full escrow lifecycle management for EVM chains. Supports both ethers.Signer and SigningWalletAdapter (EnvKey, OWS) for signing.

Supported on 10 EVM networks: Base, Base Sepolia, Ethereum, Ethereum Sepolia, Polygon, Arbitrum, Optimism, Celo, Monad, Avalanche. Since 2.99.0 also on Arc and Arc Testnet, on the x402r canonical escrow: there release sends capture and refundInEscrow sends void, which only returns the whole capturable amount. See Escrow on Arc.

With Private Key (ethers.Signer)

import { AdvancedEscrowClient } from 'uvd-x402-sdk/backend';

const client = new AdvancedEscrowClient(process.env.PRIVATE_KEY!, {
  chainId: 8453, // Base
});
await client.init();

// Build payment info
const paymentInfo = client.buildPaymentInfo(
  '0xWorkerAddress...', // receiver
  '5000000',           // amount in atomic units ($5.00 USDC)
  'standard',          // tier: 'standard' | 'express' | 'premium'
);

// Authorize: lock funds in escrow
const auth = await client.authorize(paymentInfo);

// Release: capture escrowed funds to receiver
await client.release(paymentInfo);

// Or refund: return escrowed funds to payer
await client.refundInEscrow(paymentInfo);

// Query escrow state on-chain
const state = await client.queryEscrowState(paymentInfo);

With SigningWalletAdapter (OWS)

import { AdvancedEscrowClient } from 'uvd-x402-sdk/backend';
import { OWSWalletAdapter } from 'uvd-x402-sdk';

const wallet = new OWSWalletAdapter(owsWalletInstance);
const client = new AdvancedEscrowClient(null, {
  wallet,
  rpcUrl: 'https://mainnet.base.org',
  chainId: 8453,
});
await client.init();

const paymentInfo = client.buildPaymentInfo('0xWorker...', '5000000', 'standard');
const auth = await client.authorize(paymentInfo);

Gasless Operations via Facilitator

Release and refund can be executed through the facilitator (no gas required):

// Gasless release
await client.releaseViaFacilitator(paymentInfo);

// Gasless refund
await client.refundViaFacilitator(paymentInfo);

Recovering an EXPIRED escrow

A release attempted after authorizationExpiry reverts with AfterAuthorizationExpiry. It is widely believed — and this SDK's own comments said so until now — that the funds are then movable only by the payer's reclaim(). That is false, and it is why stuck escrows were written off.

From AuthCaptureEscrow.sol:

  • partialVoid is onlySender(paymentInfo.operator) — the operator is the facilitator, not the payer — it sends the tokens to the payer, and it never reads authorizationExpiry. It works before expiry and after it.
  • reclaim is onlySender(paymentInfo.payer) and gated on expiry. It is a payer's self-service escape hatch, which is why the facilitator does not expose it — not the only exit.

refundViaFacilitator sends action: "refundInEscrow", which reaches partialVoid. So a stuck escrow is recoverable with no gas, no payer, and no regard for the expiry:

const state = await client.queryEscrowState(paymentInfo);
if (state.capturableAmount !== '0') {
  const result = await client.refundViaFacilitator(paymentInfo, state.capturableAmount);
  if (!result.success && result.retryable) {
    // No verdict. The tokens are still in escrow — send it again.
  }
}

Widening the release window still matters: it is what pays the worker, and a refund does not.

Direct Charge (No Escrow)

// Instant payment without escrow hold
await client.charge(paymentInfo);

Commerce Scheme

The 'commerce' scheme is a semantic alias for 'escrow', introduced for marketplace integrations (Execution Market, arbiter workflows). It uses the same contracts, ABI, and ERC-3009 flow as 'escrow'.

import type { X402Scheme } from 'uvd-x402-sdk';

// All three schemes are valid
const scheme: X402Scheme = 'commerce'; // or 'exact' or 'escrow'

// PaymentRequirements and X402 headers accept all schemes
const header = {
  x402Version: 2,
  scheme: 'commerce',
  network: 'eip155:8453',
  payload: { /* ... */ },
};

// Default behavior unchanged: buildPaymentRequirements() defaults to 'exact'

The facilitator's /supported endpoint advertises both 'escrow' and 'commerce' entries for all 11 escrow-capable networks (14 entries each).

Bazaar Discovery

Register and discover paid x402 resources across the network.

The Bazaar is served by the facilitator itself under /discovery/*. No API key, no separate host.

import { BazaarClient, isAlive } from 'uvd-x402-sdk/backend';

const bazaar = new BazaarClient();

// List resources. Every filter is applied server-side over the whole catalog,
// so `pagination.total` is the real number of matches -- filtering one page
// locally is not the same thing and will under-report.
const page = await bazaar.listResources({
  network: 'eip155:8453',
  health: 'alive',   // only endpoints a probe actually reached
  tier: 'vip',       // first_party | vip | verified | listed
  limit: 20,
});

for (const r of page.items) {
  console.log(r.url, r.health?.status, `${r.health?.latencyMs}ms`, r.curation?.label);
}
console.log(`${page.items.length} of ${page.pagination.total}`);

// Free-text search. The parameter is `q`; anything else is rejected with a 400.
const hits = await bazaar.listResources({ q: 'logs' });

// Walk the whole filtered catalog, one page at a time
for await (const r of bazaar.iterateResources({ health: 'alive' })) {
  if (isAlive(r)) console.log(r.url);
}

// Register a resource. Registration is open and rate limited.
await bazaar.registerResource({
  url: 'https://api.example.com/v1/generate',
  description: 'Generate images with AI',
  accepts: [{
    scheme: 'exact',
    network: 'eip155:8453',
    asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    amount: '50000',
    payTo: '0x1234...',
    maxTimeoutSeconds: 60,
  }],
  metadata: { category: 'ai', tags: ['image'] },
});

// Aggregate catalog metrics
const stats = await bazaar.getStats();
console.log(stats.total, stats.visible, stats.byHealth.alive);

Timestamps (firstSeen, lastSeen, lastUpdated, health.lastChecked) are Unix epoch seconds. Use epochToDate() to get a Date.

x402 v2 requests (buildVerifyRequestV2 / buildSettleRequestV2)

If your 402 advertises CAIP-2 networks (eip155:8453), you are speaking v2 and must send the v2 envelope. buildVerifyRequest emits the v1 one and cannot express v2:

import { buildVerifyRequestV2 } from 'uvd-x402-sdk';

const body = buildVerifyRequestV2(
  payment.payload,
  { url: 'https://api.example.com/thing', description: 'Thing', mimeType: 'application/json' },
  { scheme: 'exact', network: 'eip155:8453', asset: '0x8335...', amount: '100000',
    payTo: '0xabc...', maxTimeoutSeconds: 300 }
);

| | v1 envelope | v2 envelope | |---|---|---| | top level | {x402Version, paymentPayload, paymentRequirements} | {x402Version, paymentPayload, resource, accepted} | | network | plain name — base | CAIP-2 — eip155:8453 | | amount field | maxAmountRequired | amount | | resource | URL string | object {url, description, mimeType} |

Do not mix levels. Each version demands its own network format and its own envelope. A v2 payload inside a v1 envelope — or a plain network name inside a v2 request — matches no variant at the facilitator and fails with data did not match any variant of untagged enum VerifyRequestEnvelope, an error that names no field. If you see it, check the envelope shape first, not the fields inside it.

The top-level x402Version names the ENVELOPE

VerifyRequest.x402Version is typed 1, not 1 | 2: it says which of the two shapes above the body has, an