Skip to content

About

Node.js + Express + TypeScript backend for Soroban milestone escrow

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Escrow Backend

Express + TypeScript backend for the Soroban milestone escrow platform.

Tech Stack

  • Node.js + Express
  • TypeScript
  • Stellar SDK
  • Soroban RPC

Table of Contents

Setup

npm install
cp .env.example .env  # configure required production / local keys
npm run dev

Architecture

Webhook idempotency

Webhooks are delivered at least once, so receivers must tolerate duplicates. Each POST body includes an event_id, and each request carries:

  • X-Milesto-Event-Id: stable per on-chain event (the RPC event id; events indexed before it was stored use <contract_id>:<ledger_sequence>:<event_type>). Dedupe on this.
  • X-Milesto-Delivery-Id: UUID per logical delivery (event to subscriber), constant across all of its retries, including those made by the background retry sweep.
  • X-Milesto-Delivery-Attempt: the attempt number, starting at 1.

See docs/webhooks.md for the full contract.

API Documentation

The complete OpenAPI specification is maintained in docs/openapi.yaml.

API Versioning

New integrations should use /api/v1. Existing /api routes remain available for backwards compatibility and return Deprecation: true.

Authentication Model

  • Public endpoints: Endpoints that simulate public contract reads (/api/jobs/:contractId, /api/jobs/by-wallet/:address, /api/jobs/:contractId/whitelist, etc.) or construct unsigned transactions for the caller's wallet to sign require no API key.
  • Admin endpoints: /api/admin/* requires an admin key passed via x-api-key: <ADMIN_API_KEY> or Authorization: Bearer <ADMIN_API_KEY>.
  • Client endpoints: When API_KEYS is configured, client integrations pass their key via x-api-key: <API_KEY>.

API Endpoints

System & Health

GET /health

Check server and contract configuration health.

  • Auth: None (Public)
  • Response: { "status": "ok", "contract": "<CONTRACT_ID>" }

Jobs API

GET /api/jobs/:contractId

Get current job state simulated directly from the Soroban contract.

  • Auth: None (Public read)
  • Rate Limit: 30 requests / minute (JOB_CONTRACT_RATE_MAX)
  • Params: contractId (Stellar contract C... address)

GET /api/jobs/by-wallet/:address

Get indexed jobs associated with a participant wallet address (as client, freelancer, or arbiter).

  • Auth: None (Public read)
  • Rate Limit: 30 requests / minute (BY_WALLET_RATE_LIMIT_MAX)
  • Params: address (Stellar G... account address)
  • Query: page (default: 1), limit (default: 20, max: 100)

POST /api/jobs/create-job-draft

Validate and persist an off-chain job escrow draft before deploying on-chain.

  • Auth: None (Public)
  • Rate Limit: 5 requests / minute (CREATE_JOB_DRAFT_RATE_MAX)
  • Body: { client, freelancer, arbiter, token, autoReleaseDays?, milestones: [{ amount, description? }] }

GET /api/jobs/drafts/:id

Fetch a job draft saved by create-job-draft (#667).

  • Auth: None (Public)
  • Response: the stored draft, or 404 when no draft has that ID

GET /api/jobs/query and POST /api/jobs/query

Execute read-only simulation queries against a Soroban contract.

  • Auth: None (Public)
  • Rate Limit: 30 requests / minute (JOB_CONTRACT_RATE_MAX)

GET /api/jobs/whitelisted-tokens

Frontend query alias to fetch accepted token contract IDs for a contract.

  • Auth: None (Public)
  • Query: contractId (required)

GET /api/jobs/:contractId/history

Fetch indexed contract lifecycle events (funded, milestone approved, completed, etc.).

  • Auth: None (Public)
  • Query: page (default: 1), limit (default: 10, max: 100)

GET /api/jobs/:contractId/fee-preview

Calculate the platform fee breakdown for a specified amount.

  • Auth: None (Public)
  • Query: amount (required)

GET /api/jobs/:contractId/whitelist

Get the list of whitelisted payment token contract addresses accepted by the escrow contract.

  • Auth: None (Public)
  • Rate Limit: 20 requests / minute (JOB_WHITELIST_RATE_MAX)

POST /api/jobs/:contractId/whitelist/update

Build an unsigned transaction to add or remove whitelisted tokens.

  • Auth: None (Public; wallet signature required on-chain)
  • Rate Limit: 10 requests / minute (JOB_WHITELIST_UPDATE_RATE_MAX)
  • Body: { token, action: "add" | "remove", adminAddress } or { addresses: [...] }

POST /api/jobs/build-tx

Build an unsigned transaction XDR for authorized contract entrypoints.

  • Auth: None (Public; caller wallet signs)
  • Rate Limit: 20 requests / minute (BUILD_TX_RATE_MAX)
  • Body: { contractId, method, args, sourceAddress }; each arg is { type, value } with type one of address, i128, u32, u64, bool, bytes (hex string) or vec
  • Response: { success: true, data: { xdr } }

POST /api/jobs/:contractId/milestones/:index/partial-release

Build an unsigned transaction XDR for partial milestone release.

  • Auth: None (Public; caller wallet signs)
  • Rate Limit: 10 requests / minute (PARTIAL_RELEASE_RATE_MAX)
  • Params: contractId, index (milestone index)
  • Body: { amount, sourceAddress }
  • Response: { success: true, data: { xdr } }

GET /api/jobs/:contractId/milestones/:index/time-remaining (and /auto-release)

Get seconds remaining until an unapproved milestone is eligible for auto-release. Also available via alias route GET /api/jobs/:contractId/milestones/:index/auto-release.

  • Auth: None (Public)
  • Rate Limit: 60 requests / minute (TIME_REMAINING_RATE_MAX)
  • Params: contractId, index (milestone index)

POST /api/jobs/:contractId/milestones/:index/claim-auto-release

Build an unsigned transaction XDR to claim an auto-released milestone.

  • Auth: None (Public; caller wallet signs)
  • Rate Limit: 10 requests / minute (CLAIM_AUTO_RELEASE_RATE_MAX)
  • Params: contractId, index
  • Body: { sourceAddress }
  • Response: { success: true, data: { xdr } }

POST /api/jobs/submit

Submit a signed transaction to the Stellar network and return execution status.

  • Auth: None (Public)
  • Rate Limit: 5 requests / minute (SUBMIT_RATE_MAX)
  • Body: { signedXdr, sourceAddress? }

Admin API

GET /api/admin/indexer-status

Retrieve indexer health, current ledger, event counters, uptime, and reconciliation status.

  • Auth: Admin API Key (x-api-key: <ADMIN_API_KEY> or Bearer)
  • Response: { success: true, data: { lastIndexedLedger, totalEvents, uptimeSeconds, lastEventAt, eventsByType, skippedLedgerRange, undecodableEvents, reconciliation, failedDeliveries, deadDeliveries, consecutiveFailures, lastSuccessfulPollAt, pollIntervalMs, ledgerLag } } (ledgerLag is null when the RPC head cannot be read)

GET /api/admin/metrics

Snapshot of the indexer metrics collector.

  • Auth: Admin API Key
  • Response: { success: true, metrics }; 500 if the metrics cannot be collected

GET /api/admin/webhook-subscriptions

Every webhook subscription, plus the active and disabled subsets. A subscription is auto-disabled after WEBHOOK_DISABLE_AFTER consecutive failed deliveries (#701); subscribing again re-enables it.

  • Auth: Admin API Key
  • Response: { success: true, subscriptions, active, disabled }

POST /api/admin/backfill

Import a historical ledger range in the background (#703). One backfill runs at a time.

  • Auth: Admin API Key
  • Body: { startLedger, endLedger, contractId? } (inclusive range; contractId limits the import to one contract)
  • Response: 202 with the job ID; 409 while another backfill is running; 400 for an invalid range

GET /api/admin/backfill

Status of the current or last backfill: running, done or failed, with eventsFound / eventsImported, or { status: "idle" }.

  • Auth: Admin API Key

GET /api/admin/contracts

List the contracts the indexer monitors, with label, active flag, ledger pointer and start ledger (#659).

  • Auth: Admin API Key

POST /api/admin/contracts

Register a contract for indexing (#659).

  • Auth: Admin API Key
  • Body: { contractId, label?, startLedger? }
  • Response: 201 with the registered contract

DELETE /api/admin/contracts/:contractId

Stop indexing a contract (#659).

  • Auth: Admin API Key

Webhooks API

POST /api/webhooks/subscribe

Register a webhook URL to receive real-time notifications for contract events.

  • Auth: None (Public)
  • Body: { contract_id, webhook_url, event_types?: [...] }

POST /api/webhooks/unsubscribe

Unsubscribe an existing webhook registration.

  • Auth: None (Public)
  • Body: { contract_id, webhook_url }

GET /api/webhooks/subscriptions

List the webhook subscriptions for a contract (#705). Query strings in webhook URLs are masked.

  • Auth: API Key
  • Query: ?contract_id=C...
  • Response: { success: true, data: { subscriptions: [{ id, webhook_url, event_types, created_at }] } }

Error Responses

All endpoints return JSON errors in a consistent shape.

Validation error

{
  "success": false,
  "error": "Request validation failed",
  "fields": [
    {
      "field": "contractId",
      "message": "contractId is required"
    }
  ]
}

Unauthorized

{
  "success": false,
  "error": "Unauthorized"
}

Rate limited

{
  "success": false,
  "error": "Too many requests, please try again later"
}

Not found

{
  "success": false,
  "error": "Job not found"
}

Server error

{
  "success": false,
  "error": "Internal server error"
}

Related Repos

About

Node.js + Express + TypeScript backend for Soroban milestone escrow

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages