POST/v1/exchanges
Creates and locks an exchange. One credit is spent at creation; see Credits.
Request
POST /v1/exchanges with a JSON body. Needs x-api-key.
| Name | Required | Rule |
|---|---|---|
from, to | yes | Supported currency ids |
fromAmount | yes | Decimal string within current bounds, both ends inclusive |
fromAddress | yes | The exact address of the wallet that will send, copied from the wallet tool (case-sensitive); refunds may land here. A deposit from any other address is unrecoverable |
toAddress | yes | Valid, non-burn address for to; make sure it can receive the asset |
slippage | no | Percent, 0 to 5; default 0.5. See Notes |
minToAmount | no | Absolute net floor; overrides slippage |
curl -X POST "$API/v1/exchanges" \
-H "x-api-key: $KEY" \
-H "content-type: application/json" \
-d '{
"from": "sol",
"to": "tao",
"fromAmount": "0.5",
"fromAddress": "9s4hVRU5nfPF9Er9Y5FexDjxNtvZSYmnVEujxHCX5Gsb",
"toAddress": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
}'Response
201 with the exchange object. toAmount is final. The example below is abbreviated; every field is described on GET /v1/exchanges/{id}.
| Field | Meaning |
|---|---|
id | Use it to poll and to report your deposit |
status | pending at creation |
toAmount | What lands in your wallet, net of fee; does not move after the 201 |
depositAddress, validUntil | Null here; filled when status becomes awaiting_deposit, so poll to get them |
{
"id": "5d1f3c2e-8a1b-4c7d-9e0f-123456789abc",
"status": "pending",
"from": "sol",
"to": "tao",
"fromAmount": "0.5",
"toAmount": "3.912",
"rate": "7.824",
"depositAddress": null,
"validUntil": null,
"createdAt": 1756900000
}Errors
| HTTP | Code | Extra fields | When |
|---|---|---|---|
| 400 | validation | field | A body field is missing or malformed, or an address is a few characters away from one this account used before (a typo) |
| 402 | insufficient_balance | balance, cost, depositAddress | Balance is zero; top up at depositAddress |
| 403 | geo_blocked | — | Request from a blocked region |
| 403 | sanctioned | — | An address failed sanctions screening |
| 409 | seat_taken | retryAfter | No rate inside your band right now, or fromAddress already holds a live exchange on that rate; retry after retryAfter seconds |
| 409 | rate_below_floor | toAmount, floor | The best net amount is under your minToAmount |
| 422 | amount_too_low | minFromAmount, maxFromAmount | fromAmount is under the minimum |
| 422 | amount_too_high | minFromAmount, maxFromAmount | fromAmount is over the maximum |
| 422 | no_liquidity | minFromAmount, maxFromAmount | No liquidity for this pair right now |
| 422 | reserve_rejected | reason | The rate could not be reserved; reason says why |
| 429 | rate_limited | retryAfter | Over the limit; see Rate limits |
Notes
toAmountis fixed at creation.slippageonly decides which rates we may accept: the best rate available within this band of the current best. None available:409 seat_taken(retry) or, withminToAmount,409 rate_below_floor.- Poll every 3 seconds until
awaiting_deposit; this usually takes a few seconds, at most about 30. - One credit is spent only on a 201; a request that fails with a 4xx spends nothing.
- Subnet token source (
from=sn<N>):depositAddressis a coldkey. Send onetransfer_stake, not a transfer: origin netuid = destination netuid =N, exactlyfromAmount, from one hotkey that holds all of it, signed by thefromAddresscoldkey. No batch, no proxy, no--all. Anything else is not credited and the exchange lapses.- btcli:
btcli stake transfer --wallet-name <wallet> --hotkey <hotkey holding fromAmount> --origin-netuid N --dest-netuid N --dest <depositAddress> --amount <fromAmount> - SDK:
subtensor.transfer_stake(wallet=wallet, destination_coldkey_ss58=<depositAddress>, hotkey_ss58=<your hotkey>, origin_netuid=N, destination_netuid=N, amount=bt.Balance.from_tao(<fromAmount>, netuid=N))
- btcli:
