Skip to content

Swap API

The /v1/swap/* endpoints provide a SideShift.ai-style instant swap between Bitcoin (L1) and projected eCash (pECX on Snowside). It is a custodial stopgap so users can move BTC ↔ pECX before the BIP-300 drivechain peg activates. When the peg goes live, this service is decommissioned (see packages/swap/DESIGN.md §12).

⚠️ This is a NAMED EXCEPTION to AGENTS.md rule 5 (custody). Unlike the rest of ecashfarm.com/v1 (read-only, non-custodial), the swap operator holds the user’s deposit for the swap window (~1 BTC confirmation). The full custody model, SGX enclave boundary, and threat history live in packages/swap/DESIGN.md (in the monorepo).

https://ecashfarm.com/v1/swap

All responses are JSON and CORS-enabled (Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: POST, GET, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization). Reads (config, quote, order/:id, orders) require no authentication. POST /order is per-IP rate-limited (30 req / 60s).

Every response carries a mode field:

  • preview — no SGX enclave is connected and Snowside Uniswap is not live. Deposit addresses are clearly-fake placeholders (preview_...); no chain is contacted; no custody occurs. Integrators can build and test the full flow now.
  • live — the operator’s SGX enclave is connected (via SGX_ENCLAVE_URL) and Snowside Uniswap is deployed. Deposit addresses are SGX-derived; the enclave watches the BTC chain and settles pECX after one confirmation.

Detect mode from GET /v1/swap/config → mode. Always check mode before trusting a deposit address. In preview mode, telling a user to send real BTC to a preview_... address will lose their funds.

Custody boundary (read this before integrating)

Section titled “Custody boundary (read this before integrating)”
Operation Where it happens
Quote / config / status reads Server (D1 + reference price)
BTC deposit-address derivation SGX enclave (operator-provisioned; V8 isolate cannot run SGX)
Holding the BTC deposit (the custody window) SGX enclave (~1 confirmation)
Selling BTC → pECX via Uniswap on Snowside SGX enclave (signs the swap tx)
Sending pECX to the user SGX enclave (signs the settle tx)
Order persistence + audit trail D1 (swap_orders, swap_events)
Cloudflare Worker role Order API + status relay + non-custodial glue — holds no keys, signs nothing

The worker never sees a private key. All signing is inside the enclave. This is a custodial design; do not describe it as “non-custodial” to users.


Service info: name, custody disclaimer, mode, endpoint list, and a link to this documentation page.

{
"ok": true,
"service": "ecash-farm-swap",
"custody": "custodial (AGENTS.md rule-5 exception; see DESIGN.md)",
"mode": "preview",
"endpoints": ["/config", "/quote", "/order", "/order/:id", "/orders?address="],
"docs": "https://docs.ecashfarm.com/reference/swap-api"
}

Operator config: fee, bounds, confirmations, deposit window, supported directions, custody string. Cached 60s (Cache-Control: public, max-age=60). This is the single source of truth for integrators — always read it at startup; do not hardcode fees or bounds.

{
"ok": true,
"mode": "preview",
"feeBps": 100,
"feePct": 1,
"minBtc": 0.0005,
"maxBtc": 5,
"confirmationsRequired": 1,
"depositWindowMinutes": 60,
"directions": ["btc_to_pecx", "pecx_to_btc"],
"custody": "custodial (rule-5 exception; see DESIGN.md)"
}
Field Meaning
feeBps / feePct Operator spread, in basis points (100 = 1.00%) and percent. Taken from the output amount.
minBtc / maxBtc Bounds on the BTC leg of a swap, in BTC. Apply to both directions (pecx_to_btc is bounded by the equivalent pECX value).
confirmationsRequired BTC confirmations the enclave waits before settling (default 1).
depositWindowMinutes Order expires if no deposit is received within this window (default 60). Late deposits are refunded.
directions Supported swap directions. An integrator should disable a direction if it is absent here.

Quote an expected swap output at the reference price minus the operator fee. In live mode the rate is fetched from GET /v1/liquidity/quote (the BTC.b/pECX Uniswap pool) and locked for the order deposit window at creation. In preview mode the rate is a mock (BTC≈$78,000, pECX≈$0.90 → 86,666.67 pECX/BTC). Bounds-checked against the BTC leg (minBtc–maxBtc).

Name Required Description
direction yes btc_to_pecx or pecx_to_btc
amount yes Input amount as a decimal string (e.g. "0.1" BTC or "1000" pECX)
{
"ok": true,
"mode": "preview",
"mocked": true,
"direction": "btc_to_pecx",
"amountIn": "0.1",
"amountOut": "8580.00000000",
"rate": 86666.66666667,
"feeBps": 100,
"feeAmount": 86.66666667,
"expiresIn": 3600
}
Field Meaning
amountOut Expected output after fee, decimal string, 8 decimals.
rate Price used (output/input). In live mode this is locked at order creation.
feeAmount Fee in the output asset units (not the input).
mocked true when mode === "preview".
expiresIn Quote validity in seconds (3600 = 1h). A quote is indicative; the rate is only locked at order creation.

{ "ok": false, "error": "..." } — invalid direction, non-numeric amount, amount out of bounds (minBtc–maxBtc), or amount below the dust threshold.


Create a swap order. The worker validates the body, computes the output + fee, persists the order + an initial created event to D1, and — in live mode — calls the SGX enclave to derive a fresh BTC deposit address. Returns the deposit address the user sends the input asset to.

Rate-limited: 30 requests / 60s per IP (429 with Retry-After).

{
"direction": "btc_to_pecx",
"amount": "0.1",
"settleAddress": "0xUserSnowsidePecxAddress",
"referrer": "alpha.pecx.shop"
}
Field Required Description
direction yes btc_to_pecx or pecx_to_btc
amount yes Input amount as a decimal string.
settleAddress yes Destination for the output asset. For btc_to_pecx this is a Snowside pECX address (0x…); for pecx_to_btc this is a BTC address (bc1…/1…/3…).
referrer no Front-end referrer for attribution (e.g. "alpha.pecx.shop", "ecashfarm.com").
{
"ok": true,
"mode": "preview",
"mocked": true,
"id": "swap_a914a5e3f8df",
"direction": "btc_to_pecx",
"depositAsset": "BTC",
"depositAddress": "preview_btc_1d89ec69",
"depositAmount": "0.1",
"settleAsset": "PECX",
"settleAddress": "0xUserSnowsidePecxAddress",
"settleAmount": "8580",
"rate": 86666.66666667,
"feeBps": 100,
"status": "awaiting_deposit",
"expiresAt": 1787979915,
"confirmationsRequired": 1
}
Field Meaning
id Opaque order id. Store it client-side — it is the only handle to poll status.
depositAddress Send the input asset here. In preview this is a preview_… placeholder; in live it is SGX-derived.
depositAmount Exact amount to send. Send it in a single transaction.
settleAmount Output the user will receive after settlement.
status Always awaiting_deposit on creation.
expiresAt Deposit-window deadline (unix seconds). A deposit after this is refunded.
mocked true when the deposit address is a preview placeholder.
  • 400 — invalid body, amount out of bounds, missing/invalid settleAddress.
  • 429 — rate limit exceeded. Retry-After header present.

Order status + append-only event trail. This is the polling endpoint: after creating an order, poll this until status reaches a terminal state (complete, expired, failed, refunded). A 5s poll interval is recommended.

Name Description
id The order id returned by POST /order.
{
"ok": true,
"mode": "preview",
"mocked": false,
"id": "swap_a914a5e3f8df",
"direction": "btc_to_pecx",
"depositAsset": "BTC",
"depositAddress": "preview_btc_1d89ec69",
"depositAmount": "0.1",
"depositTxid": null,
"depositConfirmed": 0,
"settleAsset": "PECX",
"settleAddress": "0xUserSnowsidePecxAddress",
"settleAmount": "8580",
"settleTxid": null,
"rate": 86666.66666667,
"feeBps": 100,
"status": "awaiting_deposit",
"expiresAt": 1787979915,
"createdAt": 1787976315,
"events": [
{ "event": "created", "detail": "direction=btc_to_pecx amount=0.1 settle=…", "created_at": 1787976315 }
]
}
Field Meaning
status The order lifecycle state (see the state machine below).
depositTxid The BTC deposit transaction id, once the enclave sees it (else null).
depositConfirmed Confirmations seen on the deposit tx.
settleTxid The pECX settle tx id, once settled (else null).
events Append-only audit trail (swap_events in D1). Never mutated.
mocked false when this is a real D1 row. In preview an unknown id returns a mock awaiting_deposit response (200) so integrators can test the polling flow; in live an unknown id returns 404.
created → awaiting_deposit → received → confirming → settling → complete
(no deposit by expiry) ↘ expired → refunded
(settle failure) ↘ failed → refunded
Status Meaning Terminal?
created Transient — order row + first event written. no
awaiting_deposit Deposit address returned; waiting for the user to send. no
received Deposit seen in mempool (0 conf). no
confirming Deposit confirmed (≥1 conf). Enclave holds BTC. no
settling Enclave selling BTC→pECX and sending to settle address. no
complete pECX delivered to settleAddress. yes
expired No deposit within the window. Late deposit is refunded. yes
failed Settlement failed (slippage/Snowside down). BTC refunded. yes
refunded BTC returned to the source address. yes

The custody window is confirming → settling. Stop polling once status is in the terminal set.


List the most recent orders for a settlement address (newest first). Used by front-ends to show a user’s swap history. Paginated via limit.

Name Required Default Description
address yes — The settlement address to filter by.
limit no 20 Max results (capped at 100).
{
"ok": true,
"mode": "preview",
"mocked": true,
"address": "0xUserSnowsidePecxAddress",
"count": 0,
"orders": []
}

Each element of orders has the same shape as the persisted swap_orders row (id, direction, deposit_asset, deposit_amount, settle_asset, settle_amount, settle_address, status, rate, fee_bps, created_at, updated_at). mocked: true with count: 0 indicates no orders found for that address (preview returns an empty list rather than an error).

{ "ok": false, "error": "Missing address" } — the address query param is required.


A minimal front-end flow (this is exactly what ecashfarm.com/swap does):

  1. Load config — GET /v1/swap/config. Read mode, feePct, minBtc, maxBtc, confirmationsRequired, depositWindowMinutes, directions. Disable the UI if mode is not the one you expect.
  2. Quote (debounced on amount input) — GET /v1/swap/quote?direction=… &amount=…. Display amountOut, rate, feeAmount. Bounds-check the amount against minBtc/maxBtc before enabling the create button.
  3. Create order — POST /v1/swap/order with direction, amount, and the user’s settleAddress. Persist the returned id + depositAddress.
  4. Show the deposit address — instruct the user to send depositAmount of depositAsset to depositAddress in a single transaction. Show the expiresAt countdown.
  5. Poll status — GET /v1/swap/order/:id every ~5s. Advance your UI through the state machine. Stop when status is terminal.
  6. On completion — show settleTxid (pECX sent) or, on expired/failed, show the refunded state with the refund tx id.
// 1. config
const cfg = await fetch('https://ecashfarm.com/v1/swap/config').then(r => r.json());
if (cfg.mode !== 'live' && cfg.mode !== 'preview') throw new Error('swap down');
// 2. quote
const q = await fetch(
`https://ecashfarm.com/v1/swap/quote?direction=btc_to_pecx&amount=0.1`
).then(r => r.json());
console.log(`You receive ${q.amountOut} pECX (rate ${q.rate}, fee ${q.feeAmount})`);
// 3. create order
const order = await fetch('https://ecashfarm.com/v1/swap/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
direction: 'btc_to_pecx',
amount: '0.1',
settleAddress: '0xYourPecxAddress',
referrer: 'your-frontend',
}),
}).then(r => r.json());
console.log(`Send ${order.depositAmount} ${order.depositAsset} to ${order.depositAddress}`);
console.log(`Order id: ${order.id} (poll /v1/swap/order/${order.id})`);
// 4. poll
const poll = async () => {
const s = await fetch(`https://ecashfarm.com/v1/swap/order/${order.id}`).then(r => r.json());
console.log(s.status, s.events);
if (!['complete', 'expired', 'failed', 'refunded'].includes(s.status)) {
setTimeout(poll, 5000);
}
};
poll();

The full machine-readable spec is available at:

GET https://ecashfarm.com/v1/swap/openapi.json

It is OpenAPI 3.1.0 with component schemas for every request/response (CreateOrderRequest, CreateOrderResponse, OrderStatusResponse, Quote, Config, ServiceInfo, OrdersListResponse, Mode, Direction, OrderStatus, Error). Point any Swagger UI instance at it, or generate a client with openapi-generator.

All non-2xx responses use:

{ "ok": false, "error": "human-readable message" }

The HTTP status code is on the response itself; the error object does not duplicate it as a status field (that name is reserved for the order lifecycle string on success responses). When the rate limit fires, the response includes a Retry-After header (seconds).

This API is a custodial stopgap. The operator holds deposits for the swap window under an SGX enclave — which is not trustless (see the SGX threat history in packages/swap/DESIGN.md). The long-term, trustless mechanism for pECX ↔ BTC is the BIP-300 drivechain peg, which will replace this service when it activates. Until then, integrators should disclose the custody model to their users and never describe the swap as “non-custodial.”