{"openapi":"3.1.0","info":{"title":"Hashlock Markets API","version":"1.0.0","description":"Guides, concepts and a quickstart: https://docs.hashlock.markets\n\nNon-custodial cross-chain atomic swaps (BTC ↔ EVM / TRON / Solana) via sealed RFQ + HTLC. Makers and takers share these endpoints. Authenticate with an API key: `Authorization: Bearer hk_...`.\n\n**Wallets first.** An order that GIVES an asset requires a proven wallet on that asset's chain, and your key starts with only the address its account signed in with. `GET /v1/me` lists both what each chain's `login` address is and what you have `proven`; `POST /v1/wallets/{evm|tron|solana|bitcoin}` proves another by signature — Bitcoin included, on a BIP-322 one. A proof adds trading reach and nothing else: it never becomes the account's payout identity, which only a wallet session can change — so a key alone cannot redirect anyone's money. The message you sign for it must say `link this wallet` and carry a nonce from `GET /v1/wallets/nonce`; that nonce is not a login nonce, so the signature you collect is not a session for that wallet's account.\n\n**Rate limits.** Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds). Over the limit returns `429` with `Retry-After`.\n\n**Idempotency.** Send an `Idempotency-Key` header on any POST to make retries safe: the operation runs once and the same response is replayed (with `Idempotency-Replayed: true`). Reusing a key with a different body returns `422`. Keys are remembered for 24h — except a `504`, which is a read of ours running out of time: on the builders nothing happened, and on `/tx/broadcast` we cannot tell — so either way a retry with the same key really retries.\n\n**Custody-agnostic settlement.** The fund/claim/refund endpoints return UNSIGNED transactions; you sign with your own key/HSM and submit via `/v1/tx/broadcast`. The server never holds your keys.\n\n---\n\n## Maker feed (WebSocket)\n\nPolling `GET /v1/rfqs` will not win you flow. `wss://<host>/v1/ws` — the host you selected above — streams every request-for-quote you are eligible for, and takes your quotes back over the same socket.\n\n**Handshake.** Connect, then send your key as the first frame. Nothing else is accepted until you do:\n\n```json\n{ \"apiKey\": \"hk_live_...\" }\n```\n\nThe key needs the `maker` scope (`GET /v1/me` shows what yours has). You get a snapshot, then a readiness marker:\n\n```json\n{ \"type\": \"snapshot\", \"rfqs\": [ ... ] }\n{ \"type\": \"ready\" }\n```\n\nThe snapshot is the **public** book only, and at most **100** entries, newest first. If you need more than that, page `GET /v1/rfqs?cursor=…` after connecting. An RFQ created while the snapshot was being built may appear both in it and in the stream that follows — frames carry the rfq id, so key on it and the duplicate is a no-op.\n\n**The stream.** Two kinds, and only two:\n\n```json\n{ \"type\": \"rfq\", \"kind\": \"created\",   \"rfq\": { ... } }\n{ \"type\": \"rfq\", \"kind\": \"cancelled\", \"rfq\": { ... } }\n```\n\n`cancelled` means the order left the book, not always that its owner cancelled it: when a listed token **closes** (it no longer passes the listing checks), every public order on it is sent as `cancelled` while its status is unchanged (`open` or `negotiating`); if the token **reopens**, the same orders come back as `created` with their original id and `createdAt`. Treat both as upserts keyed on the id.\n\nAn RFQ also leaves the book when it **expires** or is **agreed** with another maker, and neither emits a frame. A book you maintain purely from the stream therefore drifts: it keeps orders nobody can take any more, and quoting into one is rejected. Reconcile with a fresh `snapshot` (reconnect) or check `GET /v1/rfqs/{id}` before committing to a price.\n\n**What reaches you.** Public RFQs, plus private ones addressed to an address on your account. Matching follows each encoding: EVM hex and bech32 (`bc1…`/`tb1…`) compare case-insensitively, base58check (TRON, legacy Bitcoin) does not — there the case of a character is part of the address. Never your own orders — neither in the snapshot nor in the stream. Note the asymmetry: **private orders arrive only on the live stream.** They are link-only by design, so they are absent from the snapshot and from `GET /v1/rfqs`, and there is no endpoint that lists the ones addressed to you. A private order sent while your socket was down is not recoverable — if you trade private flow, stay connected.\n\n**Quoting.** Send the quote down the same socket; no HTTP round-trip, no second authentication. `quoteAmount` is in base units, as everywhere else in this API:\n\n```json\n{ \"quote\": { \"rfqId\": \"…\", \"quoteAmount\": \"1500000000\" } }\n→ { \"type\": \"quoted\", \"rfqId\": \"…\", \"threadId\": \"…\" }\n```\n\nThe `threadId` is a negotiation thread: `GET /v1/threads/{id}` and the propose/accept endpoints take it from there. `POST /v1/rfqs/{id}/quotes` does the same thing over REST if you prefer.\n\n**Errors** arrive as `{ \"type\": \"error\", \"error\": \"…\" }` and do not close the socket.\n\n**Operating it.** There is no heartbeat and no resume: detect a dead socket yourself and reconnect. The fresh `snapshot` is your recovery for the public book, with the two limits above — 100 entries, and no private orders. Nothing is replayed, so this socket is not a settlement log. For that register a webhook (`POST /v1/webhooks`): the feed tells you what to quote, webhooks tell you what happened to your swaps.\n\n**Webhook signature.** `x-hashlock-signature: sha256=<hex>` is HMAC-SHA256 over `<timestamp>.<body>` with your webhook secret; the timestamp is in `x-hashlock-timestamp`. Verify before trusting a delivery, and reply 2xx.\n\n**Webhook events.** `quote.created`, `swap.agreed`, `swap.funded`, `secret.revealed`, `swap.settled`, `swap.refunded`, `swap.half_settled` (one leg claimed, the other refunded at its timelock) — subscribe to none and you get all of them. `POST /v1/webhooks/{id}/ping` also delivers a `ping` event, so handle an unknown `type` by returning 2xx rather than erroring.\n\n**Webhook delivery is best-effort, not durable.** A non-2xx or a timeout is retried after 15s, 30s, 60s, 120s and 240s — **6 attempts over 7m45s** — and is then dropped; there is no redelivery endpoint and no delivery log beyond `lastStatus` on `GET /v1/webhooks`. If your receiver is down longer than that you will lose events, so reconcile against `GET /v1/swaps/{id}` — that is the authoritative state — rather than treating the webhook as the record."},"servers":[{"url":"https://api.hashlock.markets","description":"production"}],"security":[{"ApiKey":[]}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","description":"An API key (hk_live_… / hk_test_…) with read/taker/maker scopes. `read` is read-only: every write except webhooks needs `taker` or `maker` (403 otherwise), and either of those can read too. Creating RFQs and listing tokens need `taker`; quoting needs `maker`."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Client-chosen unique key; retries with the same key replay the first response (24h)."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Asset":{"type":"object","properties":{"id":{"type":"string"},"chain":{"type":"string"},"symbol":{"type":"string"},"address":{"type":["string","null"],"description":"token contract (null = native)"},"decimals":{"type":"integer"},"enabled":{"type":"boolean"},"verified":{"type":"boolean","description":"curated by Hashlock; false = listed automatically after checks — trade it by contract address, not by symbol"},"source":{"type":"string","description":"'curated', 'launchpad:<bankr|virtuals|clanker>' or 'api'"}}},"Rfq":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["sell_base","buy_base"]},"baseAssetId":{"type":"string","description":"asset uuid — list them with GET /v1/assets"},"baseAmount":{"type":"string","description":"base units"},"quoteAssetId":{"type":"string","description":"asset uuid — list them with GET /v1/assets"},"status":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"}}},"CreateRfq":{"type":"object","required":["direction","baseAssetId","baseAmount","quoteAssetId","ttlSeconds"],"properties":{"direction":{"type":"string","enum":["sell_base","buy_base"]},"baseAssetId":{"type":"string","description":"asset uuid — list them with GET /v1/assets"},"baseAmount":{"type":"string","description":"base units (integer string)"},"quoteAssetId":{"type":"string","description":"asset uuid — list them with GET /v1/assets"},"ttlSeconds":{"type":"integer"},"visibility":{"type":"string","enum":["public","private"]},"askAmount":{"type":"string"},"targetAddress":{"type":"string"}}},"Swap":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}}}}},"paths":{"/v1/me":{"get":{"summary":"Verify the key, list its scopes and the wallets attached to its account","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"wallets":{"type":"object","description":"per chain family: the login address, and the addresses proven by signature","properties":{"evm":{"type":"object","properties":{"login":{"type":["string","null"],"description":"the address this account signed in with, and where it is paid"},"proven":{"type":"array","items":{"type":"string"},"description":"addresses proven by signature — tradeable, never a payout identity"}}},"tron":{"type":"object","properties":{"login":{"type":["string","null"],"description":"the address this account signed in with, and where it is paid"},"proven":{"type":"array","items":{"type":"string"},"description":"addresses proven by signature — tradeable, never a payout identity"}}},"bitcoin":{"type":"object","properties":{"login":{"type":["string","null"],"description":"the address this account signed in with, and where it is paid"},"proven":{"type":"array","items":{"type":"string"},"description":"addresses proven by signature — tradeable, never a payout identity"}}},"solana":{"type":"object","properties":{"login":{"type":["string","null"],"description":"the address this account signed in with, and where it is paid"},"proven":{"type":"array","items":{"type":"string"},"description":"addresses proven by signature — tradeable, never a payout identity"}}}}}}}}}},"401":{"description":"invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getMe"}},"/v1/wallets/nonce":{"get":{"summary":"A single-use nonce to put in the message you are about to sign","description":"Expires shortly and is consumed by the first proof that presents it. It is a LINK nonce: it cannot be spent at a login endpoint, so the signature you collect is not a session. `?purpose=authorize` issues the nonce for the login wallet's authorisation instead — a separate pool.","parameters":[{"name":"purpose","in":"query","required":false,"schema":{"type":"string","enum":["link","authorize"]}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"nonce":{"type":"string"}}}}}}},"operationId":"getWalletsNonce"}},"/v1/keys/nonce":{"get":{"summary":"Start minting an API key: a single-use nonce (5 minutes). No key needed.","security":[],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"nonce":{"type":"string"}}}}}}},"operationId":"getKeysNonce"}},"/v1/keys":{"post":{"summary":"Mint an API key with a wallet signature — no browser, no key needed (agents start here)","description":"Sign, with the wallet, a message that says `create an API key`, names the address and carries a nonce from `GET /v1/keys/nonce`. Optional lines `Key name: <name>` and `Scopes: read, taker` (default all three) decide the key — they are read from the signed text only. The key belongs to the account that wallet SIGNS IN to (created on first use); a wallet proved onto another account does not reach it. An account with only a wallet holds ONE live key: a new signed mint replaces the previous one (also the way to recover a lost or leaked key). An account signed up at /developers with email, Google or Telegram holds up to 10. All keys of an account share one rate budget. It expires after 90 days (`expiresAt`; mint a new one to renew), an account holds at most 10 live keys, the owner is told (in Telegram, when linked) of every new one in Telegram, and the plaintext key is returned once. Signing rules per rail are those of POST /v1/wallets/<chain>.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rail","address","message","signature"],"properties":{"rail":{"type":"string","enum":["evm","tron","solana","bitcoin"]},"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"}}},"example":{"rail":"evm","address":"0xYourAddress","message":"Hashlock Markets — create an API key.\n\nAddress: 0xYourAddress\nKey name: my-agent\nScopes: read, taker, maker\nNonce: <GET /v1/keys/nonce>\nIssued At: 2026-09-30T12:00:00Z","signature":"0x…"}}}},"responses":{"200":{"description":"the key — shown once","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"key":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"name":{"type":["string","null"]},"expiresAt":{"type":"string","format":"date-time"}}}}}},"400":{"description":"bad signature, spent nonce, or the account already holds 10 live keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postKeys"}},"/v1/wallets/{address}":{"delete":{"summary":"Withdraw a proof this account made","description":"Removes the address from what this account can trade from. Reach for it after rotating a leaked key: the proofs made with that key outlive it otherwise. Proofs only — a login address is not one and is not removable here. Putting a proof back needs that wallet to sign again.","parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"withdrawn","content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string"},"family":{"type":"string"},"proven":{"type":"boolean","enum":[false]}}}}}},"404":{"description":"this account has no proof for that address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"deleteWalletsByAddress"}},"/v1/wallets/evm":{"post":{"summary":"Prove a EVM wallet this account holds","description":"Sign with personal_sign / EIP-191 a message carrying `Hashlock Markets`, the words `link this wallet`, the address itself and a nonce from `GET /v1/wallets/nonce`. Needed before this account can create an order that GIVES a EVM asset. The proof is recorded as a proof — it does not become the account's payout identity, which only a wallet session can set. ALONE it counts for giving on EVM only. Add `authorization` — your LOGIN wallet signing a message that carries `Hashlock Markets`, the words `authorise this wallet`, the address being linked (and, except for a Bitcoin login, the login address) and a nonce from `GET /v1/wallets/nonce?purpose=authorize` — and the wallet also counts as a payout address, a Bitcoin/Solana refund address and a TRON funding wallet, as if linked at hashlock.markets. That is how an agent with no browser trades across chains.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"},"authorization":{"type":"object","description":"optional: the account's login wallet vouching for this one (see above)","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"}}}}},"example":{"address":"<your address on that chain>","message":"Hashlock Markets — link this wallet.\n\nAddress: <the same address>\nNonce: <GET /v1/wallets/nonce>","signature":"<that message, signed by that address>"}}}},"responses":{"200":{"description":"proven (repeating a proof you already hold is the same 200); `authorised` says whether it counts beyond EVM giving","content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string"},"family":{"type":"string","enum":["evm"]},"proven":{"type":"boolean"},"authorised":{"type":"boolean"}}}}}},"400":{"description":"bad signature, a message that does not say `link this wallet` (or `authorise this wallet`), an authorisation not signed by this account's login wallet or not naming this address, or a nonce that was never issued / already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"that wallet belongs to another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWalletsEvm"}},"/v1/wallets/tron":{"post":{"summary":"Prove a TRON wallet this account holds","description":"Sign with signMessageV2 a message carrying `Hashlock Markets`, the words `link this wallet`, the address itself and a nonce from `GET /v1/wallets/nonce`. Needed before this account can create an order that GIVES a TRON asset. The proof is recorded as a proof — it does not become the account's payout identity, which only a wallet session can set. ALONE it counts for giving on EVM only. Add `authorization` — your LOGIN wallet signing a message that carries `Hashlock Markets`, the words `authorise this wallet`, the address being linked (and, except for a Bitcoin login, the login address) and a nonce from `GET /v1/wallets/nonce?purpose=authorize` — and the wallet also counts as a payout address, a Bitcoin/Solana refund address and a TRON funding wallet, as if linked at hashlock.markets. That is how an agent with no browser trades across chains.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"},"authorization":{"type":"object","description":"optional: the account's login wallet vouching for this one (see above)","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"}}}}},"example":{"address":"<your address on that chain>","message":"Hashlock Markets — link this wallet.\n\nAddress: <the same address>\nNonce: <GET /v1/wallets/nonce>","signature":"<that message, signed by that address>"}}}},"responses":{"200":{"description":"proven (repeating a proof you already hold is the same 200); `authorised` says whether it counts beyond EVM giving","content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string"},"family":{"type":"string","enum":["tvm"]},"proven":{"type":"boolean"},"authorised":{"type":"boolean"}}}}}},"400":{"description":"bad signature, a message that does not say `link this wallet` (or `authorise this wallet`), an authorisation not signed by this account's login wallet or not naming this address, or a nonce that was never issued / already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"that wallet belongs to another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWalletsTron"}},"/v1/wallets/solana":{"post":{"summary":"Prove a Solana wallet this account holds","description":"Sign with ed25519 signMessage, base58 a message carrying `Hashlock Markets`, the words `link this wallet`, the address itself and a nonce from `GET /v1/wallets/nonce`. Needed before this account can create an order that GIVES a Solana asset. The proof is recorded as a proof — it does not become the account's payout identity, which only a wallet session can set. ALONE it counts for giving on EVM only. Add `authorization` — your LOGIN wallet signing a message that carries `Hashlock Markets`, the words `authorise this wallet`, the address being linked (and, except for a Bitcoin login, the login address) and a nonce from `GET /v1/wallets/nonce?purpose=authorize` — and the wallet also counts as a payout address, a Bitcoin/Solana refund address and a TRON funding wallet, as if linked at hashlock.markets. That is how an agent with no browser trades across chains.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"},"authorization":{"type":"object","description":"optional: the account's login wallet vouching for this one (see above)","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"}}}}},"example":{"address":"<your address on that chain>","message":"Hashlock Markets — link this wallet.\n\nAddress: <the same address>\nNonce: <GET /v1/wallets/nonce>","signature":"<that message, signed by that address>"}}}},"responses":{"200":{"description":"proven (repeating a proof you already hold is the same 200); `authorised` says whether it counts beyond EVM giving","content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string"},"family":{"type":"string","enum":["svm"]},"proven":{"type":"boolean"},"authorised":{"type":"boolean"}}}}}},"400":{"description":"bad signature, a message that does not say `link this wallet` (or `authorise this wallet`), an authorisation not signed by this account's login wallet or not naming this address, or a nonce that was never issued / already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"that wallet belongs to another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWalletsSolana"}},"/v1/wallets/bitcoin":{"post":{"summary":"Prove a Bitcoin wallet this account holds","description":"Sign with BIP-322 (UniSat / OKX `signMessage(msg, 'bip322-simple')`) a message carrying `Hashlock Markets`, the words `link this wallet`, and a nonce from `GET /v1/wallets/nonce`. Needed before this account can create an order that GIVES a Bitcoin asset. The proof is recorded as a proof — it does not become the account's payout identity, which only a wallet session can set. ALONE it counts for giving on EVM only. Add `authorization` — your LOGIN wallet signing a message that carries `Hashlock Markets`, the words `authorise this wallet`, the address being linked (and, except for a Bitcoin login, the login address) and a nonce from `GET /v1/wallets/nonce?purpose=authorize` — and the wallet also counts as a payout address, a Bitcoin/Solana refund address and a TRON funding wallet, as if linked at hashlock.markets. That is how an agent with no browser trades across chains.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"},"authorization":{"type":"object","description":"optional: the account's login wallet vouching for this one (see above)","required":["address","message","signature"],"properties":{"address":{"type":"string"},"message":{"type":"string"},"signature":{"type":"string"}}}}},"example":{"address":"<your address on that chain>","message":"Hashlock Markets — link this wallet.\n\nAddress: <the same address>\nNonce: <GET /v1/wallets/nonce>","signature":"<that message, signed by that address>"}}}},"responses":{"200":{"description":"proven (repeating a proof you already hold is the same 200); `authorised` says whether it counts beyond EVM giving","content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string"},"family":{"type":"string","enum":["bitcoin"]},"proven":{"type":"boolean"},"authorised":{"type":"boolean"}}}}}},"400":{"description":"bad signature, a message that does not say `link this wallet` (or `authorise this wallet`), an authorisation not signed by this account's login wallet or not naming this address, or a nonce that was never issued / already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"that wallet belongs to another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWalletsBitcoin"}},"/v1/assets":{"get":{"summary":"Enabled asset registry","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"assets":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}}}}}}}},"operationId":"getAssets"},"post":{"summary":"Ask for a token to be listed","description":"Runs the same checks as the launchpad listener: an on-chain check (EVM: a simulated transfer; Solana: a classic SPL mint with no mint or freeze authority — Token-2022 is not listable), security scan (Powered by GoPlus Security) and market thresholds (market cap ≥ $1M, liquidity ≥ $100k, 24h volume ≥ $250k, ≥ 500 holders, ≥ 24h old, top 10 holders ≤ 40% excluding pools). EVM chains and Solana, where Hashlock supports them. A listed token has `verified: false`. 20 requests an hour per account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain","address"],"properties":{"chain":{"type":"string"},"address":{"type":"string","description":"the token contract (EVM) or mint (Solana, base58 as is)"}}}}}},"responses":{"200":{"description":"already listed, or a closed token reopened because it passes again","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["listed"]},"asset":{"$ref":"#/components/schemas/Asset"},"security":{"type":"string"}}}}}},"201":{"description":"listed now (`verified: false`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["listed"]},"asset":{"$ref":"#/components/schemas/Asset"},"security":{"type":"string"}}}}}},"202":{"description":"not yet. 'pending': it stays queued and is listed automatically if it passes within a week. 'closed': a closed listed token that does not pass yet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","closed"]},"reasons":{"type":"array","items":{"type":"string"}},"security":{"type":"string"}}}}}},"400":{"description":"not a supported chain, or not a token address on it"},"422":{"description":"final. 'rejected' (with reasons): it failed a safety check. 'delisted' (with error): closed for good — by the operator, or for a safety failure after listing","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["rejected","delisted"]},"reasons":{"type":"array","items":{"type":"string"}},"error":{"type":"string"},"security":{"type":"string"}}}}}},"429":{"description":"more than 20 listing requests this hour"}},"operationId":"postAssets"}},"/v1/rfqs":{"get":{"summary":"Order book (open RFQs to quote) — cursor-paginated","parameters":[{"name":"baseAssetId","in":"query","schema":{"type":"string"}},{"name":"quoteAssetId","in":"query","schema":{"type":"string"}},{"name":"direction","in":"query","schema":{"type":"string","enum":["sell_base","buy_base"]}},{"name":"limit","in":"query","description":"page size, 1–100 (default 20)","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"nextCursor from the previous page","schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"rfqs":{"type":"array","items":{"$ref":"#/components/schemas/Rfq"}},"nextCursor":{"type":"string","nullable":true,"description":"pass as ?cursor for the next page; null = last page"}}}}}}},"operationId":"getRfqs"},"post":{"summary":"Create an RFQ (scope: taker)","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRfq"},"example":{"direction":"sell_base","baseAssetId":"<asset uuid — GET /v1/assets>","baseAmount":"50000000","quoteAssetId":"<asset uuid — GET /v1/assets>","ttlSeconds":900,"visibility":"public"}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"type":"object","properties":{"rfq":{"$ref":"#/components/schemas/Rfq"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postRfqs"}},"/v1/rfqs/mine":{"get":{"summary":"Your own RFQs — private ones included, every status — cursor-paginated","description":"The order book lists only public open RFQs. This lists everything you created, newest first: how to find an order whose create call timed out, and its id to cancel it.","parameters":[{"name":"limit","in":"query","description":"page size, 1–100 (default 20)","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"nextCursor from the previous page","schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"rfqs":{"type":"array","items":{"$ref":"#/components/schemas/Rfq"}},"nextCursor":{"type":"string","nullable":true,"description":"pass as ?cursor for the next page; null = last page"}}}}}}},"operationId":"getRfqsMine"}},"/v1/rfqs/{id}/threads":{"get":{"summary":"The negotiation threads on an RFQ — every one for its creator, your own for a maker","description":"How a taker finds the quotes on its RFQ without a webhook: each thread is one maker's quote. Read one with `GET /v1/threads/{id}`. Newest activity first.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"threads":{"type":"array","items":{"type":"object"}}}}}}},"404":{"description":"no such RFQ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getRfqsByIdThreads"}},"/v1/rfqs/{id}":{"get":{"summary":"Get an RFQ","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getRfqsById"}},"/v1/rfqs/{id}/cancel":{"post":{"summary":"Withdraw your own RFQ (scope: taker)","description":"Only before a deal is agreed, and only your own order — the id alone does not cancel someone else's.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"rfq":{"$ref":"#/components/schemas/Rfq"}}}}}},"400":{"description":"not yours, already agreed, or already gone","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postRfqsByIdCancel"}},"/v1/rfqs/{id}/quotes":{"post":{"summary":"Quote an RFQ (scope: maker) — opens a settlement thread","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quoteAmount"],"properties":{"quoteAmount":{"type":"string","description":"base units"}}},"example":{"quoteAmount":"<amount in base units, integer>"}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postRfqsByIdQuotes"}},"/v1/swaps":{"get":{"summary":"List your swaps — cursor-paginated","parameters":[{"name":"limit","in":"query","description":"page size, 1–100 (default 20)","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","description":"nextCursor from the previous page","schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"swaps":{"type":"array","items":{"$ref":"#/components/schemas/Swap"}},"nextCursor":{"type":"string","nullable":true}}}}}}},"operationId":"getSwaps"}},"/v1/swaps/{id}":{"get":{"summary":"Swap lifecycle status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"swap":{"$ref":"#/components/schemas/Swap"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getSwapsById"}},"/v1/swaps/{id}/legs/{leg}/fund":{"post":{"summary":"Build UNSIGNED transaction(s) to fund a leg — sign with your own key/HSM, then broadcast","description":"EVM: sign every returned transaction from the returned `from` address — the leg refund address. The factory refuses any other sender, and the escrow refunds to it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"leg","in":"path","required":true,"schema":{"type":"string","enum":["a","b"]}}],"responses":{"200":{"description":"chain-specific signing material (EVM txs / BTC pay-to / TRON txs / Solana transaction, base64)","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"a state to wait out or act on, not an error: the leg is already funded, the initiator's leg must be funded first, or an address is still missing — the message says which (never stored by Idempotency-Key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"temporarily unavailable (TRON funding paused, or the already-funded check could not be read) — retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"a chain or explorer read of ours ran out of time — nothing was changed; retry (an Idempotency-Key does not remember a 504)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postSwapsByIdLegsByLegFund"}},"/v1/swaps/{id}/legs/{leg}/claim":{"post":{"summary":"Build UNSIGNED claim (reveals the secret) — body: { secret: 32-byte hex }","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"leg","in":"path","required":true,"schema":{"type":"string","enum":["a","b"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["secret"],"properties":{"secret":{"type":"string"}}},"example":{"secret":"<the 32-byte hex preimage of this swap hashlock>"}}}},"responses":{"200":{"description":"signing material","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"a chain or explorer read of ours ran out of time — nothing was changed; retry (an Idempotency-Key does not remember a 504)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postSwapsByIdLegsByLegClaim"}},"/v1/swaps/{id}/legs/{leg}/refund":{"post":{"summary":"Build UNSIGNED refund (after the timelock)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"leg","in":"path","required":true,"schema":{"type":"string","enum":["a","b"]}},{"name":"escrow","in":"query","required":false,"schema":{"type":"string"},"description":"bitcoin only: refund an escrow this leg moved OFF (after a pubkey correction) instead of its current one; refused for an address that was never this leg's escrow"}],"responses":{"200":{"description":"signing material","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"a chain or explorer read of ours ran out of time — nothing was changed; retry (an Idempotency-Key does not remember a 504)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postSwapsByIdLegsByLegRefund"}},"/v1/threads/{id}":{"get":{"summary":"Get a negotiation thread (messages, current terms)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getThreadsById"}},"/v1/threads/{id}/propose":{"post":{"summary":"Propose a new price on a thread","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quoteAmount"],"properties":{"quoteAmount":{"type":"string","description":"base units"}}},"example":{"quoteAmount":"<amount in base units, integer>"}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postThreadsByIdPropose"}},"/v1/threads/{id}/accept-proposal":{"post":{"summary":"Accept the counterparty's pending price","description":"Name the price you are accepting: it is refused if a newer counter has replaced it since you read the thread.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quoteAmount"],"properties":{"quoteAmount":{"type":"string","description":"the pending price, base units — must equal the thread's pendingAmount"}}},"example":{"quoteAmount":"<the pending price, base units — GET /v1/threads/{id}>"}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postThreadsByIdAcceptProposal"}},"/v1/threads/{id}/accept":{"post":{"summary":"Accept the current terms — COMMITS, and cannot be undone","description":"Name the price you are accepting: it is refused if the terms moved since you read the thread. The initiator (funds the long leg) also passes hashlock = sha256(secret); when BOTH sides accept, the swap is created.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quoteAmount"],"properties":{"quoteAmount":{"type":"string","description":"the current price, base units — must equal the thread's currentQuoteAmount"},"hashlock":{"type":"string","description":"32-byte hex; required from the initiator"}}},"example":{"quoteAmount":"<the current price, base units — GET /v1/threads/{id}>","hashlock":"<sha256(secret), 32-byte hex>"}}}},"responses":{"200":{"description":"ok — returns { thread, swap? }","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postThreadsByIdAccept"}},"/v1/swaps/{id}/address":{"post":{"summary":"Set your receive (payout) / refund address for a leg","description":"Set your receive (payout) / refund address for a leg. Bitcoin: the compressed pubkey (hex) — the P2WSH is derived once both sides are set. Required before funding. An address that RECEIVES money — the payout one, and on Bitcoin and Solana the refund one too — must be a wallet this account signed in with, one proven with the login wallet's authorisation (POST /v1/wallets/<chain> with `authorization`), or one proved from a signed-in wallet session on the web (for Bitcoin, the pubkey of such an address). A proof made with an API key alone widens what you can trade but does not count here, so a leaked key cannot redirect your money.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain","address"],"properties":{"chain":{"type":"string"},"address":{"type":"string"},"leg":{"type":"string","enum":["a","b"],"description":"required only when both legs of the swap are on `chain`"}}},"example":{"chain":"<a chain of this swap>","address":"<your payout address on that chain>"}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"swap":{"$ref":"#/components/schemas/Swap"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"a funding of this leg is on its way — its refund address is fixed until it is recorded or fails","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"a chain or explorer read of ours ran out of time — nothing was changed; retry (an Idempotency-Key does not remember a 504)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postSwapsByIdAddress"}},"/v1/swaps/{id}/fund-intent":{"post":{"summary":"Reserve an EVM leg's funding, then report its broadcast","description":"Call it WITHOUT txHash before the first transaction of an EVM leg you fund (approve or createSwap): it reserves the leg, and is refused with 409 while another funding of it is live — from any client or wallet. Call it again WITH txHash as soon as your createSwap is broadcast. While the reservation stands, every client shows the leg as on its way and its refund address is fixed. It is released when the escrow is recorded, or when the transaction reverts, mines without this swap's escrow, or is not seen on chain for 30 minutes; a reservation that never names a transaction lapses after 30 minutes. Pass `at` (the reservation's token) when attaching if more than one of your clients may fund. The same txHash again changes nothing.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["leg"],"properties":{"leg":{"type":"string","enum":["a","b"]},"txHash":{"type":"string","description":"0x + 64 hex — omit to reserve"},"from":{"type":"string","description":"when reserving: the wallet that will send the escrow, recorded as the leg's refund address in the same write"},"at":{"type":"string","description":"when attaching: the reservation's token — meta.fundIntent[leg].at as the reserve returned it"}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"swap":{"$ref":"#/components/schemas/Swap"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"another funding of this leg is on its way","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postSwapsByIdFundIntent"}},"/v1/swaps/{id}/fund-intent/{leg}":{"delete":{"summary":"Give back a funding reservation that never broadcast","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"leg","in":"path","required":true,"schema":{"type":"string","enum":["a","b"]}},{"name":"at","in":"query","required":true,"schema":{"type":"string"},"description":"the reservation's token, as the reserve returned it"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"swap":{"$ref":"#/components/schemas/Swap"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"deleteSwapsByIdFundIntentByLeg"}},"/v1/webhooks":{"get":{"summary":"List your webhooks (secret not shown)","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}}},"operationId":"getWebhooks"},"post":{"summary":"Register a webhook","description":"Register a webhook. Events: quote.created, swap.agreed, swap.funded, secret.revealed, swap.settled, swap.refunded, swap.half_settled (omit = all). Returns the signing secret ONCE; deliveries are POSTed with X-Hashlock-Signature: sha256=HMAC-SHA256(secret, `${timestamp}.${body}`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https only. Registration rejects an address LITERAL that is non-public, but does no DNS — a hostname resolving into a private range registers fine and then fails every delivery, visible only as lastStatus. Delivery resolves and re-checks at connect time, follows no redirects, and times out after 10s."},"events":{"type":"array","items":{"type":"string"}}}},"example":{"url":"<your https endpoint>","events":["swap.funded","swap.settled"]}}}},"responses":{"201":{"description":"created — { webhook, secret }","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWebhooks"}},"/v1/webhooks/{id}":{"delete":{"summary":"Delete a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok"},"404":{"description":"not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"deleteWebhooksById"}},"/v1/webhooks/{id}/ping":{"post":{"summary":"Send a test delivery to a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok — { deliveryId }","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postWebhooksByIdPing"}},"/v1/tx/broadcast":{"post":{"summary":"Relay a client-signed transaction","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chain","signed"],"properties":{"chain":{"type":"string","enum":["evm","tron","bitcoin","solana"]},"signed":{"description":"evm: 0x raw transaction hex · tron: the signed transaction object · bitcoin: raw hex, or { psbtBase64, signaturesHex[], preimageHex? } · solana: the signed transaction, base64"}}},"example":{"chain":"<evm | tron | bitcoin | solana>","signed":"0x<signed raw transaction hex>"}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"txid":{"type":"string"}}}}}},"400":{"description":"error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"the node did not answer in time — the transaction MAY already be broadcast; look the txid up before sending it again. Not remembered by Idempotency-Key, so a retry really retries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"postTxBroadcast"}}}}