API

Authentication

One ed25519 trading key, signed per request. It cannot withdraw.

Public reads need nothing. Everything else is authenticated by an ed25519 signature the caller produces, over the exact request they are making.

The two values

ValueShapeWhere it comes from
Account id0x followed by 64 hex charactersReturned by POST /api/prep/register, and derived from the wallet address.
Trading keyA 32 byte ed25519 secret; its public half is ed25519:<base58>Generated in the browser when a user enables trading. Never sent to this server.

How a user generates one

  1. 1Open Swapmos in a browser and connect the wallet the account belongs to.
  2. 2Open the futures terminal and press Enable trading.
  3. 3The browser generates an ed25519 key pair. The wallet signs a typed message binding the public half to the account, and the app posts that to /api/prep/trading-key. The secret half stays in the browser.
  4. 4Copy the account id and the 32 byte secret into your own client's configuration. That pair is everything a third party needs.
The secret is generated in the browser and is never sent to this server. No endpoint returns it and no endpoint can recover it. A client that lost one enables trading again and binds a new key.

What the key can and cannot do

  • It is scoped to reading and trading. It can place, cancel and close, and read the account it belongs to.
  • It is not a wallet private key and not a seed phrase. Nothing in this API has a field for one.
  • It cannot withdraw. A withdrawal is signed by the wallet itself, upstream, in addition to this key, so a trading key alone moves nothing off the exchange. The withdrawal paths are reachable through the relay precisely because the wallet signature is what actually authorises them.
  • It expires. The expiration is chosen when the key is bound and comes back from POST /api/prep/trading-key. Past it, every signed call fails.
  • It can be revoked in the app without touching the wallet, and balances belong to the account rather than to the key.
The worst case for a leaked trading key is bad trades, not theft. Treat it as a credential anyway: an environment variable or a config file, never a repository and never a prompt.

Signing a private request

The signature covers exactly this string, and nothing else:

timestamp + METHOD + path + body

No host, no origin and no headers are covered, which is why the signature survives being relayed. You sign for the upstream path; this server forwards your signature unchanged and never holds a key of yours.

  1. 1Take a millisecond timestamp, Date.now().
  2. 2Serialise the body once, to a string. That exact string is what you sign and what you send.
  3. 3Concatenate timestamp, the uppercased method, the path including its query string, and the body string. Omit the body entirely when there is none.
  4. 4Sign those bytes with the 32 byte ed25519 secret.
  5. 5Encode the signature as base64url with no padding.
  6. 6POST the envelope below to /api/prep/relay.

POST/api/prep/relay

The one door for every private call, read or write. It forwards a request you already authorised; it cannot produce a signature, so it cannot act for anyone.

ParameterTypeRequiredNotes
methodstringyesGET, POST, PUT or DELETE.
pathstringyes2 to 512 characters, must start with /v1/, must be on the allowlist below. Include the query string.
bodystringnoUp to 32768 characters, and a string rather than an object. Forwarded byte for byte.
auth.accountIdstringyes0x and 64 hex characters.
auth.keystringyesThe public half, ed25519:<base58>, at most 128 characters.
auth.signaturestringyesbase64url, no padding, 16 to 256 characters.
auth.timestampstringyesThe same millisecond timestamp you signed, 10 to 16 digits.
A signed read
curl -X POST https://swapmos.com/api/prep/relay \
  -H 'content-type: application/json' \
  -d '{
    "method": "GET",
    "path": "/v1/positions",
    "auth": {
      "accountId": "0x<64 hex characters>",
      "key": "ed25519:<base58 public key>",
      "signature": "<base64url signature>",
      "timestamp": "1717171717171"
    }
  }'

The response is the upstream payload for that path, scrubbed, at the top level. This endpoint does not reshape it, so its fields are the upstream's own names.

Serialise the body once and send that same string. Re-serialising it anywhere, even into identical JSON with a different key order, changes the bytes the signature covers and the call is rejected.

