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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| fromChain | number | yes | Chain id. Must be an integer. |
| toChain | number | yes | Chain id. Same chain is a swap, different is a bridge. |
| fromToken | string | yes | 0x and 40 hex characters. |
| toToken | string | yes | 0x and 40 hex characters. |
| fromAmount | string | yes | Base units, 1 to 78 digits, not zero. Not a decimal figure. |
| fromAddress | string | yes | The sender, 0x and 40 hex characters. |
| toAddress | string | no | The recipient. Defaults to the sender. |
| slippage | number | no | A fraction above 0 and at most 0.5. 0.005 is half a percent. |
| order | string | no | FASTEST or CHEAPEST. Anything else is invalid_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'{
"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
}- 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.
GET/api/swap/tokens
| Parameter | Type | Required | Notes |
|---|---|---|---|
| chain | number | yes | Chain id, a positive integer. Otherwise invalid_chain. |
| q | string | no | Search term. Matches an address, a symbol or a name. |
| limit | number | no | Defaults 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.
{
"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.
{
"chains": [
{
"id": 42161,
"key": "arb",
"name": "Arbitrum",
"logoURI": "https://…",
"nativeSymbol": "ETH"
}
]
}GET/api/swap/status
| Parameter | Type | Required | Notes |
|---|---|---|---|
| txHash | string | yes | 0x and 64 hex characters. Otherwise a 400. |
| source | string | no | primary by default. Pass fallback when the quote's tracking.source said so. |
| fromChain | number | no | Positive integer. Helps the lookup resolve faster. |
| toChain | number | no | Positive 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.
{
"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.
| status | HTTP | Meaning |
|---|---|---|
| signed | 200 | The voucher is in the response body. |
| already_claimed | 409 | That deposit has been redeemed. |
| deposit_not_found | 404 | No such deposit on chain. |
| signer_mismatch | 503 | The relayer is misconfigured. Not a caller error. |
| error | 500 | Anything else. The message is truncated. |
GET on this path answers 405 with { "status": "error", "message": "Use POST." }.