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, andGET /configPOST /swap/quoteandPOST /verify-signature
The API-key protected read and helper routes are:
GET /orders,GET /orders/{order_id},GET /fills,GET /fills/{order_id}, andGET /balancesGET /permit/metadata- transaction builders such as
POST /approve,POST /deposit,POST /tx/send,POST /transfer, andPOST /transfer/send
API Keys¶
API keys are read-only credentials sent as:
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_secretis 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.
expirationmust be strictly in the future.expirationmust 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-chainOrder.uuidfield.
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:
Standalone Limit Orders¶
For a normal limit order:
leg_id = 0group_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_idis derived from order 0leg_idincrements0, 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_addressis the market base token address.to_addressis the market quote token address.bidspendsto_addressto buyfrom_address.askspendsfrom_addressto sell intoto_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.