What the relay will forward

An allowlist, anchored at both ends and matched against the path including its query string. Anything outside it is refused with forbidden before it leaves this server, so this is a relay for the calls this product makes rather than an open proxy.

FamilyPaths
Account/v1/client/info, /holding, /statistics, /leverage, /leverages, /margin_mode, /margin_modes, /key_info
Positions/v1/positions, /v1/position/{symbol}, /v1/position_history, /v1/position_margin, /v1/liquidations, /v1/funding_fee/history
Orders/v1/order, /v1/order/{id}, /v1/orders, /v1/batch-order, /v1/algo/order, /v1/algo/order/{id}, /v1/algo/orders, /v1/trades, /v1/trade/{id}
Balance movement/v1/withdraw_request, /v1/withdraw_nonce, /v1/settle_nonce, /v1/settle_pnl, /v1/pnl_settlement/history, /v1/asset/history, /v1/internal_transfer, /v1/internal_transfer_history
Volume and referral/v1/volume/user/daily, /v1/volume/user/stats, /v1/referral/info, /bind, /rebate_summary, /referee_info, /referee_rebate_summary, /referral_history, /referee_history, /edit_referral_code, and under /v1/referral/multi_level: /statistics, /referee_list, /rebate_info, /max_rebate_rate, /volume_prerequisite, /claim_code
Notifications/v1/notification/inbox, /v1/notification/inbox/{id}
A signature more than five minutes old is refused with expired_signature before it is forwarded. Sign at the moment you send, not when you build the request.

This endpoint is rate limited twice: once against your address, and once against the account id in the envelope, so one busy account behind a shared exit node cannot throttle everybody else on it.

The agent endpoint takes headers instead

POST /mcp, the Model Context Protocol endpoint, accepts the same two credentials as headers and does the signing for you. It is the only place in this product that reads them.

HeaderValue
x-account-idThe account id: 0x and 64 hex characters.
x-trading-keyThe 32 byte trading key secret, as hex.
Those headers work on /mcp only. /api/prep/relay ignores them and takes the signed envelope above. Sending the secret to /mcp means handing the signing step to this server, which is the trade the protocol requires; the relay never sees a secret at all.

Creating an account

Three endpoints, in this order. All three are public, because each one carries a wallet signature that is what actually authorises it.

GET/api/prep/registration-nonce

No parameters. The nonce is bound into the message the wallet signs, which is what stops a captured signature creating the account twice.

{ "nonce": "1234567890" }

POST/api/prep/register

ParameterTypeRequiredNotes
message.brokerIdstringyesOverwritten server-side. Whatever you send is discarded.
message.chainIdnumberyesPositive integer.
message.timestampnumberyesPositive integer, milliseconds.
message.registrationNoncenumber or stringyesFrom the nonce endpoint.
signaturestringyes0x and hex. The wallet's signature over the typed message.
addressstringyes0x and 40 hex characters.
Response
{ "accountId": "0x<64 hex characters>", "created": true }

An address that already has an account is not an error. It comes back as { "created": false } with no account id, because registering twice is the second person to press the button rather than a failure.

POST/api/prep/trading-key

ParameterTypeRequiredNotes
message.brokerIdstringyesOverwritten server-side.
message.chainIdnumberyesPositive integer.
message.tradingKeystringyesThe public half only, ed25519:<base58>.
message.scopestringyesThe scope the key is bound with, for example read,trading.
message.timestampnumberyesPositive integer, milliseconds.
message.expirationnumberyesPositive integer, milliseconds. When the key stops working.
signaturestringyes0x and hex.
addressstringyes0x and 40 hex characters.
Response
{ "bound": true, "expiration": 1748000000000 }
Send the public half only. There is no field anywhere in this API for a secret, and any client or document that asks you for one is not this software.

All three are on the write policy, which is ten requests a minute with a burst of five. That is deliberate: account creation is not a polling loop.