Swap API
Swap API
Section titled “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 inpackages/swap/DESIGN.md(in the monorepo).
Base URL
Section titled “Base URL”https://ecashfarm.com/v1/swapAll 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).
Modes: preview vs live
Section titled “Modes: preview vs live”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 (viaSGX_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.
GET /v1/swap
Section titled “GET /v1/swap”Service info: name, custody disclaimer, mode, endpoint list, and a link to
this documentation page.
Response (200)
Section titled “Response (200)”{ "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"}GET /v1/swap/config
Section titled “GET /v1/swap/config”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.
Response (200)
Section titled “Response (200)”{ "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. |
GET /v1/swap/quote
Section titled “GET /v1/swap/quote”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).
Query parameters
Section titled “Query parameters”| 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) |
Response (200)
Section titled “Response (200)”{ "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. |
Errors (400)
Section titled “Errors (400)”{ "ok": false, "error": "..." } — invalid direction, non-numeric amount,
amount out of bounds (minBtc–maxBtc), or amount below the dust threshold.
POST /v1/swap/order
Section titled “POST /v1/swap/order”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).
Request body
Section titled “Request body”{ "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"). |
Response (200)
Section titled “Response (200)”{ "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. |
Errors (400 / 429)
Section titled “Errors (400 / 429)”400— invalid body, amount out of bounds, missing/invalidsettleAddress.429— rate limit exceeded.Retry-Afterheader present.
GET /v1/swap/order/:id
Section titled “GET /v1/swap/order/:id”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.
Path parameters
Section titled “Path parameters”| Name | Description |
|---|---|
id |
The order id returned by POST /order. |
Response (200)
Section titled “Response (200)”{ "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. |
Order state machine
Section titled “Order state machine”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.
GET /v1/swap/orders
Section titled “GET /v1/swap/orders”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.
Query parameters
Section titled “Query parameters”| Name | Required | Default | Description |
|---|---|---|---|
address |
yes | — | The settlement address to filter by. |
limit |
no | 20 | Max results (capped at 100). |
Response (200)
Section titled “Response (200)”{ "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).
Errors (400)
Section titled “Errors (400)”{ "ok": false, "error": "Missing address" } — the address query param
is required.
Integration guide
Section titled “Integration guide”A minimal front-end flow (this is exactly what ecashfarm.com/swap does):
- Load config —
GET /v1/swap/config. Readmode,feePct,minBtc,maxBtc,confirmationsRequired,depositWindowMinutes,directions. Disable the UI ifmodeis not the one you expect. - Quote (debounced on amount input) —
GET /v1/swap/quote?direction=… &amount=…. DisplayamountOut,rate,feeAmount. Bounds-check the amount againstminBtc/maxBtcbefore enabling the create button. - Create order —
POST /v1/swap/orderwithdirection,amount, and the user’ssettleAddress. Persist the returnedid+depositAddress. - Show the deposit address — instruct the user to send
depositAmountofdepositAssettodepositAddressin a single transaction. Show theexpiresAtcountdown. - Poll status —
GET /v1/swap/order/:idevery ~5s. Advance your UI through the state machine. Stop whenstatusis terminal. - On completion — show
settleTxid(pECX sent) or, onexpired/failed, show therefundedstate with the refund tx id.
Browser example (no dependencies)
Section titled “Browser example (no dependencies)”// 1. configconst 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. quoteconst 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 orderconst 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. pollconst 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();OpenAPI / Swagger
Section titled “OpenAPI / Swagger”The full machine-readable spec is available at:
GET https://ecashfarm.com/v1/swap/openapi.jsonIt 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.
Error format
Section titled “Error format”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).
Disclaimer
Section titled “Disclaimer”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.”