Skip to content

Authentication

Sera uses two authentication modes:

  • API keys for account reads and transaction-building helpers.
  • EIP-712 signatures for trading, cancellation, withdrawal, and API-key management.

In practice, the public utility routes are:

  • GET /health, GET /system/time, GET /tokens, GET /markets, and GET /config
  • POST /swap/quote and POST /verify-signature

The API-key protected read and helper routes are:

  • GET /orders, GET /orders/{order_id}, GET /fills, GET /fills/{order_id}, and GET /balances
  • GET /permit/metadata
  • transaction builders such as POST /approve, POST /deposit, POST /tx/send, POST /transfer, and POST /transfer/send

API Keys

API keys are read-only credentials sent as:

Authorization: Bearer {api_key}:{api_secret}

Endpoint summary:

Method Path Signed With
POST /api-keys EIP-712 body payload (action=create)
GET or POST /api-keys or /api-keys/list EIP-712 query parameters or body payload (action=list)
DELETE or POST /api-keys or /api-keys/revoke EIP-712 query parameters or body payload (action=revoke_<api_key>)
POST /api-keys/revoke-all EIP-712 body payload (action=revoke_all)
POST /api-keys/self-revoke Bearer credentials of the key being revoked
POST /api-keys/verify None (the key being verified is the body)

Create An API Key

API keys are created by signing an EIP-712 ManageApiKey message.

const domain = {
  name: 'Sera',
  version: '1',
  chainId: 11155111,
  verifyingContract: '0xd0fc92d8eF9bE26D7288fCa1D6458f675e72B83a'
};

const types = {
  ManageApiKey: [
    { name: 'owner', type: 'address' },
    { name: 'action', type: 'string' },
    { name: 'timestamp', type: 'uint256' }
  ]
};

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action: 'create',
  timestamp
});

const response = await fetch('https://api.sera.cx/api/v1/api-keys', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    action: 'create',
    timestamp,
    signature,
    label: 'Trading bot'
  })
});

const { api_key, api_secret } = await response.json();

Notes:

  • The signed timestamp must be within 5 minutes of server time.
  • A wallet can hold up to 10 active API keys.
  • api_secret is returned only once. Store it securely.

List API Keys

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action: 'list',
  timestamp
});

const url = new URL('https://api.sera.cx/api/v1/api-keys');
url.searchParams.set('owner_address', walletAddress);
url.searchParams.set('action', 'list');
url.searchParams.set('timestamp', String(timestamp));
url.searchParams.set('signature', signature);

const keys = await fetch(url).then(r => r.json());

If your client prefers a JSON body over signed query parameters, POST /api-keys/list accepts the same owner_address, action, timestamp, and signature fields in the request body.

Revoke An API Key

const action = `revoke_${apiKeyToRevoke}`;
const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action,
  timestamp
});

const url = new URL('https://api.sera.cx/api/v1/api-keys');
url.searchParams.set('owner_address', walletAddress);
url.searchParams.set('api_key', apiKeyToRevoke);
url.searchParams.set('action', action);
url.searchParams.set('timestamp', String(timestamp));
url.searchParams.set('signature', signature);

await fetch(url, { method: 'DELETE' });

If your client prefers a JSON body over signed query parameters, POST /api-keys/revoke accepts the same fields plus api_key in the request body.

Revoke All API Keys

Revoke every active API key for a wallet in a single signature. The action is the literal string revoke_all. Useful after a suspected leak, device loss, or as part of a wallet-rotation playbook — one wallet popup instead of N.

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action: 'revoke_all',
  timestamp
});

const res = await fetch('https://api.sera.cx/api/v1/api-keys/revoke-all', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    action: 'revoke_all',
    timestamp,
    signature
  })
});

// 200: { "status": "ok", "revoked_api_keys": ["sera_...", "sera_..."], "count": 2 }
// 200: { "status": "ok", "revoked_api_keys": [], "count": 0 }   // no active keys
// 409: signature already used (replay) — re-sign with a fresh timestamp

Self-Revoke (Bearer-Authenticated)

Revoke the API key whose credentials you are currently using, without a wallet signature. Authenticate with Authorization: Bearer {api_key}:{api_secret}; the body's api_key must equal the bearer's api_key (you can only revoke the key you are signed in as — to revoke a sibling key, use DELETE /api-keys with a wallet signature).

