Documentation

arc-agent-pay — Python SDK for agents that discover and pay for APIs, enforce durable spending policy, carry on-chain identity, and settle validation-gated jobs on Arc.

Install

arc-agent-pay is a general-purpose toolkit for agent payments — it isn't limited to the research demo shown in the playground. The SDK is open source (MIT) at github.com/hamedkharazmi/arc-agent-pay and installable from PyPI:

bash
pip install arc-agent-pay          # minimal core
pip install "arc-agent-pay[all]"   # every feature

These examples require SDK 0.4.0 or newer. The core SDK has minimal dependencies; heavy features are opt-in extras:

ExtraAddsUse when
[agent]langgraph, langchain-openai, openaiRunning the LangGraph paying research agent
[rag]chromadb, fastembedEmbedding-based service discovery
[onchain]web3ERC-8004 identity/reputation and ERC-8183 job contracts
[observability]langfuseTracing + the offline eval harness
[mcp]mcpMCP server for Claude Desktop, Cursor, etc.
[llm]openaiLLM synthesis without the full agent stack
[all]all of the aboveEverything in one install

Quick start

Paying an x402-gated API takes a funded Arc Testnet wallet and a budget — the client handles the 402 → sign → retry loop for you:

python
from arc_agent_pay import PaymentClient, ServiceRegistry
from arc_agent_pay.models import Chain
from eth_account import Account

registry = ServiceRegistry()
services = registry.search("crypto prices")

account = Account.from_key("0x" + private_key)

async with PaymentClient(
    account=account,
    budget_usdc="0.05",
    chain=Chain.ARC_TESTNET,
) as client:
    response = await client.get(services[0].url)  # 402 → pay → retry
    data = response.json()
    print(client.summary())

How it works

Every API call goes through the x402 payment flow automatically:

text
1. Agent calls  GET /prices
2. Server returns  402 Payment Required  + PAYMENT-REQUIRED header
3. PaymentClient parses the header — price: 0.001 USDC, pay_to: 0x…
4. Signs EIP-3009 TransferWithAuthorization  (off-chain, no gas yet)
5. Retries with  X-402-Payment  header
6. Server verifies signature → calls transferWithAuthorization on-chain
7. Arc Testnet confirms → tx hash returned in PAYMENT-RESPONSE header
8. 200 OK — agent gets the data

Chain:    Arc Testnet · chain ID 5042002
USDC:     0x3600000000000000000000000000000000000000
Protocol: EIP-3009 v2 (transferWithAuthorization)

Spending controls

BudgetGuard limits one client session. SDK 0.4.0's PaymentPolicy checks the complete x402 quote before signing: maximum price, rolling spend and velocity, provider host, network, asset, recipient, and an emergency kill switch. Pair it with the SQLite journal when policy and payment history must survive restarts or coordinate multiple processes:

python
from arc_agent_pay import PaymentClient, PaymentPolicy, SqlitePaymentStore

policy = PaymentPolicy(
    max_payment_usdc="0.05",             # maximum single quote
    daily_cap_usdc="2.00",               # prospective rolling 24-hour cap
    max_payments_per_hour=30,            # velocity brake
    provider_daily_cap_usdc="0.50",      # per provider host
    allowed_hosts={"api.example.com"},
    allowed_networks={"eip155:5042002"},
)

async with PaymentClient(
    account=account,
    budget_usdc="0.25",                  # hard cap for this session
    policy=policy,
    payment_store=SqlitePaymentStore("./agent-payments.db"),
) as client:
    response = await client.get(
        "https://api.example.com/report",
        payment_id="order_research_report_0001",
    )

Reservations and rolling-cap checks happen atomically. Pending, authorized, successful, and ambiguous payments continue to count; only a conclusive failure releases the reservation. Durable policy fails closed by default. A payment ID can safely resume the same quoted request only when the seller declares standard x402 payment-identifier support.

Validation-gated workflows

SDK 0.4.0 supports jobs whose payment resolves only after delivery and evaluation. Partner-neutral WorkOrder, DeliveryEvidence, and signed verdict models bind the parties, budget, task, delivered content, and decision. The Erc8183Client maps those commitments to the six-state ERC-8183 draft reference profile. Install the [onchain] extra for Web3 support:

python
from arc_agent_pay import (
    Erc8183Client,
    deliverable_commitment,
    verdict_commitment,
)

# Each transition is sent by the account assigned to that role.
client_jobs = Erc8183Client(contract_address, account=client, rpc=rpc_url)
provider_jobs = Erc8183Client(contract_address, account=provider, rpc=rpc_url)
evaluator_jobs = Erc8183Client(contract_address, account=evaluator, rpc=rpc_url)
created = client_jobs.create_job(
    provider=provider.address,
    evaluator=evaluator.address,
    expired_at=expired_at,
    description="ipfs://job-brief",
)

provider_jobs.set_budget(created.job_id, 100_000)
client_jobs.approve_payment(100_000)
client_jobs.fund(created.job_id, expected_budget=100_000)
provider_jobs.submit(created.job_id, deliverable_commitment(delivery))
evaluator_jobs.complete(created.job_id, verdict_commitment(verdict))

