@arbitova/sdk
Non-custodial USDC escrow for agent-to-agent payments on Base.
Two agents lock USDC into a contract, one delivers, the other confirms or disputes, and a neutral arbiter resolves. Arbitova never holds the money — the contract does.
No API keys. No registration. No custody. Your Ethereum address is your identity.
npm install @arbitova/sdk ethers
Contract: 0xA8a031bcaD2f840b451c19db8e43CEAF86a088fC on Base Sepolia (mainnet launching after audit)
Spec: A2A-ESCROW-RFC-v0.1
Live UI: arbitova.com/pay
Why this exists
Every A2A / agent-commerce spec in the wild — MCP, Google's A2A, ERC-7683, Coinbase's Agent Commerce — defines how agents talk. None of them define how money moves when the agents don't trust each other.
Arbitova is the missing settlement primitive:
- Deterministic state machine.
createEscrow → markDelivered → {confirmDelivery | dispute → resolve | cancel}. No hidden branches, no admin override.
- No auto-release after timeout. Review windows expire into
DISPUTED, not into seller payout. Silence is safer than a wrong confirmation.
- Content-hash pinned on-chain. Sellers can't swap the delivery file after the buyer inspects.
- Verdict transparency. Every arbiter decision is a signed JSON blob; its
keccak256 is stored on-chain. Anyone can audit.
This is not a marketplace. There is no Arbitova account, no listing fee, no Arbitova Pro tier. The protocol is the whole product.
30-second quickstart
Two terminals. Both need a Base Sepolia private key with some test USDC + a pinch of ETH for gas.
Faucet: https://faucet.circle.com/ (Base Sepolia USDC) · gas: https://www.alchemy.com/faucets/base-sepolia
Buyer
import { Arbitova } from '@arbitova/sdk';
const buyer = await Arbitova.fromPrivateKey({ privateKey: process.env.BUYER_PK });
const { escrowId, txHash } = await buyer.createEscrow({
seller: process.env.SELLER_ADDRESS,
amount: '5.00',
deliveryHours: 24,
reviewHours: 24,
verificationURI: 'https://example.com/spec.json',
});
console.log(`Escrow #${escrowId} locked — ${buyer.explorerTx(txHash)}`);
Seller
import { Arbitova } from '@arbitova/sdk';
const seller = await Arbitova.fromPrivateKey({ privateKey: process.env.SELLER_PK });
seller.onEscrowCreated(async (ev) => {
if (ev.seller.toLowerCase() !== (await seller.address()).toLowerCase()) return;
console.log(`New escrow #${ev.id}: ${ev.amount} USDC from ${ev.buyer}`);
const deliveryContent = Buffer.from('hello, buyer');
await seller.markDelivered({
escrowId: ev.id,
deliveryPayloadURI: 'https://example.com/receipt.json',
deliveryContentBytes: deliveryContent,
});
});
Buyer confirms (happy path)
await buyer.confirmDelivery(escrowId);
Buyer disputes (sad path)
await buyer.dispute(escrowId, 'Delivery incomplete — criterion #2 failed');
That's the whole thing.
Lifecycle
┌──────────────────┐
│ CREATED │ buyer locked USDC
└────────┬─────────┘
│
▼ seller.markDelivered()
┌──────────────────┐
│ DELIVERED │ deliveryHash on-chain
└────────┬─────────┘
│
buyer.confirmDelivery()│ │ buyer.dispute()
│ │ or seller.dispute()
▼ ▼
┌─────────────┐ ┌──────────┐
│ RELEASED │ │ DISPUTED │ waiting for arbiter
└─────────────┘ └────┬─────┘
│ arbiter.resolve(bps split + verdictHash)
▼
┌──────────┐
│ RESOLVED │
└──────────┘
Two terminal states not drawn: CANCELLED (buyer calls cancelIfNotDelivered after delivery window) and auto-escalation into DISPUTED if the review window expires without confirmation.
Browser wallet
import { Arbitova } from '@arbitova/sdk';
const client = await Arbitova.fromWallet(window.ethereum);
await client.confirmDelivery('17');
Works with any EIP-1193 wallet (MetaMask, Coinbase Wallet, Rainbow, Rabby, etc.).
Framework integrations
Three reference A2A demos, all end-to-end on Base Sepolia with a live AI arbiter:
Python agents: pip install arbitova[path_b], then from arbitova import path_b. Same contract surface, same verdicts.
MCP server: npm install @arbitova/mcp-server — six on-chain tools (same contract surface) for any MCP client.
Networks
base-sepolia (default) | Production contract, real Circle USDC |
base-sepolia-test | Test contract with mock USDC for CI |
const client = await Arbitova.fromPrivateKey({
privateKey: process.env.PK,
network: 'base-sepolia-test',
});
Base mainnet launches after external audit + multisig arbiter migration. Watch Dev Log for the announcement.
Fees
confirmDelivery / escalateIfExpired | 0.5% | deducted from seller payout |
| Arbiter resolves a dispute | 2% | split per arbiter verdict |
Fees accrue in the contract. The protocol runs on them; there is no subscription.
API
All methods async. Write methods require a signer (private key or browser wallet).
Arbitova.fromPrivateKey({ privateKey, network? }) | — | Client signed by private key |
Arbitova.fromWallet(window.ethereum) | — | Client using an EIP-1193 wallet |
Arbitova.fromReadOnly({ network? }) | — | Read-only (no signing) |
createEscrow({ seller, amount, deliveryHours, reviewHours, verificationURI }) | buyer | Lock USDC (auto-approves). Returns { escrowId, txHash, receipt } |
markDelivered({ escrowId, deliveryPayloadURI, deliveryContentBytes? }) | seller | Commits keccak256(content) (or URI) on-chain |
confirmDelivery(escrowId) | buyer | Happy path — pays seller (minus 0.5%) |
dispute(escrowId, reason) | buyer or seller | Opens dispute; reason emitted in event |
cancelIfNotDelivered(escrowId) | buyer | After delivery deadline if seller no-showed |
escalateIfExpired(escrowId) | anyone | After review deadline — forces DISPUTED, not auto-release |
getEscrow(escrowId) | — | Full on-chain state |
getUsdcBalance(addr?) | — | USDC balance |
listEscrowsForAddress(role, addr?) | — | Scan escrows where address is buyer or seller |
onEscrowCreated / onDelivered / onReleased / onDisputed / onResolved / onCancelled | — | Event subscriptions |
explorerTx(hash) / explorerAddr(addr) | — | Basescan URL helpers |
Arbitova.keccakURI(uri) | static | Compute deliveryHash for an off-chain URI |
subscribeEvents({ address, onEvent }) | — | Server-sent events push stream (alternative to polling) |
Verification specs
Your verificationURI should link to a JSON document defining what "done" means. Two modes:
manual — human-readable prose for buyer, seller, and arbiter
programmatic — an HTTPS endpoint an arbiter can hit to get a pass/fail
Schema: arbitova.com/schemas/ · Reference: RFC v0.1
Security
Migrating from v2 (custodial)
v3 is a rewrite. If you have working v2 code, pin to @arbitova/sdk@^2.3.1 until you're ready — v3 doesn't share any API surface with v2. Migration guide: MIGRATION.md.
License
MIT · github.com/jiayuanliang0716-max/Arbitova