Skip to content

Quickstart

Network & Compatibility

Resource Value
API base URL https://api.testnet.sera.cx/api/v1
Chain Sepolia (chainId 11155111)
Sera contract 0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e
Vault contract 0x3c7945840bAE0d7e7f3824Ebccef1962629250F0
SOR contract 0x83c1368110B640A729f3810De5FBe94b99aa5668

Signing primitives. Every trading mutation is an EIP-712 typed-data signature against the Sera domain. Deposits that take the permit path use the ERC-2612 Permit extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call GET /permit/metadata to check support before signing. API-key management uses an EIP-712 ManageApiKey payload.

Tested clients. Python eth_account >= 0.10 + requests; TypeScript ethers v6 (signer.signTypedData). Browser wallets confirmed working with EIP-712 typed data: MetaMask, Rabby, Frame, Coinbase Wallet, Trust, Rainbow. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

Address casing. Read endpoints (/balances, /orders, /fills) treat owner_address as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

This guide walks you through your first interaction with Sera — from querying available tokens to placing your first swap.

Prerequisites

  • An Ethereum wallet (e.g., MetaMask)
  • Testnet ETH for deposits, withdrawals, and limit-order settlement (swap-only flows do not require ETH). Use a Sepolia faucet.
  • Testnet stablecoins to trade with — claim any of the registered tokens (USDC, EURC, GBPA, XSGD, …) for free from the Sera testnet faucet (see below).

Sera Testnet Token Faucet

Every stablecoin in the testnet token registry can be minted for free from the Sera faucet contract on Sepolia:

Faucet: 0x4FAc3BB8B77547E2Da7ed903baDBeD2f46cBe65a

Use it from the web app's "Get test tokens" flow at testnet.sera.cx, or call the faucet directly with the token address you want. None of the steps below will work without a non-zero balance of the relevant testnet token.

Step 1: Explore Available Tokens

Query the token registry to see what stablecoins are available:

curl https://api.testnet.sera.cx/api/v1/tokens
import requests

response = requests.get("https://api.testnet.sera.cx/api/v1/tokens")
tokens = response.json()["tokens"]
print(tokens)
const response = await fetch("https://api.testnet.sera.cx/api/v1/tokens");
const { tokens } = await response.json();
console.log(tokens);

Step 2: Get a Swap Quote

Get a quote to swap between two tokens:

curl -X POST https://api.testnet.sera.cx/api/v1/swap/quote \
  -H "Content-Type: application/json" \
  -d '{
    "from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
    "to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "from_amount": "1000000000",
    "owner_address": "0xYOUR_ADDRESS",
    "recipient": "0xYOUR_ADDRESS",
    "expiration": 1735689600,
    "gas_mode": "receive_less"
  }'
import time, requests

quote = requests.post(
    "https://api.testnet.sera.cx/api/v1/swap/quote",
    json={
        "from_token":     "0x965d4b4546716e416e950bc30467d128455d2d0e",  # USDC
        "to_token":       "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",  # EURC
        "from_amount":    "1000000000",                                  # 1000 USDC (6 dec)
        "owner_address":  "0xYOUR_ADDRESS",
        "recipient":      "0xYOUR_ADDRESS",
        "expiration":     int(time.time()) + 3600,
        "gas_mode":       "receive_less",
    },
    timeout=10,
).json()
print(quote)
const response = await fetch("https://api.testnet.sera.cx/api/v1/swap/quote", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    from_token:    "0x965d4b4546716e416e950bc30467d128455d2d0e",   // USDC
    to_token:      "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",   // EURC
    from_amount:   "1000000000",                                    // 1000 USDC (6 dec)
    owner_address: "0xYOUR_ADDRESS",
    recipient:     "0xYOUR_ADDRESS",
    expiration:    Math.floor(Date.now() / 1000) + 3600,
    gas_mode:      "receive_less",
  }),
});
const quote = await response.json();
console.log(quote);

The response includes a uuid and route_params — these are the parameters you'll sign with your wallet.

Step 3: Sign and Execute the Swap

Sign the route_params using EIP-712 typed data signing, then submit:

from eth_account import Account
from eth_account.messages import encode_typed_data

# `domain` and `INTENT_TYPES` are documented under Authentication
signable  = encode_typed_data(domain, INTENT_TYPES, quote["route_params"])
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

