Skip to content
darkrouteRHPublic

About

DarkRoute public API v1: quote and route private, non-custodial swaps from your own code. OpenAPI spec, curl walkthrough, runnable examples in shell, Node and Python. No key, rate-limited, the fee printed on every quote.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DarkRoute public API v1

The same router the app, the Chrome extension and the Android shell use, from your own code. Quote a private route, create a non-custodial order, read its status.

https://app.darkroute.exchange/api/v1
Method Route What Limit
GET /tokens Assets the router can quote, popular first 60/min
POST /quote Dry quote for a pair and amount. Never creates an order 40/min
POST /order Create an order; returns the id. Deposit address is on the status call. Partner key, or a site-issued order token 10/min
GET /order/{id} Status, deposit address, deadline, fee, receipt data per IP
GET /token/{address}/quote What a token costs to trade, read from its own Uniswap v4 pools. Robinhood Chain or Arc 60/min
GET /nft/destination Whether an address has ever been used, for the NFT checkout flow 60/min
GET /stats Orders, settled, volume, gross fee, burned. Cached 60 s 60/min
GET /burns Buybacks and burns with hashes, plus two live supply numbers 60/min
GET /openapi.json The spec, also in this repo as openapi.json cached

Limits are per IP. CORS is open, so the API works from a browser too.

Pricing a token from its own pools

GET /token/{address}/quote is different from POST /quote. That one asks a cross-chain venue what it would fill. This one reads Uniswap v4 directly and tells you what a given token costs to trade on the chain it lives on: the best pool, its fee, whether it has a hook, and what stands between the pool price and what actually lands.

# Robinhood Chain, spending 0.01 ETH
curl "https://app.darkroute.exchange/api/v1/token/0xebB4C5B97E4117e30EC82ce025E6f21dded05436/quote?buy=0.01"

# Arc, spending 1 USDC
curl "https://app.darkroute.exchange/api/v1/token/0x01d776dc060f5a0a7296ac60a2222c992e284f01/quote?chain=arc&buy=1"

# selling instead
curl ".../quote?chain=arc&sell=100000"

Two things in the response are worth reading before you build on it.

chain.routeAsset is not the same everywhere. Robinhood Chain routes through native ETH. Arc routes through USDC at 0x3600…0000, because 78.7% of Arc's v4 pools are quoted in USDC and only 1.7% in native. A token whose only pool is against something else gets no route and is told so, rather than being quietly sent somewhere it did not ask to go.

chain.tradable can be false. On Arc it is, and will stay false until somebody deploys a UniversalRouter there: it is the contract a swap is addressed to, and Arc has none. We can price a fill on Arc and we cannot send one. The field exists so your code can tell the difference instead of discovering it at signing time.

worst is what the least favourable pool that answered would have paid for the same size. It is there so the spread between pools is visible rather than claimed. On a chain where anyone can open a pool for any token, that gap has been measured at more than 90 points.

A 503 means either no pool could fill that size or the chain could not be read. The two are distinguished in error, deliberately: a chain we cannot reach is never reported to you as a token that does not exist.

Every route above is public except one: POST /order needs a partner key, or a short lived token the site issues to a real page load and binds to the caller's IP.

Until 20 September 2026 this said a key was required outright. The check behind that read the Origin header, which any caller sets, so one added header walked through it. An outside reporter, gege, showed us after we had told him it was closed. The token shuts the trivial case and not a script that fetches a token first, and we would rather describe it accurately than promise a wall. Keys have been needed since 15 September 2026. Quoting, prices, status and supply stay open to anyone, without asking us for anything.

An earlier version of this file said the opposite: that keyless access would stay when keys arrived. It did not, for order creation, and the line is being corrected rather than deleted. Orders are where money moves and where having a name on the other end is worth something. The rest of the promise held.

Keys are issued by hand. Email support@darkroute.exchange, say what you are building, and you get a key or a reason. What a key carries, and what a holder may and may not claim about DarkRoute, is at https://darkroute.exchange/integrate.

curl -X POST https://app.darkroute.exchange/api/v1/order \
  -H "Authorization: Bearer dr_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{...}'

