Zunivo Docs

zunivo · x402 on arc

x402 payments on Arc

Charge USDC per API call and let an agent pay for it without a human in the loop. Standard x402, settled on Arc.

server middlewareagent clientEIP-3009 facilitatorArc mainnettestnet sandbox

What is this

x402 is a payment standard built on the HTTP 402 Payment Required status code. The server answers with 402 and a machine-readable price, the client attaches a payment and retries, the server returns the resource. It's governed by the Linux Foundation and used by Coinbase, Circle and others.

The public facilitators cover Base, Polygon, Arbitrum and Solana but not Arc. zunivo-x402-arc fills that gap: an x402 client can pay for an API on Arc, where gas is in USDC and blocks are sub-second.

How x402 works

Request. An agent calls a priced endpoint like any HTTP request.
402. The server replies 402 Payment Required with PaymentRequirements: price, token, recipient, network.
Pay. The agent pays in USDC on Arc — either by settling a Zunivo order through our verified router, or (advanced) by signing an EIP-3009 authorization.
Retry with proof. The agent resends the request with an X-PAYMENT header.
Serve. The server verifies settlement, returns the data, and adds an X-PAYMENT-RESPONSE receipt header.

Quickstart

# install
npm install zunivo-x402-arc

# run a paid API (needs a Zunivo API key from https://api.zunivo.io)
ZUNIVO_API=https://api.zunivo.io ZUNIVO_KEY=zk_… PAY_TO=you.agent npm run example:server

# in another terminal, an agent pays it — one command, one function
AGENT_PK=0x… npm run example:agent

Server — charge USDC per call

Three lines protect any Express route:

import express from "express";
import { paymentRequired } from "zunivo-x402-arc";

const app = express();
const pay = paymentRequired({
  price: "0.05",              // human USDC
  payTo: "you.agent",         // a .agent name or 0x address
  zunivoApi: "https://api.zunivo.io",
  zunivoKey: process.env.ZUNIVO_KEY,
});

app.get("/v1/data", pay, (req, res) => res.json({ premium: "…" }));

Unpaid requests receive a spec-compliant 402. Paid requests pass through with an X-PAYMENT-RESPONSE receipt header. Each payment is consumed exactly once.

The 402 a client sees

{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "arc",                 // also listed as CAIP-2 "eip155:5042"; sandbox: "arc-testnet" / "eip155:5042002"
    "maxAmountRequired": "50000",        // 0.05 USDC in 6-decimal base units
    "asset": "0x3600000000000000000000000000000000000000",
    "payTo": "you.agent",
    "maxTimeoutSeconds": 120,
    "extra": {
      "name": "USDC", "decimals": 6, "chainId": 5042,
      "zunivoOrderId": "02e2b580-…",     // order minted for this call — pay it, then retry with proof
      "payToAddress": "0x6963…90c8",     // payTo resolved to its 20-byte address — settle to THIS, not the name
      "payUrl": "https://app.zunivo.io/pay?oid=…"  // human-payable fallback for the same order
    }
  }]
}
writing your own client?Settle to extra.payToAddress (the resolved 20-byte address), never to payTo when it is a .agent name — the chain only accepts addresses. Our SDK client does this for you since 0.1.1.

Agent — pay automatically

Getting the price, paying on Arc and retrying with proof is one wrapped fetch:

import { createX402Fetch } from "zunivo-x402-arc/client";

const x402fetch = createX402Fetch({
  privateKey: process.env.AGENT_PK,
  maxPrice: "1",                  // refuse anything pricier (optional guard)
  onEvent: (e) => console.log(e.type),
});

const res = await x402fetch("https://api.example.dev/v1/data");
const data = await res.json();   // paid for, on-chain, no human

.agent service discovery

The holder of a .agent name can publish an agent card (endpoint, x402 manifest, description) through the ZunivoAgentRecords contract. Another agent can then go from the name to a paid call in two lines:

import { connectAgent } from "zunivo-x402-arc";