submit_body = {"uuid": quote["uuid"], "signature": "0x" + signature.removeprefix("0x")}
permit = quote.get("permit") or {}
if permit.get("permit_required") and permit.get("permit_supported"):
    typed = permit["eip712"]
    permit_message = encode_typed_data(typed["domain"], typed["types"], typed["message"])
    permit_signature = Account.from_key(PRIVATE_KEY).sign_message(permit_message).signature.hex()
    submit_body["permit_signature"] = "0x" + permit_signature.removeprefix("0x")
    submit_body["permit_deadline"] = int(typed["message"]["deadline"])
elif permit.get("permit_required") and int(permit["current_allowance_raw"]) < int(permit["value_raw"]):
    raise RuntimeError("Approve the quoted spender, confirm the transaction, then request a fresh quote")

submit = requests.post(
    "https://api.testnet.sera.cx/api/v1/swap",
    json=submit_body,
    timeout=10,
).json()
// `domain` and `INTENT_TYPES` are documented under Authentication
const signature = await signer.signTypedData(domain, INTENT_TYPES, quote.route_params);

const submitBody: { uuid: string; signature: string; permit_signature?: string; permit_deadline?: number } = {
  uuid: quote.uuid, signature,
};
const permit = quote.permit;
if (permit?.permit_required && permit.permit_supported) {
  const typed = permit.eip712;
  submitBody.permit_signature = await signer.signTypedData(typed.domain, typed.types, typed.message);
  submitBody.permit_deadline = Number(typed.message.deadline);
} else if (permit?.permit_required && BigInt(permit.current_allowance_raw) < BigInt(permit.value_raw)) {
  throw new Error("Approve the quoted spender, confirm the transaction, then request a fresh quote");
}

const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(submitBody),
});

A quote authorizes one execution. Persist the quote uuid and complete signed body. If a timeout or 503 leaves the response unknown, retry that identical body to recover a stored result. Once POST /swap returns trade_id, query GET /orders/{trade_id} with your API key. The quote uuid and the resulting trade_id are different identifiers; do not query an order using the quote ID. Do not create another swap while the first outcome is unknown.

For detailed signing instructions, see Authentication.

Step 4: Check Your Balances

Create an API key to query your balances and order history. The /balances endpoint returns both your wallet balance (tokens in your Ethereum wallet) and your Vault balance (tokens deposited for limit order trading), along with any frozen amounts locked in open orders.

For balances, vault_available is the accounting amount, not guaranteed spendable funds. Inspect vault_spendable and the recovery fields, including zero-balance recovery rows. Read /config capabilities before offering deposits, instant withdrawals or balance sync. Use balance synchronization when available; never deposit twice to fix a delayed balance.

balances = requests.get(
    "https://api.testnet.sera.cx/api/v1/balances",
    params={"owner_address": "0xYOUR_ADDRESS"},
    headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
    timeout=10,
).json()["balances"]

for bal in balances:
    print(f"{bal['symbol']}:")
    print(f"  Wallet:         {bal['wallet_balance']}")
    print(f"  Vault available: {bal['vault_available']}")
    print(f"  Vault spendable: {bal.get('vault_spendable')} ({bal.get('spendability_status')})")
    print(f"  Vault frozen:    {bal['vault_frozen']}")
const response = await fetch(
  "https://api.testnet.sera.cx/api/v1/balances?owner_address=0xYOUR_ADDRESS",
  { headers: { "Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET" } },
);
const { balances } = await response.json();

for (const bal of balances) {
  console.log(`${bal.symbol}:`);
  console.log(`  Wallet:          ${bal.wallet_balance}`);
  console.log(`  Vault available: ${bal.vault_available}`);
  console.log(`  Vault spendable: ${bal.vault_spendable} (${bal.spendability_status})`);
  console.log(`  Vault frozen:    ${bal.vault_frozen}`);
}

Using the Web App

You can also trade directly through the Sera web interface:

  1. Visit testnet.sera.cx
  2. Connect your wallet
  3. Select a currency pair
  4. Place a limit order or execute an instant swap
  5. Monitor your orders in the dashboard

Next Steps

For an unsupported permit token with insufficient allowance, use approve and transaction broadcast, wait for confirmation, and request a fresh quote before repeating the signing step. For target-output swaps, follow the Exact Output flow.

For ordinary Swap retries, preserve the entire signed body and follow submission identity and retries. A successful identical retry can return the original result.