Status & discovery API
Read-only JSON. No keys, no state mutation, the same posture as the CypherFaucet / TestnetPool APIs. The swap page uses it to quote; the stats page reads it for live liquidity. Base URL: https://api.testnetswap.com/api.
Amounts appear both in display units (e.g. tBTC) and integer satoshis (*_sats) so clients can avoid floating-point drift.
Every response carries a common envelope: ok (boolean), api_version (integer, bumped only on breaking schema changes), generated_at (unix seconds the response was produced, for cache/freshness logic), and source. The examples below omit the envelope for brevity except where noted. Numbers in them are illustrative. Call the endpoint for live rates and limits.
GET /api/pairs
Supported pairs with the current rate, limits, and free liquidity (total minus what's committed to in-flight swaps).
{
"ok": true,
"pairs": [
{ "from": "tLTC", "to": "tBTC", "rate": 0.01,
"min": 0.001, "max": 0.5,
"min_sats": 100000, "max_sats": 50000000,
"liquidity_free": 0.00994694, "liquidity_free_sats": 994694,
"liquidity_unit": "tBTC" },
{ "from": "tBTC", "to": "tLTC", ... }
]
}
GET /api/quote
Query: ?from=tLTC&to=tBTC&amount=0.5. Quotes against free liquidity, so a pending swap never over-commits the pool.
{
"ok": true,
"from": "tLTC", "to": "tBTC",
"send": 0.5, "receive": 0.005,
"send_sats": 50000000, "receive_sats": 500000,
"rate": 0.01, "fee": 0,
"min": 0.001, "max": 0.5
}
On failure: { "ok": false, "reason": "..." }, e.g. below min, above max, or over free liquidity. Note this still returns HTTP 200; check the ok field.
This REST quote is indicative: the rate is a fixed nominal value, with no reserved quote id or expiry attached. The binding quote for a real swap is negotiated at swap start over the WSS relay and bound to the amounts and timelocks then, so re-quote right before you swap.
GET /api/status
Maker health and live liquidity. Drives uptime monitoring and the "maker online" indicator.
{
"ok": true,
"maker_online": true,
"pairs_up": ["tLTC-tBTC", "tBTC-tLTC"],
"liquidity": {
"tBTC": { "total": 1000000, "committed": 0, "free": 994694 },
"tLTC": { "total": 499714, "committed": 0, "free": 499714 },
"tBTC:xmr-funding": { "total": 500000, "committed": 0, "free": 500000 },
"tLTC:xmr-funding": { "total": 250000, "committed": 0, "free": 250000 }
},
"active_swaps": 0,
"xmr": {
"enabled": true,
"networks": ["testnet", "stagenet"],
"tickers": ["tXMR", "sXMR"],
"settle": ["tBTC", "tLTC"],
"rates": { "tBTC": 0.01, "tLTC": 0.7 },
"free": { "tBTC": 500000, "tLTC": 250000 },
"min_pico": 1000000000, "max_pico": 50000000000,
"rate_tbtc_per_xmr": 0.01,
"active": 0, "max_concurrent": 4
},
"uptime_secs": 76,
"version": "0.1.0",
"api_version": 1,
"generated_at": 1783681073,
"source": "https://github.com/Tech1k/testnetswap"
}
The liquidity object is in integer satoshis (total / committed / free per coin). The <coin>:xmr-funding keys (e.g. tBTC:xmr-funding, tLTC:xmr-funding) mirror the confirmed free balance of each XMR settle funding address (the dedicated pool the maker locks for a Monero swap, separate from the HTLC pool) so committed is always 0 there; a key is present only when that coin is XMR-funded. uptime_secs counts from when the status server started; version is the maker build. xmr advertises the native Monero-swap capability so a client can offer exactly the networks the maker serves ({ "enabled": false } when off); min_pico/max_pico are piconero, settle lists the coins XMR can settle to with per-coin rates, and free is the confirmed sats available at each settle funding address (same numbers as the :xmr-funding liquidity keys). (This example shows the full envelope.)
GET /api/stats
Cumulative, all-time service totals (the maker counts every swap before it's reaped). Powers the Stats page.
{
"ok": true,
"completed": 42,
"refunded": 3,
"failed": 1,
"total": 46,
"success_rate": 0.933,
"volume": { "tLTC": 128.4, "tBTC": 0.1052, "tXMR": 2.5 },
"volume_sats": { "tLTC": 12840000000, "tBTC": 10520000, "tXMR": 2500000000000 },
"by_pair": { "tLTC->tBTC": 30, "tBTC->tLTC": 12, "tXMR->tBTC": 8 },
"by_day": {
"2026-07-09": { "completed": 5, "refunded": 0, "volume": { "tLTC": 1500000000, "tBTC": 1230000 } },
"2026-07-10": { "completed": 3, "refunded": 1, "volume": { "tLTC": 900000000, "tBTC": 738000 } }
},
"first_at": 1782660054,
"last_at": 1783681073,
"active_swaps": 0,
"uptime_secs": 76
}
volume is in display units (coins for tBTC/tLTC, XMR for tXMR/sXMR) and volume_sats is raw: satoshis for tBTC/tLTC but piconero (1e12/XMR) for tXMR/sXMR, so a client derives amounts as value / 1e12 for XMR and value / 1e8 otherwise. by_day is a per-day series (keyed YYYY-MM-DD, UTC) with per-day volume in the same raw units (sats for tBTC/tLTC, pico for XMR), kept for the most recent 200 days; it powers the activity chart on the Stats page.
Monero swaps (over the WSS relay)
Native tXMR/sXMR → tBTC/tLTC swaps are not part of this HTTP API. They run over the WSS relay (the same one HTLC swaps use), because they're an interactive multi-message protocol, not a request/response. The HTTP API only advertises the capability via status.xmr (above). The relay preamble a taker uses:
taker → maker : { "type": "xmr_request_quote", "from": "tXMR", "to": "tBTC",
"send_pico": 5000000000, "quote_only": true }
maker → taker : { "type": "xmr_quote", "lock_sats": 5000, "xmr_pico": 5000000000,
"t1_blocks": 72, "t2_blocks": 72, "rate": 0.01,
"network": "testnet", "to": "tBTC" }
The from-ticker selects the Monero network: tXMR = testnet, sXMR = stagenet. A maker that doesn't serve that network replies { "type": "xmr_error", "reason": "…" }. With quote_only:true the maker just prices it; without it, the maker begins the adaptor swap on that session and the two sides exchange the driver messages (bundle, lock_outpoint, cancel_presig, refund_adaptor, btc_locked, xmr_locked, redeem_adaptor). The full construction is in the source (swap-xmr).
Status codes & rate limits
- 200: request handled. Always check ok: a valid-but-unfillable request (quote below the minimum, over free liquidity) still returns 200 with ok:false and a reason.
- 400: malformed request URL.
- 404: unknown endpoint.
- 405: any method other than GET / OPTIONS.
There is currently no application-level rate limit on this read-only API, though that may change. Please cache where you can and lean on generated_at instead of polling aggressively; a reverse proxy in front of the maker may add its own limits and return 429.
Deep-link into a swap
Any app (e.g. a wallet's "Swap" button) can hand off to the in-browser swap, pre-filling the form via /swap.html query params. No SDK, no embed:
https://testnetswap.com/swap.html?from=tLTC&to=tBTC&amount=0.1&dest=<tBTC address> # params (all optional): # from tLTC | tBTC | tXMR | sXMR coin the user sends (XMR -> tBTC, or tLTC if the maker offers it) # to tBTC | tLTC receive coin (derived from `from` if omitted) # amount <number> send amount # dest <address> receive address (used only if valid for the receive coin) # refund <monero address> tXMR refund address, for a Monero swap # maker <maker_id> route through a specific maker (see /roster)
Every param is validated before use and only ever populates a form field. The swap still runs entirely in the user's browser, non-custodially, with keys generated there. Nothing is pre-authorized; the user reviews and starts the swap.
Notes
All responses are read-only and CORS-enabled. The swap itself runs over a separate WSS relay (your browser talks to a maker directly); this HTTP API is only for discovery and status. Multiple makers can register on the relay: the live roster is served at /roster, and a taker binds to one via ?maker=<id>. See the network page and the source.