Base URL: https://api.escro.ai
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.
Example (Node.js)
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,
};
Example (Python)
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,
}
Example (cURL)
# Assuming $WALLET, $SIGNATURE, $TIMESTAMP are set
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 of code values.
Rate Limits
| Endpoint | Limit |
|---|---|
GET /v1/escrows |
6 req/min |
GET /v1/escrows/:address |
10 req/min |
POST /v1/escrows |
Default |
POST /v1/escrows/:address/* |
Default |
GET /rep/:wallet |
10 req/min |
GET /rep |
10 req/min |
GET /health |
Unlimited |
When rate-limited, the API returns HTTP 429 with code RATE_LIMITED.
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"
}
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
}
Example:
curl "https://api.escro.ai/v1/escrows?state=FUNDED&limit=10"
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: EscrowAccount (see Types)
Response 404:
{ "error": "Escrow not found", "code": "ESCROW_NOT_FOUND" }
Example:
curl "https://api.escro.ai/v1/escrows/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
POST /v1/escrows
Create a new escrow with a task specification.
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) |
deadlineUnix |
integer | Yes | Unix timestamp (seconds) when the escrow deadline expires. Must be ≥ now + 60s and ≤ now + 30 days. |
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.
Example:
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 '{
"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,
"deadlineUnix": 1712016000,
"assignedWorker": "9aE476sH92Vz7DMPyq5WLPkrKWivxuTFhN7bvj7waxQr"
}'
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 |
Example:
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 |
Example:
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.
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 |
Example:
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 |
Example:
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 |
Example:
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"
}'
GET /rep/:wallet
EscroRep per-wallet reputation, derived entirely from on-chain escrow events. Exposes raw components only (completion rate, dispute rate, avg delivery time, settled volume, wallet age) — there is deliberately no single composite score; consumers compute their own weighting.
Auth: None (public)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
wallet |
string | Base58 Solana wallet address |
Response 200: raw worker-side and buyer-side components plus walletAgeSeconds (derived at serve time). An unknown wallet returns the same shape with zeroed counts and null rates/age (never a 404). μUSDC volumes are decimal strings.
{
"wallet": "9aE476sH92Vz7DMPyq5WLPkrKWivxuTFhN7bvj7waxQr",
"asWorker": {
"tasksClaimed": 12,
"completions": 11,
"completionsViaRelease": 9,
"completionsViaTimeout": 2,
"nonDeliveries": 1,
"completionRate": 0.9166666666666666,
"disputes": { "total": 0, "won": 0, "lost": 0 },
"disputeRate": 0,
"volumeSettledUsdc": "55000000",
"avgCreateToClaimSeconds": 100,
"avgClaimToSubmitSeconds": 200
},
"asBuyer": {
"escrowsCreated": 0,
"timeoutReleases": 0,
"disputes": { "total": 0, "won": 0, "lost": 0 },
"disputeRate": null,
"volumeSettledUsdc": "0"
},
"firstActivityAt": 1711929600,
"lastActivityAt": 1712016000,
"walletAgeSeconds": 86400
}
Responses carry Cache-Control: public, max-age=30 and a weak ETag validator keyed on the wallet’s raw aggregates (not the serve-time clock). Send it back as If-None-Match to revalidate: the API replies 304 Not Modified while the aggregates are unchanged and 200 with a new ETag once they change, so a stale cache is invalidated the moment the wallet’s on-chain history moves. Metric semantics are defined in ARCHITECTURE §9.
Example:
curl "https://api.escro.ai/rep/9aE476sH92Vz7DMPyq5WLPkrKWivxuTFhN7bvj7waxQr"
GET /rep
EscroRep bulk export for anyone building ranking layers on top (ARCHITECTURE §9): every indexed wallet’s aggregates, each wallet exactly once. Same raw components only contract as GET /rep/:wallet, with deliberately no composite score.
Auth: None (public)
Response 200: wallets is an array of the same per-wallet shape returned by GET /rep/:wallet (raw worker/buyer components plus walletAgeSeconds); count is wallets.length; generatedAt is the serving clock (unix seconds) the age values were derived at.
{
"wallets": [
{
"wallet": "9aE476sH92Vz7DMPyq5WLPkrKWivxuTFhN7bvj7waxQr",
"asWorker": { "tasksClaimed": 12, "completionRate": 0.9166666666666666, "volumeSettledUsdc": "55000000" },
"asBuyer": { "escrowsCreated": 0, "volumeSettledUsdc": "0" },
"firstActivityAt": 1711929600,
"lastActivityAt": 1712016000,
"walletAgeSeconds": 86400
}
],
"count": 1,
"generatedAt": 1712016000
}
Like the per-wallet endpoint, the export carries Cache-Control: public, max-age=30 and a weak ETag (keyed on all wallets’ aggregates); an If-None-Match replay returns 304 until any wallet’s aggregates change, then 200 with a fresh ETag.
Example:
curl "https://api.escro.ai/rep"
Types
EscrowAccount
Full on-chain representation of an escrow, enriched with off-chain metadata.
{
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"maker": "BuyerPubkey...",
"taker": "WorkerPubkey...",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "10000000",
"state": "FUNDED",
"taskSpecHash": "a1b2c3...",
"taskSpecUri": "ipfs://Qm...",
"oracle": "OraclePubkey...",
"oracleFeeBps": 500,
"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 (0–10000) |
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 |
Generated on 2026-08-13