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.
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:
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:
| Extra | Adds | Use when |
|---|---|---|
| [agent] | langgraph, langchain-openai, openai | Running the LangGraph paying research agent |
| [rag] | chromadb, fastembed | Embedding-based service discovery |
| [onchain] | web3 | ERC-8004 identity/reputation and ERC-8183 job contracts |
| [observability] | langfuse | Tracing + the offline eval harness |
| [mcp] | mcp | MCP server for Claude Desktop, Cursor, etc. |
| [llm] | openai | LLM synthesis without the full agent stack |
| [all] | all of the above | Everything in one install |
Paying an x402-gated API takes a funded Arc Testnet wallet and a budget — the client handles the 402 → sign → retry loop for you:
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())Every API call goes through the x402 payment flow automatically:
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)
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:
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.
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:
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 profileAsync httpx wrapper that intercepts HTTP 402 responses, applies quote-aware policy, journals the payment lifecycle, signs EIP-3009 authorizations, and retries automatically.
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 |
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.
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_limits | Whether the journal must calculate rolling totals |
Stdlib-only, cross-process SQLite lifecycle journal and serialization point for rolling policy reservations.
SqlitePaymentStore(path: str)
| get(payment_id) | Read one durable Payment record |
| list(limit=100) | Newest durable payment records |
| reserve / update | Atomic lifecycle operations used by PaymentClient |
In-memory registry of x402-gated API services. Search by keyword or tag.
ServiceRegistry()
| search(query, max_results=5) | Keyword/tag search → list[Service] |
| register(service) | Add a Service to the registry |
| all() | Return all registered services |
Tracks cumulative spend for a session and raises BudgetExhaustedError when the limit is reached.
BudgetGuard(budget_usdc: str)
| check_and_record(amount_usdc) | Raises if over budget, else records |
| remaining | Decimal — remaining USDC |
| spent | Decimal — total spent this session |
Cross-run policy for rolling daily spend, payment velocity, and per-counterparty daily spend. Pair with an in-memory or durable SQLite ledger.
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 |
| active | Whether any rolling cap is configured |
Web3 client for the pinned ERC-8183 draft reference profile. Reads jobs and sends role-authorized lifecycle transactions.
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_refund | Resolve or expire the escrowed job |
The hosted x402 demo uses two EOA wallets, both funded with Arc Testnet USDC:
| Role | Env var | Purpose |
|---|---|---|
| Agent (payer) | AGENT_PRIVATE_KEY | Signs EIP-3009 authorizations off-chain |
| Server (facilitator) | SERVER_PRIVATE_KEY | Pays gas, submits transferWithAuthorization on-chain |
Generate a fresh EOA:
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:
circle wallet transfer <ADDRESS> --amount 5 --address 0x40a2f3926fb79b91b8012c8f1dc3a1c6e4ded2cc --chain ARC-TESTNET --testnet
Publicly deployed at api.agentpay.bond
| Endpoint | Price | Data source |
|---|---|---|
| GET /prices | $0.001 USDC | Synthetic crypto prices |
| POST /research | $0.005 USDC | Synthetic research brief |
| GET /news | $0.002 USDC | Synthetic headlines |
| GET /whales | $0.010 USDC | Real — Arc Testnet Blockscout |