Skip to main content

Authentication

How to register a signing key and authenticate REST and WebSocket requests with an API key.

Market data is public. For your account's data, authenticate every request with an API key and secret. You receive them when you register a signing key.

  • Every account endpoint and every private WebSocket channel accepts only an API key. Without one, REST answers 401 and a private-channel subscribe answers AUTH_REQUIRED.
  • A claimed x-account-address header or ?account= value is not proof of identity and is ignored for account data.
  • An endpoint with an address in the path (for example /account/:address/orders) answers 403 when that address is not your key's account.

How the app does it​

When you choose Complete Sign In in the app, it does three things:

  1. Create a signing key

    It generates a random signing key in your browser.

  2. Link it to your wallet

    It asks your wallet to sign an EIP-712 Register message that links the signing key to your wallet. This is not a transaction and costs no gas.

  3. Register

    It sends both signatures to POST /register, which returns an api_key and api_secret.

Register a key yourself​

POST /register with a JSON body:

{
"account": "0xYourWallet",
"signing_key": "0xSigningKeyAddress",
"expiry": "<now + 604800>",
"account_signature": "0x…",
"signing_key_signature": "0x…"
}

The typed data for this network, taken from the backend's signature verifier:

{
"domain": {
"name": "RealTime Arc Testnet",
"version": "1",
"chainId": 5042002
},
"types": {
"Register": [
{
"name": "key",
"type": "address"
},
{
"name": "expiry",
"type": "uint256"
}
],
"SignKey": [
{
"name": "account",
"type": "address"
}
]
}
}

Register and SignKey are two separate signatures: sign each with only its own type ({ Register: … } for account_signature, { SignKey: … } for signing_key_signature). expiry is Unix seconds and must be in the future; a past value gives a key that lasts one minute, and a longer one is capped at 30 days (the app signs the maximum value, so its keys last 30 days).

With ethers v6:

import { ethers } from 'ethers';

const wallet = new ethers.Wallet(PRIVATE_KEY); // your trading wallet
const signingKey = ethers.Wallet.createRandom(); // keep its private key safe
const domain = { name: DOMAIN_NAME, version: '1', chainId: CHAIN_ID }; // from the block above
const expiry = Math.floor(Date.now() / 1000) + 7 * 86400;

const account_signature = await wallet.signTypedData(
domain,
{ Register: [{ name: 'key', type: 'address' }, { name: 'expiry', type: 'uint256' }] },
{ key: signingKey.address, expiry },
);
const signing_key_signature = await signingKey.signTypedData(
domain,
{ SignKey: [{ name: 'account', type: 'address' }] },
{ account: wallet.address },
);

// POST /register with:
// { account: wallet.address, signing_key: signingKey.address, expiry: String(expiry),
// account_signature, signing_key_signature }
  • account_signature: your wallet signs Register { key: signing_key, expiry }.
  • signing_key_signature: the signing key signs SignKey { account }.
  • Both signatures are required. A missing or invalid signature is rejected with 401.
  • Optional fields:
    • referral_code.
    • invite_code, only on invite-only deployments.
    • no_api_key: true, which updates your signing key without issuing a new API key when you already have one.

The response is 201 with { "api_key", "api_secret", "account", "signing_key" }; with no_api_key: true and an existing key, only { "account", "signing_key" }. A malformed account returns 400; on invite-only deployments a missing or invalid invite code returns 403, and a used one 409. Store the secret securely, because it is not shown again.

Key lifetime​

expiry is required: it is part of the signed Register message. The key lives until expiry, capped at 30 days by default (the operator can lower the cap). An expiry already in the past gives a key that lives 60 seconds. When a key expires, register again.

How many keys you can hold​

An account keeps at most 5 live API keys by default. Each registration adds one, and when that goes over the cap, the oldest key is revoked, and its open WebSocket connections lose their identity. Every new browser sign-in counts as a registration.

Authenticate REST requests​

Send both headers. For example, your account overview in your preferred language:

GET /account
curl "https://arc-testnet-api.realtime.exchange/account" \
-H "API-KEY: $API_KEY" \
-H "API-SECRET: $API_SECRET"

Open GET /account in the API reference and playground →

  • A valid key decides the account: a conflicting x-account-address header or ?account= value is ignored.
  • The legacy header names AEVO-KEY / AEVO-SECRET are also accepted.

Authenticate the WebSocket​

After connecting, send an auth operation on the message event:

socket.emit('message', { op: 'auth', data: { key: API_KEY, secret: API_SECRET } });

The reply arrives on the message event as a JSON string:

  • Success: {"data":{"success":true,"account":"0x…"}}, with the account lowercased.
  • Failure: success: false and a code:
CodeMeaning
AUTH_INVALIDKey or secret missing or wrong
AUTH_UNAVAILABLEThe server could not check the key (for example, a storage outage). Try again
AUTH_RATE_LIMITEDToo many attempts; the reply includes retryAfterMs
AUTH_EXPIREDSent unprompted when your key expires or is revoked (by DELETE /api-key, or because a new registration pushed it over the key cap) while you are connected

Rules:

  • Attempts are limited to 5 attempts per connection and 30 per IP address, each per 60 seconds.
  • A failed auth (AUTH_INVALID, AUTH_UNAVAILABLE), or an expired or revoked key (AUTH_EXPIRED), removes the connection's identity and unsubscribes it from all private channels. Authenticate again, then resubscribe. AUTH_RATE_LIMITED leaves the current identity in place.

After a successful auth you can subscribe to your own account's private channels. See WebSocket API.

Keep your credentials safe​

Revoke a key​

DELETE /api-key, authenticated with an API key of the same account:

# Revoke the key this request is signed with
curl -X DELETE "$API_URL/api-key" -H "API-KEY: $API_KEY" -H "API-SECRET: $API_SECRET"

# Revoke another of your keys
curl -X DELETE "$API_URL/api-key" -H "API-KEY: $API_KEY" -H "API-SECRET: $API_SECRET" -H "Content-Type: application/json" -d '{"api_key": "KEY_TO_REVOKE"}'

# Revoke every key of your account
curl -X DELETE "$API_URL/api-key" -H "API-KEY: $API_KEY" -H "API-SECRET: $API_SECRET" -H "Content-Type: application/json" -d '{"all": true}'
  • A single revoke returns {"success": true}, or 404 when the key is not found.
  • all: true returns {"success": true, "revoked": <count>}. all must be the JSON boolean true; with any other value, the request revokes the key named in api_key, or the key it is signed with when there is none.
  • A WebSocket connection authenticated with a revoked key loses its identity at once.

If you think a secret has leaked, revoke it straight away. Shorter expiry values also limit the exposure.