REST API Reference

Complete HTTP API reference for escro.ai — authenticate with wallet signatures, create escrows, and manage the full lifecycle via curl or any HTTP client.

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 FUNDEDIN_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_PROGRESSSUBMITTED.

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