trust-escrow
v0.1.0
Published
Escrow primitive for the Machine Economy — CLI and library for AI agent escrow on Base
Maintainers
Readme
trust-escrow
x402 for payments. Trust for escrow.
Escrow primitive for AI agent transactions on Base. AI agents lock USDC in escrow, an evaluator agent verifies delivery quality, funds are released or split based on the outcome. 0.3% protocol fee.
What it does
- Agent A (buyer) locks USDC in escrow before work starts
- Agent B (seller) delivers the work and marks it complete
- Agent A approves and releases funds — or opens a dispute
- Agent C (arbiter/evaluator) resolves disputes by splitting funds (0–100%)
- Automatic refund if seller doesn't deliver by deadline
- Automatic release if buyer doesn't act after delivery
Install
# Library (import in your agent code)
npm install trust-escrow
# CLI (use from terminal or agent shell)
npx trust-escrow --helpQuick Start — CLI
# Create escrow: Agent A locks 10 USDC for Agent B, Agent C arbitrates
npx trust-escrow create \
--buyer 0xAGENT_A \
--seller 0xAGENT_B \
--arbiter 0xAGENT_C \
--amount 10 \
--delivery-time-limit 7d \
--payment-time-limit 14d \
--arbiter-time-limit 30d \
--private-key $BUYER_PRIVATE_KEY
# Fund escrow (auto-approves USDC allowance)
npx trust-escrow fund 12 --private-key $BUYER_PRIVATE_KEY
# Seller marks delivery complete
npx trust-escrow deliver 12 --private-key $SELLER_PRIVATE_KEY
# Buyer releases funds (happy path)
npx trust-escrow release 12 --private-key $BUYER_PRIVATE_KEY
# OR: Buyer opens dispute (quality not satisfactory)
npx trust-escrow dispute 12 --private-key $BUYER_PRIVATE_KEY
# Arbiter resolves: 70% to seller, 30% to buyer
npx trust-escrow resolve 12 --split 70 --private-key $ARBITER_PRIVATE_KEY
# Check status (no private key needed)
npx trust-escrow status 12Quick Start — Library
import { createPublicClient, createWalletClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import {
createEscrow,
fundEscrow,
markDelivered,
releaseFunds,
openDispute,
resolveDispute,
getEscrow,
parseUSDC,
formatUSDC,
type TrustConfig,
} from 'trust-escrow'
// Build config for an agent wallet
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const config: TrustConfig = {
contractAddress: '0x2aCEa5D1F1e562F7D4040e0003A4bf46030f2ac0',
tokenAddress: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // USDC on Base Sepolia
chain: baseSepolia,
publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
walletClient: createWalletClient({ account, chain: baseSepolia, transport: http() }),
}
// Create escrow: 10 USDC with default time limits
const txHash = await createEscrow(config, {
buyer: '0xAGENT_A',
seller: '0xAGENT_B',
arbiter: '0xAGENT_C',
amount: parseUSDC('10'),
deliveryTimeLimit: 0n, // 0 = contract default (30 days)
paymentTimeLimit: 0n, // 0 = contract default (14 days)
arbiterTimeLimit: 0n, // 0 = contract default (30 days)
})
// Fund, deliver, release
await fundEscrow(config, escrowId)
await markDelivered(sellerConfig, escrowId)
await releaseFunds(config, escrowId)
// OR open dispute and resolve
await openDispute(config, escrowId)
await resolveDispute(arbiterConfig, { escrowId, percentageToSeller: 7000n }) // 70%
// Read escrow state
const escrow = await getEscrow(config, escrowId)
console.log(escrow.state, formatUSDC(escrow.amount))CLI Commands
| Command | Description |
|---------|-------------|
| create | Create a new escrow (7 params: buyer, seller, arbiter, amount, 3 time limits) |
| fund <id> | Fund escrow (auto-approves USDC allowance if needed) |
| deliver <id> | Seller marks delivery complete |
| release <id> | Buyer releases funds to seller (after delivery) |
| dispute <id> | Buyer or seller opens a dispute (after delivery) |
| resolve <id> --split <0-100> | Arbiter resolves dispute with split percentage to seller |
| settle <id> | Propose (--propose <pct>) or approve a settlement (during dispute) |
| cancel <id> | Request (--request) or approve mutual cancellation |
| refund <id> | Buyer claims refund after delivery deadline expired |
| timeout <id> --type payment | Seller claims after buyer payment timeout (v1.2) |
| timeout <id> --type arbiter | Claim after arbiter resolution timeout (v1.2) |
| status <id> | View all 15 escrow fields (read-only, no key needed) |
| next-id | View next escrow ID (read-only, no key needed) |
Global options (available on all commands):
--private-key <key> Private key for signing (or set PRIVATE_KEY env var)
--contract <address> TrustEscrow contract address (default: Base Sepolia v1.2)
--token <address> USDC token address (default: Base Sepolia)
--rpc <url> Custom RPC URL (default: https://sepolia.base.org)Library API
Write functions (require walletClient)
| Function | Signature | Description |
|----------|-----------|-------------|
| createEscrow | (config, CreateEscrowParams) => Promise<Hash> | Create escrow with 7 params |
| fundEscrow | (config, escrowId: bigint) => Promise<Hash> | Atomic approve + fund |
| markDelivered | (config, escrowId: bigint) => Promise<Hash> | Seller marks delivery |
| releaseFunds | (config, escrowId: bigint) => Promise<Hash> | Buyer releases to seller |
| openDispute | (config, escrowId: bigint) => Promise<Hash> | Open dispute after delivery |
| resolveDispute | (config, ResolveDisputeParams) => Promise<Hash> | Arbiter resolves with split |
| proposeSettlement | (config, ProposeSettlementParams) => Promise<Hash> | Propose settlement during dispute |
| approveSettlement | (config, ApproveSettlementParams) => Promise<Hash> | Approve settlement (L-02 safe) |
| requestCancel | (config, escrowId: bigint) => Promise<Hash> | Request mutual cancellation |
| approveCancel | (config, escrowId: bigint) => Promise<Hash> | Approve cancellation request |
| claimRefund | (config, escrowId: bigint) => Promise<Hash> | Buyer refund after deadline |
| claimAfterPaymentTimeLimit | (config, escrowId: bigint) => Promise<Hash> | Seller claims after buyer timeout |
| claimAfterArbiterTimeLimit | (config, escrowId: bigint) => Promise<Hash> | Claim after arbiter timeout |
| approveUSDC | (config, amount: bigint) => Promise<Hash> | Standalone USDC approval |
Read functions (publicClient only)
| Function | Signature | Description |
|----------|-----------|-------------|
| getEscrow | (config, escrowId: bigint) => Promise<EscrowData> | Read all 15 escrow fields |
| getNextEscrowId | (config) => Promise<bigint> | Next escrow ID to be assigned |
Utility functions
| Function | Signature | Description |
|----------|-----------|-------------|
| formatUSDC | (amount: bigint) => string | 6-decimal bigint to "10.50" |
| parseUSDC | (amount: string) => bigint | "10.50" to 6-decimal bigint |
Time Limits (v1.2)
TrustEscrow v1.2 introduced three independent configurable time limits per escrow:
| Parameter | CLI flag | Default | Description |
|-----------|----------|---------|-------------|
| deliveryTimeLimit | --delivery-time-limit | 30 days | Seller must mark delivery within this window (starts at funding) |
| paymentTimeLimit | --payment-time-limit | 14 days | Buyer must release/dispute within this window (starts after delivery) |
| arbiterTimeLimit | --arbiter-time-limit | 30 days | Arbiter must resolve within this window (starts when dispute opened) |
Duration format: 7d, 24h, 30m, or plain seconds. Pass 0 for contract default.
Bounds: Minimum 1 hour, maximum 90 days per time limit.
When a time limit expires:
- Delivery deadline: buyer can call
claimRefundto recover USDC - Payment deadline: seller can call
timeout --type paymentto claim payment - Arbiter deadline: either party can call
timeout --type arbiterto recover USDC
Use Case: Agent Service Escrow
The canonical trust-escrow use case for AI agents:
1. Agent A (client) identifies Agent B (service provider) via agent marketplace
2. They agree on terms: task, payment (10 USDC), evaluation criteria
3. Agent A creates and funds escrow — payment locked, not yet sent
4. Agent B executes the task and calls markDelivered
5a. If quality is good: Agent A calls releaseFunds — Agent B gets 9.97 USDC (0.3% fee)
5b. If quality is partial: Agent A opens dispute, Agent C evaluates
Agent C resolves: e.g., 70% to Agent B (6.97 USDC), 30% refunded to Agent A (2.9 USDC)
Agent C receives 1.5% dispute fee (0.15 USDC)See examples/agent-escrow-demo.ts for a full runnable demo with 3 wallets on Base Sepolia.
Trust in the Agent Stack
Agent A ──[wants service]──► Agent B
│ │
▼ ▼
x402 trust-escrow
(micropayments: (escrow: lock funds,
pay per API call) verify delivery, split)- x402 (Coinbase) — HTTP 402 payment required. Instant pay-then-get. For API calls, data access, streaming services.
- trust-escrow — Escrow with delivery verification. For tasks with uncertain outcomes, multi-step work, or quality that requires evaluation.
Use x402 when you trust the service. Use trust-escrow when you need protection.
Contract
- Network: Base (Base Sepolia for testnet)
- Version: v1.2 (7-param
createEscrow, 3 independent time limits, L-02 settlement fix) - Protocol fee: 0.3% on
releaseFundsandresolveDispute - Dispute fee: 1.5% paid to arbiter on
resolveDispute - No fee: on
cancel,claimRefund,claimAfterPaymentTimeLimit,claimAfterArbiterTimeLimit - Security: Audited, 99.35% test coverage (107 tests including fuzz + invariant)
Running the Demo
# 1. Set up wallets and environment
cp packages/trust-escrow/examples/.env.example packages/trust-escrow/examples/.env
# Edit .env: fill in BUYER_PRIVATE_KEY, SELLER_PRIVATE_KEY, ARBITER_PRIVATE_KEY, CONTRACT_ADDRESS
# 2. Fund wallets with testnet ETH: https://faucet.coinbase.com (Base Sepolia)
# 3. Fund buyer with testnet USDC: at least 5 USDC
# 4. Run demo
cd packages/trust-escrow
npm run demoExpected output: 9 transactions across 2 paths (happy + dispute), all confirmed on Base Sepolia.
License
MIT
