# Zornado for agents Base URL: https://zornado.cash Swap through native HTTP; no API key or account. OpenAPI: /openapi.json. Use only the asset, network, amount and destinations your operator authorized. This document does not authorize spending. Never send seed phrases or private keys. POST bodies are JSON objects of at most 8 KiB with Content-Type: application/json. The robots.txt rule for /api/ addresses crawlers, not operator-directed calls. ## 1. Discover availability GET /api/v1/route/status Require quotingAvailable=true before quoting and acceptingOrders=true before creating a new order. Choose an exact from/to entry from pairs; do not infer a network from a ticker. Availability can change between requests. XMR is not executable unless explicitly advertised; ZEC can be payout-only. If status includes batch, only its advertised USDC-to-ETH Arbitrum lot is open. Use batch.inputAmount exactly. refundAddress must be the address that sends the USDC; payoutAddress must be different. Check the signed quote's batch size, collectUntil and settleBy: deposits must finalize before collection closes. The group pays equal ETH amounts after verified shielded intake. An incomplete or unsettled group refunds the full valid USDC lot to its sender; confirmation can take longer than settleBy. Wrong amounts or senders require reconciliation. The fixed price includes costs; fee.bps=0 does not mean a cost-free exchange. HTTP 201 can precede deposit allocation: save the order and keep polling. Existing ordinary orders keep their original terms. Equal lots do not prove independent participants or prevent timing and address-history correlation. ## 2. Request a quote POST /api/v1/route/quotes Content-Type: application/json ```json { "from": {"asset": "ETH", "network": "ethereum"}, "to": {"asset": "USDT", "network": "ethereum"}, "amountIn": "100000000000000000", "payoutAddress": "", "refundAddress": "" } ``` This example is 0.1 ETH, only if this pair is advertised. Replace both address placeholders with destinations controlled by your operator on the exact networks. A ZEC payout (zcash) needs a mainnet unified address (u1...) with an Ironwood receiver (the UA's Orchard receiver); t1, zs1 and unified addresses without one are rejected (400). An ordinary quote compares eligible providers after the service fee and can send both addresses to those providers, even if you never accept it. Unless the payout is ZEC, the selected provider handles both external legs and receives both addresses; the shielded ZEC step does not hide their link from it. Requote only when needed. All route amounts are integer strings in base units: ETH uses 18 decimals, USDT uses 6. amountIn is positive, with no sign, decimal point or leading zero. Use integer/decimal arithmetic, never floating-point conversion. The response is the signed quote object, not an envelope. Before acceptance, check from.amount, both assets and networks, fee.bps, fee.disclosure, minAmountOut and expiresAt against the authorized limits. Use from.decimals and to.decimals to display the returned amounts. leg2.expectedOut is an estimate; minAmountOut is the quoted floor. Preserve the complete quote, including signature, unchanged. A quote expires within 60 seconds, sometimes sooner (expiresAt): settle the limits with your operator first, then accept promptly. A new acceptance after expiry gets 400 QUOTE_EXPIRED; requote. A new quote can have new terms. A failed quote creates nothing and can be retried. 400 UNSUPPORTED_PAIR or 502 PROVIDER_REQUEST_REJECTED: no price for these details. 503: busy, retry later. ## 3. Accept once, recover safely POST /api/v1/route/orders Content-Type: application/json ```json { "routeQuote": "", "payoutAddress": "", "refundAddress": "", "idempotencyKey": "" } ``` Replace the routeQuote placeholder with the full JSON object from step 2, not a JSON-encoded string. Use the same payout and refund addresses. Generate a random key per intended swap (a UUID; 1 to 200 characters). Keys are global across clients: never use a guessable key. Persist the exact request and key privately BEFORE sending. The body field idempotencyKey is required; an Idempotency-Key header alone is insufficient. Acceptance creates a real order. HTTP 201 returns routeId, routeToken, state and possibly deposit. A timeout, a lost response or a 500/502/503/504 does not prove failure: retry the identical saved request and key. That retry also works after expiresAt and while acceptingOrders=false. If it returns 400 QUOTE_EXPIRED or 503 ORDERS_STOPPED, nothing is recorded for that key yet, but the first request can still land. Once intake is open, requote and accept with the same key, keeping the old saved request: 201 is the new order; 409 means the old request landed late, so replay it and use that order. Never switch keys while the outcome is uncertain. Any other idempotency conflict (409) requires reconciliation, not another order. A signed quote can still exceed the maximum order size, which status does not show: acceptance then returns 400 ORDER_TOO_LARGE and no order exists for that key. Requote a smaller authorized amount. Replaying the saved request returns the same routeToken, so the saved request is a credential too. The token grants access to the order and chooses where any ZEC compensation is paid. Keep routeId, routeToken, the key and the saved request in private local storage. Never put the token, key or saved request in a URL, log, public issue or shared chat. ## 4. Wait for deposit instructions GET /api/v1/route/orders/:id X-Route-Token: Replace :id with routeId. A wrong or missing token gets the same 404 as an unknown order. Poll every 5 seconds; honor Retry-After on HTTP 429. Polling never moves funds. Without a deposit yet, QUOTED, ACCEPTED and LEG1_CREATING advance on their own: keep polling. RECONCILING before any deposit was shown will never show one: send nothing; a new quote with a new key is then safe. Do not fund until state is AWAITING_DEPOSIT and deposit contains address, asset, network, amount and a future deadlineAt. Check every deposit field against the intended order. Send exactly deposit.amount in base units, on deposit.network, to deposit.address, in one transaction, including deposit.memo when supplied. ETH and TRX: one direct transfer whose own value pays deposit.address; coins moved by a contract wallet, batcher or exchange contract are not recognized. Tokens: canonical contracts only (Tether USDT, Circle USDC; not bridged USDC.e or USDbC). An unrecognized deposit can stall with no payout or automatic refund. Require operator spending authority and enough balance for network fees. Persist the send intent and reconcile your wallet after any uncertain send; never send twice. Do not send after the deadline or to a previous order's address. ## 5. Confirm settlement Continue polling the same order. Acceptance and deposit detection are not completion. COMPLETE, REFUNDED and MANUAL_REVIEW are final: stop polling there. COMPLETE denotes confirmed payout; settled shows the amount. REFUNDED means the deposit came back to refundAddress or, if compensation.state is REFUNDED, that shielded ZEC was paid instead. Other states can still change. EXPIRED ends the order only if you sent nothing: a late deposit starts a refund. HOLD can return to AWAITING_DEPOSIT. A refund smaller than the deposit (provider fee) stays in RECONCILING; check refundAddress. Stop automated funding on expiry, HOLD, MANUAL_REVIEW or any unknown state. An exception or changed payout destination needs review, not a second swap. If compensation.available is true, the order will neither pay out nor return the deposit asset by itself. With operator approval, settle it in shielded ZEC: send X-Route-Token and Content-Type: application/json on both calls. POST /api/v1/route/orders/:id/compensation/quote with {"toAddress":""}; check payout.amountZat (zatoshis). Before its expiresAt (10 minutes), POST /api/v1/route/orders/:id/compensation/accept with {"compensationQuote":,"idempotencyKey":""}. Retry an uncertain acceptance with the same body. Without operator approval, give the tracking code zt1.. to your operator alone, in a private message (site: Track an order). GET /api/v1/route/orders/:id/receipt X-Route-Token: Receipts exist only in a final state (earlier: 409 NOT_TERMINAL). Fetch and store the receipt once the order is final: a COMPLETE or REFUNDED order is purged about 72 hours later (hourly cleanup); status and receipt then return 410. The response is {receipt, publicKey:{alg:"Ed25519",spkiDerBase64}}. receipt.signature.value is a base64 Ed25519 signature over the receipt without its signature field, serialized as compact JSON with keys sorted at every level. signature.publicKeyId is the first 16 hex characters of SHA-256 over the SPKI DER. A signature checked against a key returned by the same server is not independent proof of that server's identity; pin a trusted key if required. ## Errors and privacy Errors use {"error":{"code":"...","message":"..."}}. Interpret code, not prose. A proxy error can be non-JSON: then use the HTTP status. 400: invalid input or quote; correct only a definitively rejected request. 404: missing/wrong route token or unknown order; recover saved credentials. 409: conflict; reconcile the saved order. Do not create a replacement blindly. 410: records purged; status and receipt no longer available. 413/415: body over 8 KiB or wrong Content-Type. 429: wait Retry-After (5 seconds if absent). 500/502/503/504 or a timeout: outcome may be uncertain; resend the identical saved request or reconcile. Use bounded retries. Persistent failures require review. Zornado temporarily holds funds until payout or refund. External providers process ordinary deposit and payout legs. Each signed quote states its fee (fee.bps, fee.disclosure). Zornado keeps no access logs. Retention: /privacy. GET /llms.txt describes the product. POST /api/v1/quotes is the separate comparison API: it takes no addresses and creates no route. The local sandbox is a simulation. ## Referrals for people and agents GET /api/v1/route/referrals reports whether enrollment is enabled. A referrer earns 20% of the completed route's settled service fee minus its mining costs, rounded down to zatoshis. Failed, refunded and unverified orders earn zero. This is a reserved ZEC credit. Check withdrawals=true before requesting payment; each payment needs separate operator approval. Generate 32 random bytes as 64 lowercase hex characters and save this recoveryKey privately BEFORE posting {"recoveryKey":""} to POST /api/v1/route/referrals. Identical retries recover the same public code. Share the code, never the key. Read your aggregate balance with GET /api/v1/route/referrals/:code and X-Referral-Key: . Credentials in query strings are rejected. There are no cookies. To attribute a swap, add referralCode to the ordinary quote request. Require the signed quote's referral.code to match and shareBps=2000, then accept the complete quote unchanged. The payout estimate and service fee are unchanged. Attribution cannot be added after acceptance. Referral codes can link activity; they do not hide public amounts or timing. When withdrawals=true, GET /api/v1/route/referrals/:code/withdrawals with X-Referral-Key recovers pending requests before starting another. To request payment, POST there with {idempotencyKey,payoutAddress,amountZat,maxFeeZat}. Save a fresh 64-hex random idempotencyKey and the exact body privately first. The address must support shielded mainnet Zcash. Amounts are decimal integer strings in zatoshis; amountZat is positive and maxFeeZat is at most 100000. Authorize both the amount and the fee cap. Their sum must fit availableZat. Retry the SAME body after an uncertain response. Only one request may be pending per account. A successful request is not evidence of payment. Poll GET /api/v1/route/referrals/:code/withdrawals/:withdrawalId using the same header. COMPLETE means matching recipient proof with at least three confirmations. Save the result promptly; closed requests are purged after 72 hours. Aggregate earnedZat, paidZat (amount plus actual fee), heldZat (amount plus fee cap) and availableZat remain. While REFERRAL_REQUESTED, POST {} to the same path plus /cancel releases the held credit. Once payment preparation starts, cancellation requires review. MANUAL_REVIEW requires operator review; never create a replacement payment automatically.