const svc = await connectAgent("data.agent", { privateKey: process.env.AGENT_PK });
const res = await svc.fetch("/v1/index/crypto");   // 402 handled — USDC paid, receipt on-chain

The recipient is pinned to the address the name resolves to on-chain, so a hostile server cannot redirect funds. To just read a card without paying, use discoverAgent(name). Publish your own card at app.zunivo.io/names; browse the directory at app.zunivo.io/agents.

records contractZunivoAgentRecords — verified at 0x4f40…306B. ENS-resolver-style text records, keyed by the name's tokenId, writable only by the current NFT holder. Records survive transfers; reads are free.

MCP — give your AI a wallet

zunivo-mcp is a Model Context Protocol server: it gives any MCP client (Claude Desktop, Claude Code, …) a budgeted USDC wallet on Arc. The AI can discover .agent services, pay for APIs over x402, and issue payment links — non-custodial, with a hard daily spend cap.

ToolDoes
zunivo_resolve_agentname → on-chain card (address, endpoint, records)
zunivo_list_agentsthe public directory of callable services
zunivo_paid_fetchcall a paid API — 402 handled, USDC paid within budget, receipt returned
zunivo_create_payment_linkinvoice anyone — returns a pay URL
zunivo_check_orderorder status + settling payments
zunivo_spend_statustoday's budget: cap, spent, remaining

Add it to Claude Desktop's claude_desktop_config.json:

{
  "mcpServers": {
    "zunivo": {
      "command": "npx",
      "args": ["-y", "zunivo-mcp"],
      "env": {
        "AGENT_PK": "0x…",                              // agent wallet — never shown to the model
        "ZUNIVO_RECORDS_ADDRESS": "0x4f40…306B",
        "ZUNIVO_DAILY_CAP": "5",                        // hard daily USDC budget
        "ZUNIVO_MAX_PER_CALL": "1",
        "ZUNIVO_SPEND_FILE": "~/.zunivo-spend.json"     // budget survives restarts — use it
      }
    }
  }
}
safetyThe private key stays in env, never in tool output. Every payment is bounded by a per-call ceiling under a hard daily cap, and the cap is a shared source of truth across restarts and instances (set ZUNIVO_SPEND_FILE). Content returned by paid endpoints is untrusted input to your model — keep the cap small.

Two settlement backends

The middleware's verify is pluggable. Two paths ship:

BackendHow it settlesStatus
Zunivo orders (default)Payment settles through our verified Arc router 0x4210…Ea55. X-PAYMENT payload is { zunivoOrderId }.works today
EIP-3009 facilitatorAgent signs a TransferWithAuthorization; the facilitator submits it to Arc's USDC. Fully standard, gasless.reference
why twoThe Zunivo-order backend needs no new on-chain code and is verified end-to-end today. The EIP-3009 path is the standard x402 scheme; it's provided as a reference so standard agents interoperate, pending on-chain confirmation of Arc USDC's 3009 selectors.

EIP-3009 (gasless, advanced)

Arc's USDC is issued by Circle and supports EIP-3009 (TransferWithAuthorization) — the same mechanism x402 uses elsewhere. The agent signs off-chain; a facilitator submits on-chain; no prior approval, no gas for the agent.

import { signPayment, createFacilitator } from "zunivo-x402-arc/facilitator";

// agent side — sign, no gas, no transaction
const payload = await signPayment({ privateKey, to: "you.agent", price: "0.05" });

// facilitator side — verify signature, then submit to Arc USDC
const fac = createFacilitator({ submitterKey });
const { settled, txHash } = await fac.settle(payload);
before productionConfirm Arc's USDC (0x3600…0000) exposes the transferWithAuthorization / receiveWithAuthorization selectors and its EIP-712 domain (name, version) on-chain. Until then, use the Zunivo-order backend as the tested path.

Decimals on Arc (read this)

