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
| Value | Shape | Where it comes from |
|---|---|---|
| Account id | 0x followed by 64 hex characters | Returned by POST /api/prep/register, and derived from the wallet address. |
| Trading key | A 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
- 1Open Swapmos in a browser and connect the wallet the account belongs to.
- 2Open the futures terminal and press Enable trading.
- 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.
- 4Copy the account id and the 32 byte secret into your own client's configuration. That pair is everything a third party needs.
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.
Signing a private request
The signature covers exactly this string, and nothing else:
timestamp + METHOD + path + bodyNo 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.
- 1Take a millisecond timestamp, Date.now().
- 2Serialise the body once, to a string. That exact string is what you sign and what you send.
- 3Concatenate timestamp, the uppercased method, the path including its query string, and the body string. Omit the body entirely when there is none.
- 4Sign those bytes with the 32 byte ed25519 secret.
- 5Encode the signature as base64url with no padding.
- 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.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| method | string | yes | GET, POST, PUT or DELETE. |
| path | string | yes | 2 to 512 characters, must start with /v1/, must be on the allowlist below. Include the query string. |
| body | string | no | Up to 32768 characters, and a string rather than an object. Forwarded byte for byte. |
| auth.accountId | string | yes | 0x and 64 hex characters. |
| auth.key | string | yes | The public half, ed25519:<base58>, at most 128 characters. |
| auth.signature | string | yes | base64url, no padding, 16 to 256 characters. |
| auth.timestamp | string | yes | The same millisecond timestamp you signed, 10 to 16 digits. |
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.
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.
| Family | Paths |
|---|---|
| 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} |
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.
| Header | Value |
|---|---|
| x-account-id | The account id: 0x and 64 hex characters. |
| x-trading-key | The 32 byte trading key secret, as hex. |
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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| message.brokerId | string | yes | Overwritten server-side. Whatever you send is discarded. |
| message.chainId | number | yes | Positive integer. |
| message.timestamp | number | yes | Positive integer, milliseconds. |
| message.registrationNonce | number or string | yes | From the nonce endpoint. |
| signature | string | yes | 0x and hex. The wallet's signature over the typed message. |
| address | string | yes | 0x and 40 hex characters. |
{ "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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| message.brokerId | string | yes | Overwritten server-side. |
| message.chainId | number | yes | Positive integer. |
| message.tradingKey | string | yes | The public half only, ed25519:<base58>. |
| message.scope | string | yes | The scope the key is bound with, for example read,trading. |
| message.timestamp | number | yes | Positive integer, milliseconds. |
| message.expiration | number | yes | Positive integer, milliseconds. When the key stops working. |
| signature | string | yes | 0x and hex. |
| address | string | yes | 0x and 40 hex characters. |
{ "bound": true, "expiration": 1748000000000 }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.