API

Swaps

Quoting only. A quote is unsigned calldata for a wallet to sign.

The swap side of this API prices routes and reports on transfers. It never signs and never broadcasts. A quote comes back as transaction data your caller hands to a wallet.

GET/api/swap/quote

ParameterTypeRequiredNotes
fromChainnumberyesChain id. Must be an integer.
toChainnumberyesChain id. Same chain is a swap, different is a bridge.
fromTokenstringyes0x and 40 hex characters.
toTokenstringyes0x and 40 hex characters.
fromAmountstringyesBase units, 1 to 78 digits, not zero. Not a decimal figure.
fromAddressstringyesThe sender, 0x and 40 hex characters.
toAddressstringnoThe recipient. Defaults to the sender.
slippagenumbernoA fraction above 0 and at most 0.5. 0.005 is half a percent.
orderstringnoFASTEST or CHEAPEST. Anything else is invalid_request.
Request
curl 'https://swapmos.com/api/swap/quote\
?fromChain=1&toChain=42161\
&fromToken=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\
&toToken=0xaf88d065e77c8cc2239327c5edb3a432268e5831\
&fromAmount=1000000000\
&fromAddress=0x<20 byte address>\
&slippage=0.005'
Response
{
  "quote": {
    "source": "primary",
    "fromChainId": 1,
    "toChainId": 42161,
    "fromToken": "0x<20 byte address>",
    "toToken": "0x<20 byte address>",
    "fromAmount": "1000000000",
    "toAmount": "999120000",
    "toAmountMin": "994124400",
    "approvalSpender": "0x<20 byte address>",
    "tx": {
      "chainId": 1,
      "to": "0x<20 byte address>",
      "data": "0x<calldata>",
      "value": "0",
      "gas": "420000"
    },
    "durationSeconds": 180,
    "gasCostUsd": 2.41,
    "expiresAt": 1717171999,
    "tracking": { "source": "primary" }
  },
  "degraded": false
}
tx is unsigned calldata. This API does not sign it, does not broadcast it, and never asks for a private key. Send it to a wallet, which is the only thing that can authorise it.
  • toAmountMin is the floor and the number a user is actually promised. toAmount is an expectation.
  • Amounts are base units of their own token, as strings. Reading them as numbers loses precision.
  • approvalSpender is present when the input is an ERC20 and absent when it is the chain's native coin. Approve that spender for fromAmount before sending.
  • tx.gas is present only when the router simulated the transaction. When it is absent, let the wallet estimate; passing a zero would put a transaction on chain with no gas.
  • expiresAt is unix seconds. Past it the route is refused with quote_expired and you re-quote.
  • degraded true means the first-choice router failed in a way that said nothing about the route, and a reserve one answered instead.

A pair nobody can price is an answer rather than a fault, and comes back 200 as { "noRoute": true } with no quote. Check for quote before reading it.

The ClubMOS chain is refused with unsupported_pair on either side. It is routed in-house by this app's own contracts and no external venue knows it exists.

GET/api/swap/tokens

ParameterTypeRequiredNotes
chainnumberyesChain id, a positive integer. Otherwise invalid_chain.
qstringnoSearch term. Matches an address, a symbol or a name.
limitnumbernoDefaults to 40, capped at 100.

The upstream list is enormous, so the search runs on the server and only a page of results crosses the wire. Unsearched, the order is curated: the chain's own coin first, then the assets a person opening a token list expects. Searched, it is relevance: an exact address, then a canonical match, then a symbol prefix, then anything containing the term.

Response
{
  "tokens": [
    {
      "chainId": 1,
      "address": "0x<20 byte address>",
      "symbol": "USDC",
      "name": "USD Coin",
      "decimals": 6,
      "logoURI": "https://…",
      "priceUSD": "1.0001",
      "verified": true,
      "canonical": true
    }
  ],
  "total": 218
}
  • verified false means an upstream screener flagged the token. It is not hidden, because hiding one from someone who searched its address is its own kind of failure. It sinks in the order, and you should mark it.
  • canonical true marks the real token for a symbol. Mainnet carries more than one asset called USDC and only one of them is Circle's.
  • total is how many matched, not how many were returned.

GET/api/swap/chains

No parameters. Every routable network, trimmed to what a picker needs.

Response
{
  "chains": [
    {
      "id": 42161,
      "key": "arb",
      "name": "Arbitrum",
      "logoURI": "https://…",
      "nativeSymbol": "ETH"
    }
  ]
}

GET/api/swap/status

ParameterTypeRequiredNotes
txHashstringyes0x and 64 hex characters. Otherwise a 400.
sourcestringnoprimary by default. Pass fallback when the quote's tracking.source said so.
fromChainnumbernoPositive integer. Helps the lookup resolve faster.
toChainnumbernoPositive integer.

One shape for both routers, so a client polls the same way whichever one built the transfer. status is one of pending, done, refunded or failed.

Response
{
  "status": "done",
  "detail": "COMPLETED",
  "message": "Transfer complete",
  "fillTxHash": "0x<32 byte hash>",
  "refundTxHash": null
}
  • A transfer that is not indexed yet comes back 200 as { "status": "pending", "detail": "not_found" }. That is normal for the first seconds after a send. Keep polling.
  • A refund lands back on the sending side, never the receiving one, so refundTxHash is the sending transaction.
  • message is only present on the primary path and may be absent.
  • This route answers a failure as { "error": "..." } without a code, except a rate-limit refusal, which carries one.

POST/api/relayer/voucher

Takes { "depositId": "0x…" }, a 32 byte ClubMOS deposit id. The server reads the real deposit on chain and signs a voucher for its true recipient and amount, so a forged request cannot produce a payable one. Anything but a string depositId is a 400.

statusHTTPMeaning
signed200The voucher is in the response body.
already_claimed409That deposit has been redeemed.
deposit_not_found404No such deposit on chain.
signer_mismatch503The relayer is misconfigured. Not a caller error.
error500Anything else. The message is truncated.

GET on this path answers 405 with { "status": "error", "message": "Use POST." }.