criticalArc's native USDC gas token uses 18 decimals; the USDC ERC-20 interface uses 6 decimals. x402 amounts are ERC-20 amounts → always 6 decimals. Mixing them mis-scales a payment by 10¹². This library keeps the two paths strictly separate; if you compute amounts yourself, use toUsdcBaseUnits() / fromUsdcBaseUnits().

Going to mainnet (money-safety)

Since 1.0.0 the SDK defaults to Arc mainnet (network: "arc"); pass "arc-testnet" for the free sandbox. On mainnet you are moving real USDC, so the library refuses unsafe defaults and requires you to opt in to three guards explicitly. Zunivo is non-custodial — funds move wallet → on-chain router → recipient; the platform never holds them — but these guards protect against the mistakes and hostile inputs that can cost real money.

before real moneyTwo gates are not covered by this library and must be cleared first: an independent security audit of the SDK and the on-chain contracts, and on-chain confirmation of the EIP-3009 path on Arc mainnet. Until the 3009 path is confirmed, keep it disabled and settle via the Zunivo-order router backend.

1. Select the network explicitly

Nothing is hardcoded to one environment. Pass network on both the server and the agent ("arc" is the default, "arc-testnet" the sandbox; the CAIP-2 forms "eip155:5042" / "eip155:5042002" are accepted too). The agent rejects a 402 whose network doesn't match its own, so it can't be tricked onto the wrong chain — a mainnet agent will never pay a testnet quote or vice-versa.

// server
const pay = paymentRequired({ price: "0.05", payTo: "you.agent", network: "arc", consumedStore });

// agent
const x402fetch = createX402Fetch({ privateKey, network: "arc", expectRecipient });

2. Provide a durable replay store

The default replay guard is in-memory and single-process — fine for testnet, unsafe for a real deployment (a restart or a second instance would let one payment be consumed twice). On mainnet the middleware throws unless you pass a shared, durable consumedStore (Redis, a database, etc). Any object with an atomic reserve(key) → boolean (returns true only the first time) works.

const consumedStore = {
  async reserve(key) { // return true iff this key was never seen (atomic, e.g. Redis SET NX)
    return await redis.set(key, "1", "NX") === "OK";
  },
};

3. Pin the recipient the agent expects

The paid endpoint tells the agent which address to pay. A compromised or malicious server could name an attacker's address. If your agent knows who it intends to pay, pass expectRecipient — the client refuses to settle to any other address. Set requireExpectRecipient: true to make that pin mandatory.

const x402fetch = createX402Fetch({
  privateKey, network: "arc",
  expectRecipient: "0x…",          // refuse to pay anyone else
  requireExpectRecipient: true,   // and refuse to run without a pin
});
dedicated RPCThe public Arc gateway rate-limits under real concurrency. In production set ZUNIVO_MAINNET_RPC_URL to your own endpoint; the SDK puts it first.

Arc constants

FieldMainnet ("arc", default)Testnet sandbox ("arc-testnet")
Chain ID5042 · eip155:50425042002 · eip155:5042002
RPChttps://rpc.mainnet.arc.iohttps://rpc.testnet.arc.network
Explorerarc.etherscan.iohttps://testnet.arcscan.app
USDC (ERC-20, 6 dp)0x3600000000000000000000000000000000000000 (same on both)
Zunivo router0xAa8c293495446d04a51A32e2e4557EDE3BfC71190x4210D40a9899e42b4946B9dC7E0C35d3cf14Ea55
Zunivo names (.agent)0x824218447E8Dbf10E535dC7fB6ab7b68105c7dDa0x244e0c8bE1Ed59636901F98920413d414B158cc5
Agent records0xFE9fca63CaA64FBf0B2F089786A706f0C99dCbB10x4f405f0aA04FD6FaE0838DeE6FD184B1f3cC306B
Zunivo APIhttps://api.zunivo.iohttps://testnet-api.zunivo.io
Apphttps://app.zunivo.iohttps://testnet.zunivo.io
Fundingreal USDC — bridge in via CCTPfaucet.circle.com