Send it as a header, never in a URL. URLs reach server logs, proxy logs, browser history and Referer headers, and a key that lands in any of those has to be replaced.

Send a User-Agent header from scripts. Cloudflare in front of the app rejects the default Python-urllib agent with a 403; any real agent string passes (the Python example sets one).

A swap in four calls

# 1. assets, pick two assetId values
curl https://app.darkroute.exchange/api/v1/tokens

# 2. quote (dry, never creates an order)
curl -X POST https://app.darkroute.exchange/api/v1/quote \
  -H "content-type: application/json" \
  -d '{"originAsset":"nep141:eth.omft.near","destinationAsset":"nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near","amount":"0.1"}'

# 3. order (needs a partner key; addresses are validated against each chain's shape)
curl -X POST https://app.darkroute.exchange/api/v1/order \
  -H "content-type: application/json" \
  -d '{"originAsset":"…","destinationAsset":"…","amount":"0.1","recipient":"<destination address>","refundTo":"<origin address>"}'

# 4. status, deposit address, receipt. Poll it.
curl https://app.darkroute.exchange/api/v1/order/dr_4f2a9c31be07

Read on the quote: routes[0].venue (named as is), routes[0].out, fee.chargedPct (0.3), and cost.allInPct, which is everything between mid-market and what lands, reported even when it is bad. On the order: order.depositAddress, order.deadline, and order.status, which walks PENDING_DEPOSIT, PROCESSING, SUCCESS, or REFUNDED, FAILED, INCOMPLETE_DEPOSIT.

Runnable versions: examples/swap.sh, examples/swap.mjs, examples/swap.py. Each one quotes and stops before creating an order unless you pass addresses.

Rules

  • A quote is dry. It never creates an order and never touches the venue's order book.
  • An order is non-custodial. The venue issues a one-time deposit address; funds go from the sender into the route. DarkRoute never holds them and cannot reverse them.
  • The id is the credential. There is no account. Anyone with an order id can read its status and deposit address, so treat ids as secrets on your side.
  • The fee is printed. 0.3% of the input, on the quote, before any deposit. If your integration hides it from the user, that is on you, not on the number.
  • Same rows as the app. Every venue that quotes is named as is. Today that is one venue, NEAR Intents; more rows appear as integrations go live, each under its real name.

Clients you can read

The Chrome extension is a complete client of this API in plain JavaScript, no build step: darkrouteRH/extension, see lib/api.js.

Problems

Open an issue here, or write to support@darkroute.exchange. Security issues: see darkroute.exchange/security.

Docs page: darkroute.exchange/api.

/nft/destination

Reports whether a destination address has ever been used, and says why when it has.

It exists for the NFT checkout flow on app.darkroute.exchange/nft, whose claim is narrow on purpose: everyone knows that address bought the NFT, nobody knows the address is you. An NFT transfer cannot be hidden. Ownership is public state rather than only public history, so anyone can query who holds a token right now. What can be kept apart is the wallet that paid and the address that bought, and that only holds while the second address has no history of its own.

So the check is the product, not decoration. Three signals, any one of them enough:

signal what it means
nonce above zero it has sent transactions, so it has a public history
contract code a contract or a delegated account, carrying its own trail
a balance something arrived before, and whoever sent it can watch what happens next
curl "https://app.darkroute.exchange/api/v1/nft/destination?chain=rh&address=0x…"
{"ok":true,"fresh":false,"reasons":[
  "it has sent 36 transactions",
  "it carries contract code, so it has its own history",
  "it already holds a balance, so somebody watched funds arrive"]}

The reasons are sentences rather than codes because they are meant to be shown to a person.

Two failures that are deliberately not collapsed into one. A chain we cannot reach answers 503 and never fresh: reporting our own outage as a clean address would be worse than having no check at all. A malformed address answers 400 rather than blaming the chain, which is a mistake this project has made before and does not intend to repeat.

Read only. Nothing is stored and nothing is logged; the address does not leave the request.

About

DarkRoute public API v1: quote and route private, non-custodial swaps from your own code. OpenAPI spec, curl walkthrough, runnable examples in shell, Node and Python. No key, rate-limited, the fee printed on every quote.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors