Zunivo Docs

zunivo · the payment layer for ai agents on arc

x402 payments on Arc

Charge USDC per API call. Let AI agents pay for it autonomously — no card, no account, no human. Standard x402 on Arc's USDC-native chain.

server middlewareagent clientEIP-3009 facilitatorArc Testnet

What is this

x402 is the HTTP-native payment standard that revives the dormant 402 Payment Required status code. A server answers a request with 402 and a machine-readable price; a client attaches a signed payment and retries; the resource is served. It's governed by the Linux Foundation and backed by Coinbase, Circle, Visa, Stripe, and Google — tens of millions of agent payments already run on it.

But its facilitators cover Base, Polygon, Arbitrum, and Solana — not Arc. zunivo-x402-arc is that missing piece: it lets an x402-speaking agent pay for an API on Arc, using Arc's USDC-native gas and sub-second finality. Same standard the agent already knows; a new chain it can now pay on.

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-testnet",
    "maxAmountRequired": "50000",        // 0.05 USDC in 6-decimal base units
    "asset": "0x3600000000000000000000000000000000000000",
    "payTo": "you.agent",
    "maxTimeoutSeconds": 120,
    "extra": {
      "name": "USDC", "decimals": 6, "chainId": 5042002,
      "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

The whole dance — discover price, pay on Arc, retry 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

A .agent name is more than a payment address — its holder can publish an on-chain agent card (endpoint, x402 manifest, description) via the ZunivoAgentRecords contract. That turns a name into a discoverable, callable, payable service. An agent goes from a human-readable 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 purest 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)

On testnet the SDK defaults are convenient. 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. The SDK refuses to run on "arc" until the mainnet constants (chain id, RPC, USDC address, router) are filled in from Arc's mainnet docs — a half-configured mainnet can never silently move funds. The agent also rejects a 402 whose network doesn't match its own, so it can't be tricked onto the wrong chain.

// 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
});
also re-confirm on mainnetDon't assume mainnet equals testnet: verify Arc mainnet's chain id, RPC, USDC ERC-20 address and its decimals on-chain before filling the mainnet config. A wrong decimals assumption mis-scales every amount.

Arc Testnet constants

FieldValue
Chain ID5042002
RPChttps://rpc.testnet.arc.network
Explorerhttps://testnet.arcscan.app
USDC (ERC-20, 6 dp)0x3600000000000000000000000000000000000000
Zunivo router0x4210D40a9899e42b4946B9dC7E0C35d3cf14Ea55
Zunivo names (.agent)0x244e0c8bE1Ed59636901F98920413d414B158cc5
x402 network idarc-testnet
Faucetfaucet.circle.com