ClearPact API
ERC-8183 compliant job escrow for AI agents. Immutable infrastructure — no admin can freeze, decide, or upgrade.
Quick Start
Get from zero to your first escrow in 3 steps. No signup form, no OAuth. Just an email and an API call.
Self-hosting prerequisite: Run npm run migrate before starting the server. Without it, /api/job returns HTTP 500 (missing jobs / job_events tables).
Get an API Key
Create a key with your email. You'll get a key that starts with cpk_live_. Save it — it's only shown once.
curl
curl -X POST https://clearpact.polsia.app/api/keys \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "name": "My First Key"}'
Create an Escrow
Lock USDC between two parties with conditions for release.
curl
curl -X POST https://clearpact.polsia.app/api/escrow \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE" \
-d '{
"payer": "0xAliceWallet",
"payee": "0xBobWallet",
"amount": "250.00",
"token": "USDC",
"conditions": [
{"type": "task_completion", "description": "Complete data analysis"}
]
}'
Settle When Conditions Are Met
Verify conditions and release funds to the payee.
curl
curl -X POST https://clearpact.polsia.app/api/escrow/ESCROW_ID/settle \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE" \
-d '{
"verifications": {"0": {"completed": true}},
"actor": "0xAliceWallet"
}'
The Quick Start uses testnet (Base Sepolia) by default — free and instant. Ready for production? See Network Selection to switch to Base mainnet with a single parameter change.
Try It: Generate an API Key
Enter your email to get a live API key right now.
Save this key now. It won't be shown again.
Key ID:
⚡ Try it Live
Create a real testnet escrow right here — no terminal required. Paste your API key from the generator above, then click Create.
📊 Try it Live — Key Usage Logs
Inspect your own API call history. Paste your key and key_id below.
🔔 Try it Live — Webhook Deliveries
Inspect delivery attempts for a webhook. Paste your key and webhook UUID.
Network Selection
ClearPact supports both Base Sepolia testnet and Base mainnet. Use testnet for prototyping and CI. Use mainnet for production workloads with real USDC.
| Parameter | Testnet (default) | Mainnet |
|---|---|---|
network value | "testnet" | "mainnet" |
| Chain ID | 84532 (Base Sepolia) | 8453 (Base) |
| USDC contract | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Get test funds | Alchemy Faucet | Real USDC required |
How to specify the network
Pass "network" in any create/fund/settle escrow request body. Omit it for testnet (default).
# Testnet (default — omit network or pass "testnet") curl -X POST https://clearpact.polsia.app/api/escrow \ -H "X-API-Key: cpk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"payer": "0xAlice", "payee": "0xBob", "amount": "100.00"}' # Mainnet — add "network": "mainnet" curl -X POST https://clearpact.polsia.app/api/escrow \ -H "X-API-Key: cpk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"payer": "0xAlice", "payee": "0xBob", "amount": "100.00", "network": "mainnet"}'
// Testnet (default) const escrow = await fetch("https://clearpact.polsia.app/api/escrow", { method: "POST", headers: { "X-API-Key": "cpk_live_YOUR_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ payer: "0xAlice", payee: "0xBob", amount: "100.00" }) }).then(r => r.json()); // Mainnet — same call, add network field const prodEscrow = await fetch("https://clearpact.polsia.app/api/escrow", { method: "POST", headers: { "X-API-Key": "cpk_live_YOUR_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ payer: "0xAlice", payee: "0xBob", amount: "100.00", network: "mainnet" }) }).then(r => r.json());
import requests HEADERS = {"X-API-Key": "cpk_live_YOUR_KEY"} # Testnet (default) escrow = requests.post( "https://clearpact.polsia.app/api/escrow", headers=HEADERS, json={"payer": "0xAlice", "payee": "0xBob", "amount": "100.00"} ).json() # Mainnet — add network="mainnet" prod_escrow = requests.post( "https://clearpact.polsia.app/api/escrow", headers=HEADERS, json={"payer": "0xAlice", "payee": "0xBob", "amount": "100.00", "network": "mainnet"} ).json()
Default is testnet. If you omit network, all requests use Base Sepolia. Explicitly pass "network": "mainnet" for production transactions.
Authentication
All escrow endpoints require an API key passed in the X-API-Key header. Key management endpoints (/api/keys) are public.
API Key Format: Keys start with cpk_live_ followed by 48 hex characters. Example: cpk_live_a1b2c3d4e5f6...
# Every request to /api/escrow/* must include:
X-API-Key: cpk_live_your_key_here
Protected endpoints (require X-API-Key):
| Method | Endpoint | Description |
|---|---|---|
GET | /api/escrow | List escrows |
POST | /api/escrow | Create escrow |
GET | /api/escrow/:id | Get escrow details |
POST | /api/escrow/:id/settle | Settle escrow |
POST | /api/x402/verify | Verify x402 payment authorization |
POST | /api/x402/settle | Settle x402 payment on-chain |
Public endpoints (no auth needed):
| Method | Endpoint | Description |
|---|---|---|
POST | /api/keys | Create API key |
GET | /api/keys | List your keys |
DELETE | /api/keys/:key_id | Revoke a key |
POST | /api/keys/:key_id/rotate | Rotate a key |
GET | /api/x402/health | x402 facilitator status |
Rate Limits
Each API key has independent rate limits. Default limits:
| Limit | Default | Header |
|---|---|---|
| Per minute | 60 requests | X-RateLimit-Limit |
| Per day | 10,000 requests | - |
| Remaining | - | X-RateLimit-Remaining |
When rate limited, responses include a Retry-After header (in seconds). Response code: 429.
Create API Key
Generate a new API key. Maximum 5 active keys per email.
REQUEST BODY
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Your email address | |
| name | string | No | Human-readable label (default: "Default") |
curl -X POST https://clearpact.polsia.app/api/keys \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "name": "Production"}'
const res = await fetch("https://clearpact.polsia.app/api/keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "dev@example.com", name: "Production" }) }); const { key } = await res.json(); console.log("Save this key:", key);
import requests res = requests.post("https://clearpact.polsia.app/api/keys", json={ "email": "dev@example.com", "name": "Production" }) data = res.json() print(f"Save this key: {data['key']}")
RESPONSE (201)
{
"success": true,
"message": "API key created. Save it now — it won't be shown again.",
"key": "cpk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6",
"key_id": "cp_1a2b3c4d5e6f7g8h",
"key_prefix": "cpk_live_a1b2c3",
"name": "Production",
"permissions": ["escrow:read", "escrow:write", "escrow:settle"],
"rate_limits": { "per_minute": 60, "per_day": 10000 },
"created_at": "2026-04-09T10:00:00.000Z"
}
List API Keys
List all API keys associated with your email. Keys are returned without the secret value.
curl "https://clearpact.polsia.app/api/keys?email=dev@example.com"
const res = await fetch( "https://clearpact.polsia.app/api/keys?email=dev@example.com" ); const { keys } = await res.json();
res = requests.get("https://clearpact.polsia.app/api/keys", params={ "email": "dev@example.com" }) keys = res.json()["keys"]
RESPONSE (200)
{
"success": true,
"keys": [
{
"key_id": "cp_1a2b3c4d5e6f7g8h",
"key_prefix": "cpk_live_a1b2c3",
"name": "Production",
"is_active": true,
"last_used_at": "2026-04-09T10:30:00.000Z",
"requests_today": 42,
"created_at": "2026-04-09T10:00:00.000Z"
}
],
"count": 1
}
Revoke API Key
Immediately revoke an API key. All future requests with this key will be rejected.
curl -X DELETE https://clearpact.polsia.app/api/keys/cp_1a2b3c4d5e6f7g8h \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
await fetch("https://clearpact.polsia.app/api/keys/cp_1a2b3c4d5e6f7g8h", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "dev@example.com" }) });
Rotate API Key
Revoke the old key and generate a new one with the same settings. Zero-downtime key rotation.
curl -X POST https://clearpact.polsia.app/api/keys/cp_1a2b3c4d5e6f7g8h/rotate \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
const res = await fetch( "https://clearpact.polsia.app/api/keys/cp_1a2b3c4d5e6f7g8h/rotate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "dev@example.com" }) } ); const { key } = await res.json(); // Update your config with the new key
GET /api/keys/:key_id/usage
List API requests made by a key — timestamps, paths, status codes, latency. Auth: the calling X-API-Key must match :key_id. Rate-limited at 60 req/min.
QUERY PARAMETERS
| Param | Type | Default | Description |
|---|---|---|---|
| from | ISO 8601 | 7 days ago | Start of date range |
| to | ISO 8601 | now | End of date range |
| status | string | — | Filter by status code or class: 200, 2xx, 4xx, 5xx |
| endpoint | string | — | Prefix filter: /escrow matches all escrow paths |
| limit | int | 50 | Max results (max 500) |
| cursor | string | — | Opaque cursor from next_cursor |
curl "https://clearpact.polsia.app/api/keys/cp_YOUR_KEY_ID/usage?status=4xx&limit=50" \ -H "X-API-Key: cpk_live_YOUR_KEY"
const res = await fetch( "https://clearpact.polsia.app/api/keys/cp_YOUR_KEY_ID/usage?status=4xx&limit=50", { headers: { "X-API-Key": "cpk_live_YOUR_KEY" } } ); const { data, next_cursor, summary } = await res.json(); // SDK equivalent: const { data, next_cursor, summary } = await client.keys.usage("cp_YOUR_KEY_ID", { status: "4xx", limit: 50 });
import requests res = requests.get( "https://clearpact.polsia.app/api/keys/cp_YOUR_KEY_ID/usage", headers={"X-API-Key": "cpk_live_YOUR_KEY"}, params={"status": "4xx", "limit": 50} ) data = res.json()
RESPONSE (200)
{
"data": [
{
"request_id": "req_1234",
"method": "POST",
"path": "/api/escrow",
"status_code": 200,
"latency_ms": 87,
"created_at": "2026-05-05T09:11:23Z"
}
],
"next_cursor": null,
"summary": {
"count": 150,
"by_status_class": { "2xx": 143, "4xx": 7 },
"by_endpoint": { "/api/escrow": 90, "/api/escrow/:id": 60 }
}
}
List Escrows
List escrows for the authenticated API key with optional filtering and pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by escrow status: pending_funding, funded, active, awaiting_verification, settling, settled, cancelled, refunded, expired, expiry_failed. Supports comma-separated multiple values. See Escrow Status Values for descriptions. |
network | string | Filter by network: testnet or mainnet. Supports comma-separated multiple values. |
created_after | string | ISO 8601 date — only escrows created on or after this date. |
created_before | string | ISO 8601 date — only escrows created on or before this date. |
limit | integer | Number of results per page. Default: 20, max: 100. |
offset | integer | Number of results to skip. Default: 0. Use with limit for pagination. |
sort | string | Sort field: created_at (default), updated_at, amount, status. |
order | string | Sort direction: desc (default) or asc. |
Response
{
"success": true,
"escrows": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"network": "testnet",
"payer": "0xPayer...",
"payee": "0xPayee...",
"amount": 500,
"token": "USDC",
"status": "settled",
"conditions": [...],
"created_at": "2025-04-01T12:00:00Z",
"settled_at": "2025-04-02T15:30:00Z",
"tx_hashes": { "create": "0x...", "settle": "0x..." }
}
],
"pagination": {
"total": 42,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Examples
List all escrows (newest first)
curl https://clearpact.polsia.app/api/escrow \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE"
Filter by status and network
curl "https://clearpact.polsia.app/api/escrow?status=settled&network=testnet" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE"
Paginate — page 2, 10 per page
curl "https://clearpact.polsia.app/api/escrow?limit=10&offset=10" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE"
Date range — last 7 days
curl "https://clearpact.polsia.app/api/escrow?created_after=2025-04-10&sort=created_at&order=asc" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE"
Active escrows only
curl "https://clearpact.polsia.app/api/escrow?status=active,funded,pending_funding" \
-H "X-API-Key: cpk_live_YOUR_KEY_HERE"
Node.js const res = await fetch( "https://clearpact.polsia.app/api/escrow?status=settled&limit=20", { headers: { 'X-API-Key': key } } ); const { escrows, pagination } = await res.json(); // pagination.total tells you the total count // pagination.has_more tells you if more pages exist
Python resp = requests.get( "https://clearpact.polsia.app/api/escrow", params={"status": "settled", "limit": "20"}, headers={"X-API-Key": key} ) data = resp.json() escrows = data["escrows"]
Create Escrow
Create a new escrow contract between two parties. Requires X-API-Key header.
REQUEST BODY
| Field | Type | Required | Description |
|---|---|---|---|
| payer | string | Yes | Payer wallet address or identifier |
| payee | string | Yes | Payee wallet address or identifier |
| amount | string/number | Yes | Amount in token units (e.g. "250.00") |
| token | string | No | Token symbol (default: "USDC") |
| network | string | No | "testnet" (default) or "mainnet" — see Network Selection |
| conditions | array | No | Settlement conditions (see Condition Types) |
| metadata | object | No | Arbitrary metadata (project name, etc.) |
| expires_at | string | No | ISO 8601 expiration timestamp |
curl -X POST https://clearpact.polsia.app/api/escrow \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY" \
-d '{
"payer": "0xAlice",
"payee": "0xBob",
"amount": "500.00",
"token": "USDC",
"conditions": [
{"type": "task_completion", "description": "Ship v2.0 release"},
{"type": "approval", "approver": "0xAlice"}
],
"metadata": {"project": "agent-audit"},
"expires_at": "2026-12-31T00:00:00Z"
}'
const res = await fetch("https://clearpact.polsia.app/api/escrow", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "cpk_live_YOUR_KEY" }, body: JSON.stringify({ payer: "0xAlice", payee: "0xBob", amount: "500.00", conditions: [ { type: "task_completion", description: "Ship v2.0" }, { type: "approval", approver: "0xAlice" } ] }) }); const { escrow } = await res.json(); console.log("Escrow ID:", escrow.id);
import requests res = requests.post( "https://clearpact.polsia.app/api/escrow", headers={"X-API-Key": "cpk_live_YOUR_KEY"}, json={ "payer": "0xAlice", "payee": "0xBob", "amount": "500.00", "conditions": [ {"type": "task_completion", "description": "Ship v2.0"} ] } ) escrow = res.json()["escrow"] print(f"Escrow ID: {escrow['id']}")
RESPONSE (201)
{
"success": true,
"network": "testnet",
"escrow": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"network": "testnet",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"status": "pending_funding",
"conditions": [
{"type": "task_completion", "description": "Ship v2.0 release"},
{"type": "approval", "approver": "0xAlice"}
],
"metadata": {"project": "agent-audit"},
"created_at": "2026-04-09T10:00:00.000Z",
"expires_at": "2026-12-31T00:00:00.000Z"
},
"blockchain": {
"network": "testnet",
"chain_id": 84532,
"contract_address": "0x7CDB80e9B154c99354d66604103fAEb148c6f5A8",
"onchain_escrow_id": 43,
"onchain_status": "pending_funding", // contract enum 0 — just created
"create_tx_hash": "0xabc1...",
"asset_address": "0x036C...",
"explorer": "https://sepolia.basescan.org/tx/0xabc1...",
"explorer_contract": "https://sepolia.basescan.org/address/0xe1E4..."
},
"erc8004": null
}
New escrows always start with status: "pending_funding" and blockchain.onchain_status: "pending_funding". See Escrow Status Values and On-Chain Mapping for the full lifecycle.
Get Escrow
Retrieve escrow details and full event audit trail.
curl https://clearpact.polsia.app/api/escrow/550e8400-e29b-41d4-a716-446655440000 \ -H "X-API-Key: cpk_live_YOUR_KEY"
const res = await fetch( "https://clearpact.polsia.app/api/escrow/550e8400-...", { headers: { "X-API-Key": "cpk_live_YOUR_KEY" } } ); const { escrow, events } = await res.json();
res = requests.get(
"https://clearpact.polsia.app/api/escrow/550e8400-...",
headers={"X-API-Key": "cpk_live_YOUR_KEY"}
)
data = res.json()
RESPONSE (200)
{
"success": true,
"network": "testnet",
"escrow": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"network": "testnet",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"status": "funded", // all possible values
"conditions": [{"type": "task_completion"}],
"metadata": {},
"created_at": "2026-04-18T10:00:00.000Z",
"updated_at": "2026-04-18T10:05:00.000Z",
"funded_at": "2026-04-18T10:05:00.000Z",
"settled_at": null,
"expires_at": "2026-04-25T10:00:00.000Z",
"tx_hashes": {
"create": "0x1a2b...",
"fund": "0x3c4d...",
"settle": null,
"refund": null
}
},
"blockchain": {
"network": "testnet",
"chain_id": 84532,
"contract_address": "0xe1E4...0DB0",
"onchain_escrow_id": 42,
"onchain_status": "funded",
"tx_hashes": { "create": "0x1a2b...", "fund": "0x3c4d...", "settle": null, "refund": null },
"explorer": "https://sepolia.basescan.org/tx/0x3c4d..."
},
"events": [
{
"event_type": "created",
"actor": "0xAlice",
"details": {"amount": 500, "token": "USDC"},
"created_at": "2026-04-18T10:00:00.000Z"
},
{
"event_type": "funded",
"actor": "0xAlice",
"details": {"tx_hash": "0x3c4d...", "amount": 500},
"created_at": "2026-04-18T10:05:00.000Z"
}
]
}
Settle Escrow
Verify all conditions and release funds. All conditions must be met for settlement.
REQUEST BODY
| Field | Type | Required | Description |
|---|---|---|---|
| verifications | object | Yes | Map of condition index to verification data |
| actor | string | No | Who is triggering the settlement |
curl -X POST https://clearpact.polsia.app/api/escrow/ESCROW_ID/settle \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY" \
-d '{
"verifications": {
"0": {"completed": true},
"1": {"approved": true, "approver": "0xAlice"}
},
"actor": "0xOracle"
}'
const res = await fetch( `https://clearpact.polsia.app/api/escrow/${escrowId}/settle`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "cpk_live_YOUR_KEY" }, body: JSON.stringify({ verifications: { "0": { completed: true }, "1": { approved: true, approver: "0xAlice" } }, actor: "0xOracle" }) } ); const { settlement } = await res.json(); console.log("Settled:", settlement.amount, settlement.token);
res = requests.post(
f"https://clearpact.polsia.app/api/escrow/{escrow_id}/settle",
headers={"X-API-Key": "cpk_live_YOUR_KEY"},
json={
"verifications": {
"0": {"completed": True},
"1": {"approved": True, "approver": "0xAlice"}
},
"actor": "0xOracle"
}
)
settlement = res.json()["settlement"]
RESPONSE — SUCCESS (200)
{
"success": true,
"message": "Escrow settled. 500 USDC released to 0xBob",
"escrow": { "status": "settled", /* ... */ },
"settlement": {
"amount": 500,
"token": "USDC",
"from": "0xAlice",
"to": "0xBob",
"conditions": [
{"index": 0, "type": "task_completion", "met": true},
{"index": 1, "type": "approval", "met": true}
],
"settled_at": "2026-04-09T12:00:00.000Z"
}
}
RESPONSE — CONDITIONS NOT MET (422)
{
"success": false,
"message": "Not all conditions are met",
"conditions": [
{"index": 0, "type": "task_completion", "met": true},
{"index": 1, "type": "approval", "met": false}
]
}
ERC-8004 Automatic Settlement
Phase 2 adds a validation adapter that automatically settles escrows when an ERC-8004-compatible proof is received. No human needs to call /settle.
How it works: Create an escrow with external_job_id. When the job is verified, submit a validation signal via POST /api/validation/submit. The adapter finds the matching escrow and calls settleEscrow() on-chain within seconds.
Flow
Create escrow with external_job_id
POST /api/escrow
{
"payer": "0xAlice",
"payee": "0xBob",
"amount": 100,
"external_job_id": "job_abc123"
}
// Response includes erc8004: { external_job_id, status: "linked" }
Fund the escrow
POST /api/escrow/ESCROW_ID/fund
// status: pending_funding → funded
Submit validation signal
POST /api/validation/submit
{
"external_job_id": "job_abc123",
"proof_hash": "0x...",
"result": "success"
}
Settlement happens automatically
The adapter processes the signal (within 5 seconds), calls settleEscrow() on-chain, and the payee receives USDC. No manual action required.
GET /api/escrow/ESCROW_ID // escrow.status: "settled" // escrow.tx_hashes.settle: "0x..."
POST /api/validation/submit
Submit an ERC-8004-compatible validation signal. Triggers automatic settlement if result = "success" and the linked escrow is funded.
| Field | Type | Required | Description |
|---|---|---|---|
external_job_id | string | Yes | Matches the escrow's external_job_id |
proof_hash | string | No | 0x-prefixed keccak256 of proof payload |
result | string | No | "success" (default) or "failure" |
metadata | object | No | Arbitrary proof metadata |
curl
curl -X POST https://clearpact.polsia.app/api/validation/submit \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY" \
-d '{
"external_job_id": "job_abc123",
"proof_hash": "0xabc...",
"result": "success",
"metadata": {"validator": "oracle-v1"}
}'
RESPONSE (202 Accepted)
{
"success": true,
"message": "Validation received. Settlement will process automatically.",
"record": {
"id": "uuid...",
"external_job_id": "job_abc123",
"proof_hash": "0xabc...",
"result": "success",
"status": "pending",
"escrow_id": "uuid..."
}
}
GET /api/validation/:id
Check the processing status of a validation record.
curl
curl https://clearpact.polsia.app/api/validation/RECORD_ID \
-H "X-API-Key: cpk_live_YOUR_KEY"
RECORD STATUS VALUES
| Status | Meaning |
|---|---|
pending | Received, queued for processing |
processing | Adapter is running settlement now |
settled | Escrow settled — settle_tx_hash populated |
rejected | Duplicate signal or already-settled escrow |
failed | Error during settlement — check error_message |
Replay Protection
The adapter prevents double-settlement via two mechanisms:
- Database constraint:
UNIQUE(external_job_id, proof_hash)— submitting the same signal twice returns409 Conflict. - Status check: Before calling
settleEscrow(), the adapter checks the escrow is not alreadysettled,cancelled, orexpired. Already-settled escrows returnstatus: "rejected"on the validation record.
Deprecation notice: POST /api/escrow/:id/settle is deprecated. It still works during the 7-day drainage period (sunset: 2026-05-23) but emits X-Deprecated: true headers. Migrate to POST /api/job/:id/complete (v2) or POST /api/validation/submit (ERC-8004 auto-settlement).
x402 Protocol
ClearPact is a native x402 facilitator — compatible with Coinbase's open agentic payment protocol (35M+ transactions). x402 lets AI agents authorize USDC payments using EIP-712 typed-data signatures, with no gas wallet required on the payer side.
How ClearPact bridges x402 into escrow: A payer agent signs a TransferWithAuthorization payload (ERC-3009). ClearPact verifies the signature off-chain (/verify), then submits USDC.transferWithAuthorization() on Base Sepolia (/settle). If the payer has an active escrow, settlement automatically funds it. Two-layer replay protection prevents double-spend.
Testnet (default): Base Sepolia (chain 84532) · USDC: 0x036CbD53842c5426634e7929541eC2318f3dCF7e
Mainnet: Base (chain 8453) · USDC: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Pass "network": "mainnet" in x402 verify/settle calls to operate on Base mainnet.
GET /api/x402/health
Check facilitator status. Returns the current chain config and feature flags. No API key required.
curl
curl https://clearpact.polsia.app/api/x402/health
RESPONSE (200)
{
"status": "ok",
"phase": "1B — verify + on-chain settlement",
"networks": {
"testnet": {
"chain_id": 84532,
"network": "base-sepolia",
"usdc_contract": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"settle_enabled": true
},
"mainnet": {
"chain_id": 8453,
"network": "base",
"usdc_contract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"settle_enabled": true
}
},
"features": {
"verify": true,
"settle": true
}
}
POST /api/x402/verify
Verify an x402 payment authorization. Accepts an EIP-712 TransferWithAuthorization payload. Checks the signature, validates time window and amount, runs two-layer replay protection (in-memory nonce + DB constraint), and logs the attempt. Returns the verification result and a transaction_id for use with /settle.
REQUEST BODY
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Yes | Payer wallet address (EIP-55 checksum) |
| to | string | Yes | Recipient wallet address |
| value | string | Yes | Transfer amount in USDC base units (6 decimals) |
| validAfter | string | Yes | Unix timestamp — signature not valid before this |
| validBefore | string | Yes | Unix timestamp — signature expires after this |
| nonce | string | Yes | 32-byte hex nonce (must be unique per payer address) |
| signature | string | Yes | EIP-712 signature (0x-prefixed, 65 bytes) |
| network | string | No | "testnet" (default) or "mainnet" |
curl -X POST https://clearpact.polsia.app/api/x402/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY" \
-d '{
"from": "0xAgentWalletAddress",
"to": "0xRecipientAddress",
"value": "1000000",
"validAfter": "0",
"validBefore": "9999999999",
"nonce": "0xabc123...32bytes",
"signature": "0xsig..."
}'
const res = await fetch("https://clearpact.polsia.app/api/x402/verify", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "cpk_live_YOUR_KEY" }, body: JSON.stringify({ from: "0xAgentWalletAddress", to: "0xRecipientAddress", value: "1000000", // 1 USDC = 1_000_000 (6 decimals) validAfter: "0", validBefore: "9999999999", nonce: "0xabc123...32bytes", signature: "0xsig..." }) }); const { verified, transaction_id, escrow_id } = await res.json();
import requests res = requests.post( "https://clearpact.polsia.app/api/x402/verify", headers={"X-API-Key": "cpk_live_YOUR_KEY"}, json={ "from": "0xAgentWalletAddress", "to": "0xRecipientAddress", "value": "1000000", # 1 USDC "validAfter": "0", "validBefore": "9999999999", "nonce": "0xabc123...32bytes", "signature": "0xsig..." } ) data = res.json() transaction_id = data["transaction_id"]
RESPONSE — VERIFIED (200)
{
"success": true,
"verified": true,
"transaction_id": "uuid-of-x402-record",
"recovered_address": "0xAgentWalletAddress",
"escrow_id": "uuid-of-linked-escrow", // null if no active escrow
"amount_usdc": "1.000000"
}
RESPONSE — INVALID SIGNATURE (422)
{
"success": false,
"verified": false,
"error": "invalid_signature",
"message": "Recovered address does not match 'from'"
}
POST /api/x402/settle
Execute an on-chain USDC settlement via transferWithAuthorization() (ERC-3009). Supports two call modes: Mode A — pass a transaction_id from a prior /verify call; Mode B — pass the full x402 payload for a single-call verify + settle. On success, returns the on-chain tx hash and block number. If the payer has an active escrow, it is automatically marked funded.
REQUEST BODY
| Field | Type | Mode | Description |
|---|---|---|---|
| transaction_id | string | A | UUID from a prior /verify response |
| from | string | B | Payer wallet address |
| to | string | B | Recipient wallet address |
| value | string | B | Amount in USDC base units (6 decimals) |
| validAfter | string | B | Unix timestamp — valid after |
| validBefore | string | B | Unix timestamp — valid before |
| nonce | string | B | 32-byte hex nonce |
| signature | string | B | EIP-712 signature |
# Mode A — settle a pre-verified transaction curl -X POST https://clearpact.polsia.app/api/x402/settle \ -H "Content-Type: application/json" \ -H "X-API-Key: cpk_live_YOUR_KEY" \ -d '{"transaction_id": "uuid-from-verify-response"}' # Mode B — verify + settle in one call curl -X POST https://clearpact.polsia.app/api/x402/settle \ -H "Content-Type: application/json" \ -H "X-API-Key: cpk_live_YOUR_KEY" \ -d '{ "from": "0xAgentWalletAddress", "to": "0xRecipientAddress", "value": "1000000", "validAfter": "0", "validBefore": "9999999999", "nonce": "0xabc123...32bytes", "signature": "0xsig..." }'
// Mode A — settle pre-verified tx const res = await fetch("https://clearpact.polsia.app/api/x402/settle", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "cpk_live_YOUR_KEY" }, body: JSON.stringify({ transaction_id: "uuid-from-verify" }) }); const { settlement_tx_hash, block_number, escrow_id } = await res.json();
# Mode A — settle pre-verified tx res = requests.post( "https://clearpact.polsia.app/api/x402/settle", headers={"X-API-Key": "cpk_live_YOUR_KEY"}, json={"transaction_id": "uuid-from-verify"} ) data = res.json() tx_hash = data["settlement_tx_hash"] block = data["block_number"]
RESPONSE — SETTLED (200)
{
"success": true,
"settlement_tx_hash": "0xabc123...on-chain tx",
"block_number": 19842301,
"amount_usdc": "1.000000",
"escrow_id": "uuid-of-funded-escrow", // null if no linked escrow
"escrow_status": "funded"
}
x402 SETTLE ERROR CODES
| Error Code | HTTP | Description |
|---|---|---|
gas_estimation_failed | 502 | Could not estimate gas — network congestion or invalid payload |
nonce_already_used | 409 | This nonce has already been used for this payer address |
insufficient_usdc_balance | 422 | Payer does not hold enough USDC to cover the transfer |
tx_reverted | 502 | On-chain transaction was submitted but reverted |
authorization_expired | 422 | Current time is outside the validAfter/validBefore window |
invalid_signature | 422 | EIP-712 signature recovery failed or address mismatch |
transaction_not_found | 404 | No x402 record found for the given transaction_id |
Contract Addresses
ClearPact deploys escrow contracts independently per network. Use the correct contract address for explorer links and direct on-chain interaction.
Base Sepolia — Testnet (Phase 4 v2)
| Contract | Address | Explorer / Verification |
|---|---|---|
| ClearPactJob proxy | 0x7CDB80e9B154c99354d66604103fAEb148c6f5A8 |
BaseScan ↗ |
| ClearPactEvaluator proxy | 0x1DDefFED6a9e28C37e1E10c292F6774D837a7Ab6 |
BaseScan ↗ |
| ClearPactJob impl | 0xf901FAE0851a78156b6952753D266E1151798a94 |
Sourcify FULL MATCH ↗ |
| ClearPactEvaluator impl | 0x12c34A6EAeaE5016B9420801CBf13B4b5b7b3c95 |
Sourcify FULL MATCH ↗ |
| USDC | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
BaseScan ↗ |
| Chain ID | 84532 | |
| Faucet | alchemy.com/faucets/base-sepolia | |
Base — Mainnet
| Contract | Address | Explorer |
|---|---|---|
| ClearPactEscrow | Deployment in progress — wallet funding required | — |
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
BaseScan ↗ |
| Chain ID | 8453 | |
API responses include contract_address, chain_id, and BaseScan explorer links in the escrow object so you never need to hardcode these values.
Condition Types
Conditions define what must be true before funds are released. Each escrow can have multiple conditions — all must be met for settlement.
task_completion
Requires explicit confirmation that work is done.
Condition: {"type": "task_completion"}
Verification: {"completed": true}
approval
Requires sign-off from a specific approver address.
Condition: {"type": "approval", "approver": "0x..."}
Verification: {"approved": true, "approver": "0x..."}
deadline
Automatically met if current time is before the deadline.
Condition: {"type": "deadline", "deadline": "2026-12-31T..."}
Verification: None needed (auto-checked)
threshold
A numeric value must meet or exceed a minimum.
Condition: {"type": "threshold", "min_value": 100}
Verification: {"value": 150}
oracle
An external oracle must confirm the outcome.
Condition: {"type": "oracle"}
Verification: {"oracle_confirmed": true}
custom
Any custom condition type. Requires explicit met flag.
Condition: {"type": "my_custom_check"}
Verification: {"met": true}
Error Codes
All errors return JSON with success: false and a descriptive error code.
| HTTP | Error Code | Description |
|---|---|---|
| 400 | invalid_email | Email is missing or malformed |
| 400 | invalid_escrow_id | Escrow ID is not a valid UUID |
| 401 | missing_api_key | X-API-Key header not provided |
| 401 | invalid_api_key | API key not recognized |
| 401 | invalid_api_key_format | Key doesn't match cpk_ format |
| 401 | api_key_revoked | Key has been revoked |
| 401 | api_key_expired | Key has expired |
| 404 | escrow_not_found | No escrow with that ID |
| 404 | key_not_found | API key not found or already revoked |
| 409 | already_settled | Escrow is already settled/expired/cancelled |
| 422 | conditions_not_met | Not all conditions verified — includes details per condition |
| 429 | rate_limit_exceeded | Too many requests per minute |
| 429 | daily_limit_exceeded | Daily request limit reached |
ERROR RESPONSE FORMAT
{
"success": false,
"error": "missing_api_key",
"message": "API key required. Pass your key in the X-API-Key header.",
"docs": "https://clearpact.polsia.app/docs#authentication"
}
Escrow Status Values
Every escrow has a status field that reflects its position in the lifecycle. Build your integration to handle all statuses — webhook payloads will deliver any of these values.
LIFECYCLE (NON-TERMINAL)
| Status | Description | Triggered by | Valid transitions | Webhook event |
|---|---|---|---|---|
pending_funding |
Created on-chain, awaiting USDC deposit | POST /api/escrow |
funded, cancelled, expired |
escrow.created |
funded |
USDC deposited on-chain, awaiting settlement or verification | POST /api/escrow/:id/fund or x402 settle |
settled, refunded, expired, expiry_failed |
escrow.funded |
awaiting_verification |
Funds held on-chain, waiting for ERC-8004 verification signal | On-chain state (smart contract enum 2) | settled, refunded, expired |
— |
active |
Created without conditions — funds locked, ready to settle | POST /api/escrow (empty conditions) |
settled, refunded, expired |
escrow.created |
settling |
Settlement transaction in progress (transient, typically <5 s) | x402 settle flow | settled |
— |
TERMINAL STATES
| Status | Description | Triggered by | Webhook event |
|---|---|---|---|
settled |
Conditions met, funds released to payee on-chain | ERC-8004 auto-settlement or POST /api/escrow/:id/settle |
escrow.settled |
cancelled |
Cancelled before funding — no funds moved | POST /api/escrow/:id/cancel (unfunded escrow) |
escrow.cancelled |
refunded |
Funded escrow cancelled — USDC returned to payer on-chain | POST /api/escrow/:id/cancel (funded escrow) |
escrow.cancelled |
expired |
Past expires_at deadline, auto-refunded by expiry worker |
Expiry worker (runs automatically) | escrow.expired |
expiry_failed |
Expiry attempted but on-chain refund failed — requires manual review | Expiry worker (on-chain refund error) | escrow.expired |
Legacy status: Escrows created before on-chain support may have status pending. This is equivalent to pending_funding and is accepted by settle/cancel endpoints. New escrows always start as pending_funding.
TYPICAL LIFECYCLE
Happy path: pending_funding → funded → settled With ERC-8004 verification: pending_funding → funded → awaiting_verification → settled Cancellation (unfunded): pending_funding → cancelled Cancellation (funded — triggers on-chain refund): funded → refunded Expiry (auto-refund): funded → expired pending_funding → expired Expiry failure (on-chain refund failed): funded → expiry_failed
Job Status Mapping (v2 ERC-8183)
ClearPact v2 uses the ERC-8183 JobStatus enum — exactly 6 values. The v1 escrow API (drainage period) is mapped below for consumers migrating from legacy routes.
v2 Contract (immutable): 0x7CDB80e9B154c99354d66604103fAEb148c6f5A8 (Base Sepolia) — enum JobStatus { Open, Funded, Submitted, Completed, Rejected, Expired }
v2 JobStatus (ERC-8183) — 6 values
v2 status |
Enum value | Description | Valid transitions from |
|---|---|---|---|
Open | 0 | Job created; awaiting budget and funding | Initial state after createJob() |
Funded | 1 | Budget deposited on-chain | Open → Funded via fund() |
Submitted | 2 | Provider submitted deliverable; awaiting evaluation | Funded → Submitted via submit() |
Completed | 3 | Evaluator or client approved; payment released to provider | Submitted → Completed via complete() |
Rejected | 4 | Result rejected; symmetric refund from Funded/Submitted (E3) | Open/Funded/Submitted → Rejected via reject() |
Expired | 5 | Past expiredAt; refund claimed by client |
Funded/Submitted → Expired via claimRefund() |
v1 → v2 Status Mapping (migration reference)
| v1 status (legacy) | v2 status (ERC-8183) | Notes |
|---|---|---|
PendingFunding (0) / pending |
Open |
Job created but unfunded |
Funded (1) / active |
Funded |
Budget locked on-chain |
AwaitingVerification (2) |
Submitted |
Work delivered, pending evaluation |
Settled (3) / completed |
Completed |
Payment released to provider |
Cancelled (5) |
Rejected |
Rejected with symmetric refund |
Refunded (4) / expired |
Expired |
Refund claimed after expiry |
settling, expiry_failed |
(no direct mapping) | Transient backend-only states, no v2 equivalent |
ERC-8183 State Machine
├─ setBudget() + fund() ──► Funded
│ └─ submit() ──────────► Submitted
│ ├─ complete() ───────► Completed ✓ payment released
│ └─ reject() ─────────► Rejected ↩ refund
│ └─ post-expiredAt: claimRefund() ─► Expired ↩ refund
└─ reject() ────────────────────► Rejected (from Open, no refund needed)
complete() + reject() callable by evaluator OR client at Submitted.E3:
reject() from Funded/Submitted triggers symmetric automatic refund.claimRefund guarantee: unconditional, non-hookable, non-pausable (PAUSER renounced).
Immutability Commitment
ClearPact is pure immutable escrow infrastructure: no admin can freeze, no admin can decide, no admin can upgrade.
At Phase 4 deploy (May 2026), ClearPact executed a 5-renounce sequence across both proxy contracts — permanently removing all administrative control. This is verifiable on-chain.
5-Renounce Sequence (Phase 4)
| Role renounced | Contract | Effect |
|---|---|---|
PAUSER_ROLE | ClearPactJob | No one can pause the contract. claimRefund() is always available. |
OPERATOR_ROLE | ClearPactJob | No operator can intervene in job lifecycle. |
EVALUATOR_ROLE | ClearPactEvaluator | ClearPact cannot appoint evaluators. Self-service only (Decision 9). |
DEFAULT_ADMIN_ROLE | ClearPactJob | No admin functions callable. Contract parameters immutable. |
DEFAULT_ADMIN_ROLE | ClearPactEvaluator | Evaluator router permanently immutable. |
Self-Service Evaluator Pattern (Decision 9)
Each job creator specifies their own evaluator EOA at createJob(). ClearPact does not appoint or arbitrate evaluators. The evaluator EOA calls complete() or reject() directly via the Job API — the Evaluator contract is not required.
// Self-service: job creator picks their evaluator at creation const { job } = await client.jobs.create( '0xProviderWallet', '0xEvaluatorEOA', // YOUR evaluator — could be a DAO, a multisig, or yourself '2026-06-01T00:00:00Z', 'Build and deploy landing page' ); // Evaluator calls complete() directly (no Evaluator contract needed) await client.jobs.complete(job.id, 'Work accepted — link verified');
Competitor Comparison
| Platform | Admin freeze surface | Admin evaluation surface | Admin upgrade surface |
|---|---|---|---|
| ClearPact | None (PAUSER renounced) | None (EVALUATOR renounced) | None (DEFAULT_ADMIN renounced) |
| Virtuals | Admin pause present | Platform arbitrates | Upgradeable, admin held |
| MeshLedger | Timelocked pause | DAO override possible | Timelocked upgrade |
| PayCrow | Admin pause present | Platform decides disputes | Proxy admin held |
Migration v1 → v2
v1 deprecation: v1 /api/escrow routes entered 7-day drainage on 2026-05-16. Sunset: 2026-05-23. Migrate all integrations to /api/job before sunset. After sunset, v1 routes will return HTTP 410 Gone.
The 7-day drainage period (v1 routes remain active) gives existing consumers time to migrate. After drainage, v1 routes are sunset.
Route Changes
| v1 route | v2 route | Notes |
|---|---|---|
POST /api/escrow | POST /api/job | 5 positional args: provider, evaluator, expiredAt, description, hook |
GET /api/escrow/:id | GET /api/job/:id | Returns Job struct (9 fields) |
POST /api/escrow/:id/fund | POST /api/job/:id/fund | Identical semantics |
POST /api/escrow/:id/settle | POST /api/job/:id/complete | complete() = approveResult (D7) |
POST /api/escrow/:id/cancel | POST /api/job/:id/reject | E3: symmetric refund from Funded/Submitted. Response: {"success":true, "job":{...}, "refund": null | {"status":"automatic", "note":"Symmetric refund..."}}. refund:null when previousStatus=Open; automatic when Funded/Submitted. Emits job.refunded with {jobId, reason:"reject_symmetric_refund", from_status}. |
| (new) | POST /api/job/:id/budget | setBudget with optional token + conditionRef |
| (new) | POST /api/job/:id/submit | Provider submits deliverable |
| (new) | POST /api/job/:id/claim-refund | Unconditional post-expiredAt refund |
Webhook Event Changes
| v1 event | v2 event | During drainage |
|---|---|---|
escrow.created | job.created | Both emitted |
escrow.funded | job.funded | Both emitted |
escrow.released | job.completed + job.paymentReleased | Both emitted |
escrow.disputed | job.rejected | Both emitted |
escrow.expired | job.expired | Both emitted |
| (new) | job.submitted | v2 only |
| (new) | job.refunded | v2 only |
| (new) | job.metadata | v2 only (ClearPactJobMetadata) |
| (new) | job.tokenSet | v2 only (ClearPactJobTokenSet) |
SDK Changes
SDK v0.3.0 (breaking in types, non-breaking in runtime during drainage):
client.jobs— new namespace with 8 lifecycle methods + 1 viewclient.escrow— deprecated alias (warns on construction), removed in v0.4.0JobStatus— exactly 6 values:Open|Funded|Submitted|Completed|Rejected|ExpiredJobtype — 9 ERC-8183 fields (replacesEscrowObject)ClearPact.contracts— static property with deployed addresses
How ClearPact Extends ERC-8183
Hooks (IACPHook)
Jobs can optionally specify a hook contract at createJob(). The hook receives beforeAction() / afterAction() callbacks on lifecycle transitions. Hooks must implement IERC165 and be whitelisted. Gas cap: 100k per hook call. claimRefund() is non-hookable — the unconditional refund guarantee cannot be blocked by a hook.
Per-Job Payment Tokens
By default, jobs use the global USDC address set at contract initialization (0x036CbD53842c5426634e7929541eC2318f3dCF7e on Base Sepolia). Callers can override the token per-job at setBudget() time by passing token. Emits job.tokenSet webhook event.
Conditional Reference Hash (conditionRef)
Pass conditionRef at setBudget() to attach a bytes32 conditional reference hash (e.g. IPFS CID of the conditions document). Emits ClearPactJobMetadata event (mapped to job.metadata webhook). This event is also emitted at createJob() with conditionRef=0 (Phase 2bis Fix #1).
Symmetric Refund on Reject (E3)
reject() implements a 3-path ACL: callable from Open, Funded, or Submitted state. When called from Funded or Submitted, it automatically returns funds to the client — no separate claimRefund() required. This is the symmetric refund guarantee (Phase 2bis Fix #3).
Webhooks
Receive real-time notifications when escrow events occur. Register a URL, choose which events to subscribe to, and ClearPact will POST signed JSON payloads to your endpoint.
Delivery guarantees: Every webhook is retried up to 3 times with exponential backoff (5 s → 30 s → 5 min). Each delivery attempt is logged in your webhook delivery history. Payloads are signed with HMAC-SHA256 so you can verify authenticity.
Register Webhook
Register a new webhook endpoint. Returns a one-time signing_secret for HMAC verification. Max 10 webhooks per API key.
REQUEST BODY
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | HTTPS endpoint that will receive webhook payloads |
| events | string[] | Yes | Event types to subscribe to. 19 available — see Webhook Events for the full list (12 v2 job.* + 7 v1 escrow.* during drainage). |
curl -X POST https://clearpact.polsia.app/api/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: cpk_live_YOUR_KEY" \
-d '{
"url": "https://your-app.com/webhooks/clearpact",
"events": [
"escrow.created",
"escrow.funded",
"escrow.settled",
"escrow.cancelled",
"escrow.expired"
]
}'
const res = await fetch("https://clearpact.polsia.app/api/webhooks", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "cpk_live_YOUR_KEY" }, body: JSON.stringify({ url: "https://your-app.com/webhooks/clearpact", events: [ "escrow.created", "escrow.funded", "escrow.settled", "escrow.cancelled", "escrow.expired" ] }) }); const { signing_secret } = await res.json(); // Store signing_secret securely — shown only once
res = requests.post(
"https://clearpact.polsia.app/api/webhooks",
headers={"X-API-Key": "cpk_live_YOUR_KEY"},
json={
"url": "https://your-app.com/webhooks/clearpact",
"events": [
"escrow.created",
"escrow.funded",
"escrow.settled",
"escrow.cancelled",
"escrow.expired"
]
}
)
data = res.json()
signing_secret = data["signing_secret"]
RESPONSE (201)
{
"success": true,
"webhook": {
"id": "wh_550e8400-e29b-41d4-a716-446655440000",
"url": "https://your-app.com/webhooks/clearpact",
"events": ["escrow.created", "escrow.funded", "escrow.settled", "escrow.cancelled", "escrow.expired"],
"created_at": "2026-04-18T10:00:00.000Z"
},
"signing_secret": "whsec_abc123...shown_only_once"
}
List Webhooks
List all webhooks registered under your API key.
curl
curl https://clearpact.polsia.app/api/webhooks \
-H "X-API-Key: cpk_live_YOUR_KEY"
RESPONSE (200)
{
"success": true,
"webhooks": [
{
"id": "wh_550e8400-e29b-41d4-a716-446655440000",
"url": "https://your-app.com/webhooks/clearpact",
"events": ["escrow.created", "escrow.funded", "escrow.settled", "escrow.cancelled", "escrow.expired"],
"created_at": "2026-04-18T10:00:00.000Z"
}
]
}
Delete Webhook
Remove a registered webhook. Stops all future deliveries to that URL.
curl
curl -X DELETE https://clearpact.polsia.app/api/webhooks/WEBHOOK_ID \
-H "X-API-Key: cpk_live_YOUR_KEY"
RESPONSE (200)
{
"success": true,
"message": "Webhook deleted"
}
GET /api/webhooks/:id/deliveries
List delivery attempts for a webhook. Returns attempt outcome, response status, and body preview for each delivery. Scoped to the authenticated key — you can only inspect your own webhooks.
QUERY PARAMETERS
| Param | Type | Default | Description |
|---|---|---|---|
| from | ISO 8601 | 7 days ago | Start of date range |
| to | ISO 8601 | now | End of date range |
| status | string | — | success | failed | pending |
| event_type | string | — | Filter by event: escrow.funded, etc. |
| limit | int | 50 | Max results (max 500) |
| cursor | string | — | Opaque cursor from next_cursor |
curl "https://clearpact.polsia.app/api/webhooks/WEBHOOK_UUID/deliveries?status=failed&limit=20" \ -H "X-API-Key: cpk_live_YOUR_KEY"
const res = await fetch( "https://clearpact.polsia.app/api/webhooks/WEBHOOK_UUID/deliveries?status=failed", { headers: { "X-API-Key": "cpk_live_YOUR_KEY" } } ); const { data, next_cursor } = await res.json(); // SDK equivalent: const { data, next_cursor } = await client.webhooks.deliveries("WEBHOOK_UUID", { status: "failed", event_type: "escrow.funded" });
import requests res = requests.get( "https://clearpact.polsia.app/api/webhooks/WEBHOOK_UUID/deliveries", headers={"X-API-Key": "cpk_live_YOUR_KEY"}, params={"status": "failed", "limit": 20} ) data = res.json()
RESPONSE (200)
{
"data": [
{
"delivery_id": "whd_550e8400-...",
"event_id": "evt_a1b2c3d4-...",
"event_type": "escrow.funded",
"attempt_number": 2,
"response_status": 200,
"response_body_preview": "{\"ok\":true}",
"duration_ms": null,
"attempted_at": "2026-05-05T09:11:25Z",
"next_attempt_at": null,
"final_status": "delivered"
}
],
"next_cursor": null
}
FINAL STATUS VALUES
| Status | Meaning |
|---|---|
delivered | Endpoint returned 2xx |
pending | In-flight or scheduled for retry |
failed | All retry attempts exhausted (3 total) |
Webhook Events
19 event types are available (12 v2 job events + 7 v1 escrow events during drainage). Subscribe to any combination when registering a webhook.
| Event name | Triggered by | Payload shape |
|---|---|---|
job.created | POST /api/job | full Job |
job.metadata | createJob (conditionRef=0) + setBudget (conditionRef≠0) | {jobId, conditionRef, description} |
job.tokenSet | setBudget non-default token | {jobId, token} |
job.providerSet | setProvider | full Job |
job.budgetSet | setBudget | full Job |
job.funded | fund | full Job |
job.submitted | submit | full Job |
job.completed | complete | full Job |
job.paymentReleased | complete post-transition | {jobId, amount, token} |
job.rejected | reject | full Job |
job.refunded | reject from Funded/Submitted OR claimRefund | {jobId, reason, from_status} |
job.expired | auto-expire OR claimRefund | full Job |
escrow.created | v1 dual-emit during drainage | full Job |
escrow.funded | v1 dual-emit during drainage | full Job |
escrow.released | v2 dual-emit at complete (renamed from escrow.settled) | full Job |
escrow.disputed | v2 dual-emit at reject | full Job |
escrow.expired | v1 dual-emit during drainage | full Job |
escrow.settled | legacy v1 — NOT emitted by v2 (registrable for backward-compat) | — |
escrow.cancelled | legacy v1 — NOT emitted by v2 (registrable for backward-compat) | — |
Payload Format
Every webhook delivery is an HTTP POST with a JSON body. The envelope is consistent across all event types:
{
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "escrow.created",
"created_at": "2026-04-18T10:00:00.000Z",
"data": { /* escrow object — same shape as GET /api/escrow/:id */ }
}
EXAMPLE: escrow.created
{
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "escrow.created",
"created_at": "2026-04-18T10:00:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"network": "testnet",
"conditions": [{"type": "task_completion"}],
"created_at": "2026-04-18T10:00:00.000Z"
}
}
EXAMPLE: escrow.funded
{
"id": "evt_b2c3d4e5-f6a7-8901-bcde-f12345678901",
"type": "escrow.funded",
"created_at": "2026-04-18T10:05:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "funded",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"network": "testnet",
"funded_at": "2026-04-18T10:05:00.000Z",
"tx_hashes": {"fund": "0xabc123..."},
"created_at": "2026-04-18T10:00:00.000Z"
}
}
EXAMPLE: escrow.settled
{
"id": "evt_c3d4e5f6-a7b8-9012-cdef-123456789012",
"type": "escrow.settled",
"created_at": "2026-04-18T10:10:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "settled",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"network": "testnet",
"settled_at": "2026-04-18T10:10:00.000Z",
"tx_hashes": {"fund": "0xabc123...", "settle": "0xdef456..."},
"created_at": "2026-04-18T10:00:00.000Z"
}
}
EXAMPLE: escrow.cancelled
{
"id": "evt_d4e5f6a7-b8c9-0123-defa-234567890123",
"type": "escrow.cancelled",
"created_at": "2026-04-18T10:15:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"network": "testnet",
"cancelled_at": "2026-04-18T10:15:00.000Z",
"created_at": "2026-04-18T10:00:00.000Z"
}
}
EXAMPLE: escrow.expired
{
"id": "evt_e5f6a7b8-c9d0-1234-efab-345678901234",
"type": "escrow.expired",
"created_at": "2026-04-18T12:00:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "expired",
"payer": "0xAlice",
"payee": "0xBob",
"amount": 500,
"token": "USDC",
"network": "testnet",
"expires_at": "2026-04-18T12:00:00.000Z",
"tx_hashes": {"refund": "0x789abc..."},
"created_at": "2026-04-18T10:00:00.000Z"
}
}
Signature Verification
Every webhook payload includes an X-ClearPact-Signature header containing an HMAC-SHA256 hex digest of the raw request body. Always verify this before processing.
// Node.js verification example const crypto = require('crypto'); function verifyWebhook(body, signature, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret) .update(body, 'utf8') .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // In your Express handler: app.post('/webhooks/clearpact', (req, res) => { const sig = req.headers['x-clearpact-signature']; if (!verifyWebhook(JSON.stringify(req.body), sig, SIGNING_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process the event console.log(req.body.type, req.body.data.id); res.status(200).json({ received: true }); });
Important: Your endpoint must return a 2xx status within 10 seconds. Non-2xx responses or timeouts trigger a retry. After 3 failed attempts, the delivery is marked as failed in the delivery log.
JavaScript / TypeScript SDK
The official ClearPact SDK wraps the REST API in a clean, fully-typed client. Zero dependencies — uses native fetch.
Install
npm install clearpact
<script src="https://clearpact.polsia.app/sdk"></script>
<!-- Exposes window.ClearPact and window.ClearPactError globally -->
Quick Start
const { ClearPact } = require('clearpact');
const client = new ClearPact({
apiKey: 'cpk_live_...',
network: 'testnet', // 'testnet' | 'mainnet'
});
// Create an escrow
const { escrow } = await client.escrow.create({
payer: '0xPayerWallet',
payee: '0xPayeeWallet',
amount: 100, // USDC
conditions: [{ type: 'task_completion' }],
});
console.log(escrow.id, escrow.status);
// "3f2ac1d0-..." "pending_funding"
// Fund it
await client.escrow.fund(escrow.id, { tx_hash: '0x...' });
// Settle
await client.escrow.settle(escrow.id, {
verifications: { task_completion: { completed: true } },
});
import { ClearPact } from 'clearpact';
const client = new ClearPact({ apiKey: 'cpk_live_...' });
const { escrow } = await client.escrow.create({
payer: '0xPayerWallet',
payee: '0xPayeeWallet',
amount: 50,
});
import { ClearPact, ClearPactError } from 'clearpact';
import type { EscrowObject } from 'clearpact/types';
const client = new ClearPact({ apiKey: 'cpk_live_...' });
try {
const { escrow }: { escrow: EscrowObject } =
await client.escrow.create({
payer: '0xPayerWallet',
payee: '0xPayeeWallet',
amount: 100,
});
console.log(escrow.id);
} catch (err) {
if (err instanceof ClearPactError) {
console.error(err.status, err.message);
}
}
Escrow Methods
| Method | Description |
|---|---|
client.escrow.create(params) | Create a new escrow |
client.escrow.get(id) | Get status + audit trail |
client.escrow.fund(id, params?) | Record a funding event |
client.escrow.settle(id, params?) | Manually settle conditions |
client.escrow.cancel(id, params?) | Cancel or refund |
// ERC-8004 auto-settlement — link escrow to an external job
const { escrow } = await client.escrow.create({
payer: '0xPayer',
payee: '0xPayee',
amount: 500,
external_job_id: 'job_abc123', // POST /api/validation/submit triggers auto-settle
conditions: [{ type: 'task_completion' }],
});
x402 Methods
| Method | Description |
|---|---|
client.x402.verify(payload, opts?) | Verify an x402 payment authorization |
client.x402.settle(params?) | Settle a verified payment on-chain |
client.x402.health() | Facilitator health + chain config |
client.x402.listTransactions(filters?) | List x402 transaction records |
client.x402.getTransaction(id) | Get a single transaction |
Webhooks
| Method | Description |
|---|---|
client.webhooks.create(params) | Register a webhook URL + events |
client.webhooks.list() | List webhooks for this key |
client.webhooks.delete(id) | Remove a webhook |
client.webhooks.deliveries(id, opts?) | List delivery history for a webhook |
// Register a webhook
const { signing_secret } = await client.webhooks.create({
url: 'https://your-app.com/webhooks/clearpact',
event_types: ['escrow.created', 'escrow.funded', 'escrow.settled', 'escrow.cancelled', 'escrow.expired'],
});
// Store signing_secret — shown only once. Verify X-ClearPact-Signature on incoming events.
// List webhooks
const { webhooks } = await client.webhooks.list();
// Inspect delivery history (debug failed deliveries)
const { data } = await client.webhooks.deliveries(webhooks[0].id, {
status: 'failed',
event_type: 'escrow.funded',
limit: 20,
});
console.log(data[0].response_body_preview);
// Remove a webhook
await client.webhooks.delete(webhookId);
Keys / Observability
| Method | Description |
|---|---|
client.keys.usage(keyId, opts?) | List API usage logs for a key |
// Inspect your own API usage (last 7 days)
const { data, summary } = await client.keys.usage('cp_YOUR_KEY_ID', {
from: '2026-05-01T00:00:00Z',
status: '4xx',
limit: 100,
});
console.log(`${summary.count} total requests`);
console.log('By status:', summary.by_status_class);
// { '2xx': 143, '4xx': 7 }
// Paginate with cursor
let cursor = null;
do {
const page = await client.keys.usage('cp_YOUR_KEY_ID', { cursor, limit: 100 });
for (const req of page.data) {
console.log(req.path, req.status_code, req.latency_ms + 'ms');
}
cursor = page.next_cursor;
} while (cursor);
Error Handling
All API errors throw a ClearPactError with a typed shape:
import { ClearPact, ClearPactError } from 'clearpact';
try {
await client.escrow.get('not-a-real-id');
} catch (err) {
if (err instanceof ClearPactError) {
err.status; // 400
err.message; // "Invalid escrow ID format"
err.error; // machine-readable code (if present)
err.errors; // array of validation messages (if present)
err.raw; // full response body
}
}
Changelog
Major milestones shipped, newest first.
Phase 5 v2: Full ERC-8183 job lifecycle API (POST /api/job and 7 lifecycle sub-routes). 4 Phase 2bis fixes (E1: ClearPactJobMetadata re-emit; E2: global paymentToken; E3: 3-path reject + symmetric refund; P8: EvaluatorRejected). 5-renounce immutability sequence executed on both contracts. SDK v0.3.0 with new JobStatus enum (6 values), client.jobs namespace, and deprecated client.escrow alias. Dual-emit webhook during 7-day v1 drainage period. Migration guide at #migration-v1-to-v2.
Two new read-only endpoints: GET /api/keys/:id/usage returns per-request logs with status, latency, and path breakdown. GET /api/webhooks/:id/deliveries returns full delivery history with attempt outcomes and response previews. Both support cursor pagination, date ranges, and status filters. SDK v0.2.0 adds client.keys.usage() and client.webhooks.deliveries().
Complete lifecycle reference with 10 documented states (5 non-terminal, 5 terminal), triggers, transitions, and corresponding webhook events. Full state machine diagram at #status-values.
Expanded webhook docs covering escrow.created, escrow.funded, escrow.settled, escrow.cancelled, and escrow.expired, each with full payload examples and HMAC signature verification guide.
cpk_live_All production API keys now use the cpk_live_ prefix. SDK, docs, and playground updated throughout.
Full-featured SDK with typed methods for escrow lifecycle, x402 protocol, and webhook management. Zero-dependency. Install via npm install clearpact.
Funded escrows past their deadline are automatically detected, refunded on-chain, and fire an escrow.expired webhook. No manual intervention required.
GET /api/escrow supports filtering by status, network, and date range with cursor-based pagination and sort. See List Escrows.
POST /api/x402/settle closes the x402 payment loop on Base Sepolia. Integrators can now use ClearPact as the settlement layer in x402 agent-to-agent payment flows.
Real-time event delivery for all 5 escrow lifecycle events. HMAC-SHA256 signing, 3 retries with exponential backoff (5s → 30s → 5min), non-blocking dispatch.
Create, list, revoke, and rotate API keys. Up to 5 keys per email. Zero-downtime key rotation supported. Rate limited to 60 req/min per key.
Initial public release. X-API-Key authentication, create/fund/settle/cancel escrow on Base Sepolia, 6 condition types, ERC-8004 auto-settlement.