Skip to content

Swap Trading

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.

Swaps provide instant execution for traders who want to exchange tokens immediately at the best available price. Unlike limit orders, swaps are Fill-or-Kill — they either execute in full or are rejected.

Swap Flow

sequenceDiagram
    participant User
    participant API as Sera API
    participant Chain as Ethereum

    User->>API: POST /swap/quote
    API-->>User: Quote (uuid, route_params, quote_breakdown)
    User->>User: Sign route_params (EIP-712)
    User->>API: POST /swap (uuid + signature)
    API->>Chain: Settlement
    Chain-->>API: Confirmation
    API-->>User: Success (trade_id)

Step 1: Request a Quote

import time, requests

quote = requests.post(
    "https://api.testnet.sera.cx/api/v1/swap/quote",
    json={
        "from_token":    "0x965d4b...d0e",   # USDC address
        "to_token":      "0xef64d1...40f",    # EURC address
        "from_amount":   "1000000000",      # 1000 USDC (6 decimals)
        "owner_address": "0xYOUR_WALLET",
        "recipient":     "0xYOUR_WALLET",   # may be a different address
        "expiration":    int(time.time()) + 3600,
        "gas_mode":      "receive_less",
    },
    timeout=10,
).json()
# quote["uuid"]           — unique quote identifier
# quote["route_params"]   — EIP-712 parameters to sign
# quote["quote_breakdown"] — before/after gas amounts for display
# quote["fee_breakdown"]   — legacy gas summary (gas_cost_usd, gas_cost_from_token)
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:    "0x965d4b...d0e",   // USDC address
    to_token:      "0xef64d1...40f",    // EURC address
    from_amount:   "1000000000",      // 1000 USDC (6 decimals)
    owner_address: "0xYOUR_WALLET",
    recipient:     "0xYOUR_WALLET",   // may be a different address
    expiration:    Math.floor(Date.now() / 1000) + 3600,
    gas_mode:      "receive_less",
  }),
});

const quote = await response.json();
// quote.uuid            — unique quote identifier
// quote.route_params    — EIP-712 parameters to sign
// quote.quote_breakdown — before/after gas amounts for display
// quote.fee_breakdown   — legacy gas summary (gas_cost_usd, gas_cost_from_token)

Quotes are single-use

POST /swap atomically consumes the stored quote on the first valid submission. If execution fails after the quote is consumed, request a new quote instead of retrying the same uuid.

Quote Parameters

Parameter Type Description
from_token address ERC-20 address of the input token
to_token address ERC-20 address of the output token
from_amount string Amount in raw token units (e.g., "1000000000" for 1000 USDC)
owner_address address Your wallet address
recipient address Where output tokens are delivered. Can be any address — set to a different wallet to send the swap output to a third party.
expiration integer Unix timestamp deadline
gas_mode string "receive_less" or "pay_more"

Quote Response

The response includes:

  • uuid — A unique identifier for this quote (used when submitting)
  • route_params — The EIP-712 Intent struct fields to sign
  • quote_breakdown — Display-only before/after gas amounts in raw token units
  • fee_breakdown — Legacy gas summary: gas_cost_usd and gas_cost_from_token
  • expires_at — When the quote expires (quotes are one-time use)

Step 2: Sign the Quote

Sign the route_params using EIP-712 typed data signing with your wallet:

from eth_account import Account
from eth_account.messages import encode_typed_data

DOMAIN = {
    "name": "Sera",
    "version": "1",
    "chainId": 11155111,                                              # Sepolia
    "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",  # Sera.sol
}

INTENT_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"},
    ]
}

# Sign quote["route_params"] exactly as returned by POST /swap/quote
signable  = encode_typed_data(DOMAIN, INTENT_TYPES, quote["route_params"])
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
import { Wallet } from "ethers";

const DOMAIN = {
  name: "Sera",
  version: "1",
  chainId: 11155111,                                                  // Sepolia
  verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",     // Sera.sol
};

const INTENT_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"  },
  ],
};

// Sign quote.route_params exactly as returned by POST /swap/quote
const signature = await signer.signTypedData(DOMAIN, INTENT_TYPES, quote.route_params);

Step 3: Execute the Swap

Submit the signed quote:

swap = requests.post(
    "https://api.testnet.sera.cx/api/v1/swap",
    json={"uuid": quote["uuid"], "signature": "0x" + signature.lstrip("0x")},
    timeout=10,
).json()
# swap["success"]  — whether the swap was accepted
# swap["trade_id"] — unique trade identifier for tracking
const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ uuid: quote.uuid, signature }),
});

const swap = await result.json();
// swap.success  — whether the swap was accepted
// swap.trade_id — unique trade identifier for tracking

Gas Modes

Unlike limit orders (where you pay gas in real ETH), swap gas costs are automatically factored into the quote by the server. You do not need to hold ETH to execute a swap — the gas is absorbed into the token amounts.

When requesting a quote, you choose how the gas cost is applied:

Mode Behavior
receive_less Gas cost is deducted from output. You spend exactly from_amount, but receive slightly less.
pay_more Gas cost is added to input. You receive the full quoted amount, but spend slightly more.

The quote_breakdown in the quote response shows the before-gas amount, gas amount, and after-gas amount in raw token units. Use it for frontend display. The fee_breakdown field remains available as a legacy gas summary in USD and input-token display units.

Your frontend should sign route_params exactly as returned. Do not recompute signed amounts from display fields.

Pricing Adjustments

Some same-currency swaps, such as USDC↔USDT, may be quoted without a cross-currency FX adjustment because both tokens track the same fiat currency. For all swaps, the public quote response exposes the signed amounts and gas display math only.

Multi-Leg Routing

Sera automatically finds optimal routes for your swap. If a direct pair doesn't exist or a multi-hop path offers better pricing, the swap routes through intermediate currencies transparently.

For example, a GBP → SGD swap might execute as:

  1. GBP → USD
  2. USD → SGD

This happens atomically — either all legs succeed or none do.

Why MEV Is Not an Issue

Sera swaps are quote-first. You do not broadcast an open-ended market order for public price discovery.

That means users are not exposing an open-ended market order to the public mempool for price discovery. Instead, you request a quote, sign the exact route_params, and settlement either executes within the signed bounds or fails.

The signed Intent includes maxInputAmount, minOutputAmount, a one-time uuid, and a deadline. Because the trade is not being discovered and repriced in the public mempool, the usual sandwich-attack vector does not exist.

Error Handling

Every 4xx response from POST /swap returns a typed envelope:

{
  "detail": {
    "detail": "Quote was rejected; request a fresh quote",
    "error_code": "QUOTE_STALE"
  }
}

Branch on error_code for routing logic; the inner detail is a human-readable string for display only. The full code list is documented in the Swap endpoints reference.

HTTP Status Meaning
200 Swap accepted and processing
400 Invalid request, signature mismatch, missing permit fields, or non-executable quote
409 error_code: "QUOTE_STALE" — the wallet's pending swap state changed between quote and submit, or the quote can no longer be accepted. The quote is not consumed; silently re-quote and re-submit
410 Quote expired or already consumed (request a new quote)
429 Rate limit exceeded (wait and retry)
503 Service temporarily unavailable