await fetch('https://api.sera.cx/api/v1/api-keys/self-revoke', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}:${apiSecret}`
  },
  body: JSON.stringify({ api_key: apiKey })
});

Verify An API Key

Confirm that an api_key/api_secret pair is valid without consuming rate-limit budget or side-effecting any state. Returns the owner address on success.

const res = await fetch('https://api.sera.cx/api/v1/api-keys/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ api_key: apiKey, api_secret: apiSecret })
});

// 200: { "valid": true, "owner_address": "0x..." }
// 401: { "detail": "Invalid api_key or api_secret" }
// 503: { "detail": "Service temporarily unavailable; please retry" } when the auth backend is unreachable

EIP-712 Domain

The public API verifies signatures against the Sepolia Sera contract domain:

const domain = {
  name: 'Sera',
  version: '1',
  chainId: 11155111,
  verifyingContract: '0xd0fc92d8eF9bE26D7288fCa1D6458f675e72B83a'
};

Use GET /config to fetch the live chain_id, sera_address, vault_address, and sor_address for the current deployment instead of hardcoding them.

Expiration Rules

Signed order payloads require expiration, and swap quote requests use the same bounded future timestamp for the signed Intent they return.

  • expiration must be strictly in the future.
  • expiration must be no more than 365 days minus the 300-second clock-skew guard from current server time.
  • Missing, zero, past, or far-future values are rejected before the request reaches matching or settlement.

Use GET /system/time to derive these timestamps and leave a small buffer instead of signing right at the edge.

UUID Binding For Order Requests

Limit orders now carry two linked identifiers:

  • order_id: a UUID4 string used as the human-readable order ID.
  • uuid_int: the decimal uint256 embedded in the signed on-chain Order.uuid field.

The API rejects requests where uuid_int does not match the composite encoding of order_id shown below. Fetch the live executor_id from GET /health, then generate uuid_int using the composite layout below:

[255:252] executor_id | [251:124] full UUID4 bits | [123:12] group_id | [11:0] leg_id

Standalone Limit Orders

For a normal limit order:

  • leg_id = 0
  • group_id = first 112 bits of order_id
function uuidStringToBigInt(uuid) {
  return BigInt(`0x${uuid.replace(/-/g, '')}`);
}

function encodeStandaloneUuid(orderId, executorId) {
  const raw = uuidStringToBigInt(orderId);
  const group = raw >> 16n;
  return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n)).toString();
}

Example valid pair:

{
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520"
}

Virtual Liquidity Batches

For VL batches:

  • all siblings share the same group_id
  • group_id is derived from order 0
  • leg_id increments 0, 1, 2, ...
function encodeVlUuid(orderId, executorId, legId, groupOrderId) {
  const raw = uuidStringToBigInt(orderId);
  const group = uuidStringToBigInt(groupOrderId) >> 16n;
  return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n) | BigInt(legId)).toString();
}

Order Signature

The signed on-chain Order struct is:

const types = {
  Order: [
    { name: 'user', type: 'address' },
    { name: 'expiration', type: 'uint48' },
    { name: 'feeBps', type: 'uint48' },
    { name: 'recipient', type: 'address' },
    { name: 'fromToken', type: 'address' },
    { name: 'toToken', type: 'address' },
    { name: 'fromAmount', type: 'uint256' },
    { name: 'toAmount', type: 'uint256' },
    { name: 'initialDepositAmount', type: 'uint256' },
    { name: 'uuid', type: 'uint256' }
  ]
};

In POST /orders requests, the API uses pair identity fields rather than direct spend/receive fields:

  • from_address is the market base token address.
  • to_address is the market quote token address.
  • bid spends to_address to buy from_address.
  • ask spends from_address to sell into to_address.

Fetch these token addresses from GET /tokens and display-oriented pair labels from GET /markets.

Example:

const orderData = {
  user: walletAddress,
  expiration: Math.floor(Date.now() / 1000) + 86400,
  feeBps: 0,
  recipient: ethers.ZeroAddress,
  fromToken: '0x...',
  toToken: '0x...',
  fromAmount: '1085000000',
  toAmount: '1000000000',
  initialDepositAmount: 0,
  uuid: BigInt(uuidInt)
};

const signature = await signer.signTypedData(domain, types, orderData);

Intent Signature For Swaps

POST /swap/quote returns route_params. Sign them exactly as returned.

const types = {
  Intent: [
    { name: 'taker', type: 'address' },
    { name: 'inputToken', type: 'address' },
    { name: 'outputToken', type: 'address' },
    { name: 'maxInputAmount', type: 'uint256' },
    { name: 'minOutputAmount', type: 'uint256' },
    { name: 'recipient', type: 'address' },
    { name: 'initialDepositAmount', type: 'uint256' },
    { name: 'uuid', type: 'uint256' },
    { name: 'deadline', type: 'uint48' }
  ]
};

const signature = await signer.signTypedData(domain, types, quote.route_params);

Cancel Signatures

CancelOrder

CancelOrder.orderId is the composite uuid_int, not the UUID string.

const types = {
  CancelOrder: [
    { name: 'owner', type: 'address' },
    { name: 'orderId', type: 'uint256' }
  ]
};

const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  orderId: BigInt(uuidInt)
});

CancelVLBatch

const types = {
  CancelVLBatch: [
    { name: 'owner', type: 'address' },
    { name: 'vlBatchId', type: 'string' }
  ]
};

Withdraw Signature

const types = {
  WithdrawIntent: [
    { name: 'user', type: 'address' },
    { name: 'tokens', type: 'address[]' },
    { name: 'amounts', type: 'uint256[]' },
    { name: 'recipient', type: 'address' },
    { name: 'deadline', type: 'uint256' },
    { name: 'uuid', type: 'uint256' }
  ]
};

Verify A Signature Before Submission

const response = await fetch('https://api.sera.cx/api/v1/verify-signature', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    side: 'bid',
    amount: '1000.0',
    price: '1.085',
    from_address: EURC_ADDRESS,
    to_address: USDC_ADDRESS,
    order_id: '00000000-0000-4000-8000-000000000001',
    uuid_int: '6427948336465191935941739505432058208337171677044006212075520',
    signature: signature,
    expiration: Math.floor(Date.now() / 1000) + 86400
  })
});

Clock Synchronization

Use GET /system/time for expiration and deadline calculations, GET /health for the live executor_id used in uuid_int generation, and GET /config for the live EIP-712 contract addresses.