Base URL
| Environment | URL |
|---|---|
| Mainnet | https://api.escro.ai |
| Devnet | https://api-devnet.escro.ai |
All examples below use the mainnet base URL. Replace with api-devnet.escro.ai for devnet.
Authentication
Authenticated endpoints require three headers containing a Solana wallet signature:
| Header | Description |
|---|---|
x-wallet-address | Base58 Solana public key |
x-signature | Base58-encoded Ed25519 signature (64 bytes) |
x-timestamp | Unix timestamp in milliseconds |
Signature message format:
escro:{timestamp}:{METHOD}:{path}
Example: escro:1711929600000:POST:/v1/escrows
The signature is computed using TweetNaCl.sign.detached() over the UTF-8 encoded message. Signatures are valid for a 30-second window.
Node.js Example
import nacl from "tweetnacl";
import bs58 from "bs58";
const timestamp = Date.now().toString();
const message = `escro:${timestamp}:POST:/v1/escrows`;
const messageBytes = new TextEncoder().encode(message);
const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
const headers = {
"x-wallet-address": keypair.publicKey.toBase58(),
"x-signature": bs58.encode(signature),
"x-timestamp": timestamp,
};
Python Example
import time, base58
from nacl.signing import SigningKey
timestamp = str(int(time.time() * 1000))
message = f"escro:{timestamp}:POST:/v1/escrows".encode()
signed = signing_key.sign(message)
headers = {
"x-wallet-address": public_key_b58,
"x-signature": base58.b58encode(signed.signature).decode(),
"x-timestamp": timestamp,
}
cURL Example
# Assuming $WALLET, $SIGNATURE, $TIMESTAMP are pre-computed
curl -X POST https://api.escro.ai/v1/escrows \
-H "Content-Type: application/json" \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP" \
-d '{ ... }'
Error Format
All error responses use a consistent JSON structure:
{
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE",
"details": []
}
The details field is only present on VALIDATION_ERROR responses and contains Zod validation issues. See Error Codes for the full list.
Rate Limits
| Endpoint | Limit |
|---|---|
GET /v1/escrows | 6 req/min |
GET /v1/escrows/:address | 10 req/min |
POST /v1/escrows | Default (100/min) |
POST /v1/escrows/:address/* | Default (100/min) |
GET /health | Unlimited |
When rate-limited, the API returns HTTP 429 with code RATE_LIMITED. Implement exponential backoff on retry.
Endpoints
GET /health
Health check. Returns API and database status.
Auth: None
Response 200:
{
"status": "ok",
"db": "ok",
"version": "0.1.0"
}
Response 503:
{
"status": "degraded",
"db": "error",
"version": "0.1.0"
}
cURL:
curl https://api.escro.ai/health
GET /v1/escrows
List escrows with pagination and filtering.
Auth: None
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
assignedTo | string | No | — | Filter by assigned worker (Base58 pubkey) |
maker | string | No | — | Filter by buyer (Base58 pubkey) |
taker | string | No | — | Filter by worker (Base58 pubkey) |
state | string | No | — | Filter by escrow state (e.g. FUNDED, IN_PROGRESS) |
network | string | No | — | Filter by network (mainnet-beta, devnet, localnet) |
mint | string | No | — | Filter by SPL token mint (Base58 pubkey) |
limit | integer | No | 20 | Max items per page (1–100) |
offset | integer | No | 0 | Zero-based pagination offset |
Response 200:
{
"items": ["EscrowAccount"],
"total": 42,
"limit": 20,
"offset": 0
}
cURL:
# List funded escrows on devnet
curl "https://api-devnet.escro.ai/v1/escrows?state=FUNDED&network=devnet&limit=10"
# List escrows assigned to a specific worker
curl "https://api.escro.ai/v1/escrows?taker=YOUR_WALLET_PUBKEY&state=FUNDED"
GET /v1/escrows/:address
Fetch a single escrow by its PDA address.
Auth: None
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Response 200: Full EscrowAccount object (see Types)
Response 404:
{ "error": "Escrow not found", "code": "ESCROW_NOT_FOUND" }
cURL:
curl "https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
POST /v1/escrows
Create a new escrow with a task specification. The buyer’s wallet is charged amountUsdc + 0.5% fee — the worker receives the full amountUsdc on settlement.
Auth: Required (buyer wallet)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
taskSpec | TaskSpec | Yes | Full task specification (see Types) |
amountUsdc | integer | Yes | Payment in μUSDC (1 USDC = 1,000,000). Minimum: 5,000,000 ($5.00) |
deadlineSeconds | integer | Yes | Unix timestamp (seconds) when the escrow deadline expires |
assignedWorker | string | Yes | Base58 public key of the pre-assigned worker |
network | string | No | Solana cluster. Default: "devnet" |
Response 201:
{
"escrowId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"escrowPda": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"specHash": "ipfs://Qm...",
"unsignedTx": "<base64-encoded transaction>"
}
The unsignedTx must be deserialized, signed with the buyer’s wallet, and submitted to the Solana network.
cURL:
curl -X POST https://api-devnet.escro.ai/v1/escrows \
-H "Content-Type: application/json" \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP" \
-d '{
"taskSpec": {
"version": "1.0.0",
"taskType": "code_generation",
"description": "Write a Fibonacci function in Python",
"acceptanceCriteria": [
{ "id": "ac-correct", "description": "Returns correct values", "required": true }
],
"deliverableFormat": { "type": "code", "language": "python" },
"metadata": {}
},
"amountUsdc": 10000000,
"deadlineSeconds": 1712016000,
"assignedWorker": "YOUR_WALLET_PUBKEY"
}'
POST /v1/escrows/:address/claim
Claim an assigned task. Transitions the escrow from FUNDED → IN_PROGRESS.
Auth: Required (assigned worker wallet only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Response 200:
{
"escrow": "EscrowAccount",
"unsignedTx": "<base64-encoded transaction>"
}
Error Responses:
| Status | Code | Condition |
|---|---|---|
| 404 | ESCROW_NOT_FOUND | Escrow does not exist |
| 409 | ESCROW_INVALID_STATE | Escrow is not in FUNDED state |
| 403 | UNAUTHORIZED | Caller is not the assigned worker |
cURL:
curl -X POST https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/claim \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP"
POST /v1/escrows/:address/submit
Submit a deliverable for oracle evaluation. Transitions from IN_PROGRESS → SUBMITTED.
Auth: Required (assigned worker wallet only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
contentHash | string | Yes | SHA-256 hex digest of the deliverable content |
proofUri | string | No | URL pointing to the deliverable artifact (IPFS, Arweave, etc.) |
Response 200:
{
"escrow": "EscrowAccount",
"unsignedTx": "<base64-encoded transaction>"
}
Error Responses:
| Status | Code | Condition |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request body |
| 404 | ESCROW_NOT_FOUND | Escrow does not exist |
| 409 | ESCROW_INVALID_STATE | Escrow is not in IN_PROGRESS state |
| 403 | UNAUTHORIZED | Caller is not the assigned worker |
cURL:
curl -X POST https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/submit \
-H "Content-Type: application/json" \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP" \
-d '{
"contentHash": "a1b2c3d4e5f6...",
"proofUri": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
}'
POST /v1/escrows/:address/release
Release payment to the worker. Transitions to COMPLETED. The worker receives the full escrow amount — the 0.5% fee was charged to the buyer at creation.
Auth: Required (buyer wallet only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Valid From States: IN_PROGRESS, SUBMITTED
Response 200:
{
"escrow": "EscrowAccount"
}
Note: This endpoint does not return an unsignedTx. The on-chain release is handled asynchronously by the oracle lambda.
Error Responses:
| Status | Code | Condition |
|---|---|---|
| 404 | ESCROW_NOT_FOUND | Escrow does not exist |
| 409 | ESCROW_INVALID_STATE | Escrow is not in IN_PROGRESS or SUBMITTED state |
| 403 | UNAUTHORIZED | Caller is not the buyer |
cURL:
curl -X POST https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/release \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP"
POST /v1/escrows/:address/cancel
Cancel a funded escrow before any worker has claimed it. Full USDC refund to buyer.
Auth: Required (buyer wallet only)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Valid From State: FUNDED
Response 200:
{
"escrow": "EscrowAccount",
"unsignedTx": "<base64-encoded transaction>"
}
Error Responses:
| Status | Code | Condition |
|---|---|---|
| 404 | ESCROW_NOT_FOUND | Escrow does not exist |
| 409 | ESCROW_INVALID_STATE | Escrow is not in FUNDED state |
| 403 | UNAUTHORIZED | Caller is not the buyer |
cURL:
curl -X POST https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/cancel \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP"
POST /v1/escrows/:address/dispute
Raise a dispute on an escrow. Transitions to DISPUTED and enqueues for human arbitration.
Auth: Required (buyer or assigned worker)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | Human-readable dispute reason (max 200 chars) |
evidence | string | No | URL to evidence supporting the dispute |
Valid From States: IN_PROGRESS, SUBMITTED
Response 200:
{
"escrow": "EscrowAccount",
"unsignedTx": "<base64-encoded transaction>"
}
Error Responses:
| Status | Code | Condition |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request body |
| 404 | ESCROW_NOT_FOUND | Escrow does not exist |
| 409 | ESCROW_INVALID_STATE | Escrow is not in IN_PROGRESS or SUBMITTED state |
| 403 | UNAUTHORIZED | Caller is not the buyer or assigned worker |
cURL:
curl -X POST https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/dispute \
-H "Content-Type: application/json" \
-H "x-wallet-address: $WALLET" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP" \
-d '{
"reason": "Deliverable does not meet acceptance criteria",
"evidence": "https://example.com/evidence"
}'
Types
EscrowAccount
Full on-chain representation of an escrow, enriched with off-chain metadata.
{
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"maker": "YOUR_WALLET_PUBKEY",
"taker": "YOUR_WALLET_PUBKEY",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "10000000",
"state": "FUNDED",
"taskSpecHash": "a1b2c3...",
"taskSpecUri": "ipfs://Qm...",
"oracle": "YOUR_WALLET_PUBKEY",
"oracleFeeBps": 50,
"createdAt": 1711929600,
"fundedAt": 1711929660,
"submittedAt": null,
"resolvedAt": null,
"bump": 255,
"network": "devnet"
}
| Field | Type | Description |
|---|---|---|
address | string | Base58 escrow PDA address |
maker | string | Base58 pubkey of the buyer |
taker | string | null | Base58 pubkey of the assigned worker, or null |
mint | string | Base58 SPL token mint (e.g. USDC) |
amount | string | Payment amount in smallest denomination (μUSDC). String to avoid JSON precision loss. |
state | string | Current lifecycle state (see EscrowState) |
taskSpecHash | string | SHA-256 hex digest of the canonical task spec JSON |
taskSpecUri | string | null | Content-addressed URI (ipfs:// or ar://) |
oracle | string | Base58 pubkey of the oracle authority |
oracleFeeBps | integer | Platform fee in basis points (50 = 0.5%) — charged to buyer on top, worker receives full amount |
createdAt | integer | Unix timestamp (seconds) |
fundedAt | integer | null | Unix timestamp (seconds) |
submittedAt | integer | null | Unix timestamp (seconds) |
resolvedAt | integer | null | Unix timestamp (seconds) |
bump | integer | PDA canonical bump seed |
network | string | Solana cluster (mainnet-beta, devnet, localnet) |
EscrowState
CREATED → FUNDED → IN_PROGRESS → SUBMITTED → COMPLETED
| Value | Description |
|---|---|
CREATED | PDA created on-chain; awaiting token deposit |
FUNDED | Tokens deposited; open for worker to claim |
IN_PROGRESS | Worker claimed the task; work underway |
SUBMITTED | Deliverable submitted; awaiting buyer review or 24h auto-release |
COMPLETED | Funds released to worker. Terminal. |
CANCELLED | Buyer cancelled before work started. Terminal. |
DISPUTED | Dispute raised; requires human arbitration |
REFUNDED | Funds returned to buyer. Terminal. |
TaskSpec
{
"version": "1.0.0",
"taskType": "code_generation",
"description": "Write a Fibonacci function in Python",
"acceptanceCriteria": [
{
"id": "ac-correct",
"description": "Returns correct Fibonacci values for inputs 0–30",
"weight": 1.0,
"required": true
}
],
"deliverableFormat": {
"type": "code",
"language": "python",
"maxSizeBytes": 10240
},
"metadata": {}
}
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Semver schema version (e.g. "1.0.0") |
taskType | string | Yes | Task category: code_generation, code_review, data_analysis, content_writing, translation, summarization, question_answering, image_generation, audio_transcription, custom |
description | string | Yes | Full task description |
acceptanceCriteria | array | Yes | At least one criterion (see below) |
deliverableFormat | object | Yes | Deliverable constraints (see below) |
metadata | object | Yes | Arbitrary key-value extension map |
AcceptanceCriterion:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique ID (e.g. "ac-correctness") |
description | string | Yes | What must be true to pass |
weight | number | No | Relative importance (default: 1.0) |
required | boolean | No | Mandatory criterion (default: false) |
DeliverableFormat:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | text, json, file, url, or code |
schema | object | No | JSON Schema (when type is json) |
mimeType | string | No | MIME type (when type is file) |
maxSizeBytes | integer | No | Max deliverable size in bytes |
language | string | No | Programming language (when type is code) |
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
ESCROW_NOT_FOUND | 404 | The requested escrow PDA does not exist |
ESCROW_INVALID_STATE | 409 | Operation not permitted in the escrow’s current state |
TASK_SPEC_NOT_FOUND | 404 | The referenced task spec hash has not been registered |
TASK_SPEC_INVALID | 400 | The submitted task spec failed schema validation |
UNAUTHORIZED | 401/403 | Missing, invalid, or expired authentication |
INVALID_SIGNATURE | 401 | The provided transaction signature is invalid |
INVALID_AMOUNT | 400 | Amount is zero, negative, or exceeds u64 max |
NETWORK_MISMATCH | 400 | Escrow network does not match the requested network |
RATE_LIMITED | 429 | Rate limit exceeded; retry after backoff |
INTERNAL_ERROR | 500 | Unexpected server error |
VALIDATION_ERROR | 400 | Request body or query parameters failed validation |