Draft-profile limitation

The ERC-8183 prose and published reference contract currently disagree on several ABI and authorization details. The SDK pins the exact reference revision and does not claim that its separate ValidationEscrow contract is ERC-8183 compliant. The hosted playground demonstrates x402 pay-per-call settlement, not this escrow flow.

Read the pinned compatibility profile

SDK API reference

PaymentClient

Async httpx wrapper that intercepts HTTP 402 responses, applies quote-aware policy, journals the payment lifecycle, signs EIP-3009 authorizations, and retries automatically.

python
PaymentClient(
    account: eth_account.Account,
    budget_usdc: str = "1.00",
    chain: Chain = Chain.ARC_TESTNET,
    policy: PaymentPolicy | None = None,
    payment_store: PaymentStore | None = None,
    on_event: Callable[[str, dict], None] | None = None,
)
get(url, payment_id=None, **kwargs)GET with auto-payment and optional safe resume ID
request(method, url, payment_id=None, **kwargs)Any HTTP method with auto-payment
summary()Returns payment audit dict

PaymentPolicy

Fail-closed controls evaluated against the selected x402 quote before any payment is signed. Rolling limits are prospective and reserved atomically by the payment store.

python
PaymentPolicy(
    max_payment_usdc: str | None = None,
    daily_cap_usdc: str | None = None,
    max_payments_per_hour: int | None = None,
    provider_daily_cap_usdc: str | None = None,
    allowed_hosts: set[str] | None = None,
    blocked_hosts: set[str] | None = None,
    allowed_networks: set[str] | None = None,
    allowed_assets: set[str] | None = None,
    allowed_pay_to: set[str] | None = None,
    payments_disabled: bool = False,
)
check_static(payment)Validate quote-local controls
has_rolling_limitsWhether the journal must calculate rolling totals

SqlitePaymentStore

Stdlib-only, cross-process SQLite lifecycle journal and serialization point for rolling policy reservations.

python
SqlitePaymentStore(path: str)
get(payment_id)Read one durable Payment record
list(limit=100)Newest durable payment records
reserve / updateAtomic lifecycle operations used by PaymentClient

ServiceRegistry

In-memory registry of x402-gated API services. Search by keyword or tag.

python
ServiceRegistry()
search(query, max_results=5)Keyword/tag search → list[Service]
register(service)Add a Service to the registry
all()Return all registered services

BudgetGuard

Tracks cumulative spend for a session and raises BudgetExhaustedError when the limit is reached.

python
BudgetGuard(budget_usdc: str)
check_and_record(amount_usdc)Raises if over budget, else records
remainingDecimal — remaining USDC
spentDecimal — total spent this session

SpendCaps

Cross-run policy for rolling daily spend, payment velocity, and per-counterparty daily spend. Pair with an in-memory or durable SQLite ledger.

python
SpendCaps(
    daily_cap_usdc: str | None = None,
    max_payments_per_hour: int | None = None,
    provider_daily_cap_usdc: str | None = None,
)
check(ledger, counterparty)Refusal reason, or None to allow
activeWhether any rolling cap is configured

Erc8183Client

Web3 client for the pinned ERC-8183 draft reference profile. Reads jobs and sends role-authorized lifecycle transactions.

python
Erc8183Client(
    address: str,
    *,
    account: eth_account.Account | None = None,
    rpc: str | None = None,
)
create_job(...)Create an Open job and return job id + tx hash
set_budget(job_id, amount)Provider sets the reference-profile budget
approve_payment(amount)Approve the job contract to pull payment tokens
fund(job_id, expected_budget=...)Client checks and funds the budget
submit(job_id, deliverable)Provider commits the delivered work
complete / reject / claim_refundResolve or expire the escrowed job

Wallets & funding

The hosted x402 demo uses two EOA wallets, both funded with Arc Testnet USDC:

RoleEnv varPurpose
Agent (payer)AGENT_PRIVATE_KEYSigns EIP-3009 authorizations off-chain
Server (facilitator)SERVER_PRIVATE_KEYPays gas, submits transferWithAuthorization on-chain

Generate a fresh EOA:

bash
uv run python -c "
from eth_account import Account
import secrets
a = Account.from_key('0x'+secrets.token_hex(32))
print('key:', a.key.hex())
print('addr:', a.address)
"

Fund from a Circle wallet:

bash
circle wallet transfer <ADDRESS> --amount 5 --address 0x40a2f3926fb79b91b8012c8f1dc3a1c6e4ded2cc --chain ARC-TESTNET --testnet

Live API endpoints

Publicly deployed at api.agentpay.bond

EndpointPriceData source
GET /prices$0.001 USDCSynthetic crypto prices
POST /research$0.005 USDCSynthetic research brief
GET /news$0.002 USDCSynthetic headlines
GET /whales$0.010 USDCReal — Arc Testnet Blockscout