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.