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 convert 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)
    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["fee_breakdown"]  — gas cost details (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.fee_breakdown   — gas cost details (gas_cost_usd, gas_cost_from_token)

Preserve the signed request

A quote authorizes one execution. A retained successful execution can return its cached result for an identical retry. A timeout, 409 or 410 alone does not prove that nothing executed; reconcile the original identity before requesting a replacement.

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
  • fee_breakdown — Gas cost details: 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:

import json
from pathlib import Path

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_sig = Account.from_key(PRIVATE_KEY).sign_message(permit_message).signature.hex()
    submit_body["permit_signature"] = "0x" + permit_sig.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")

journal = Path("swap-requests")
journal.mkdir(mode=0o700, exist_ok=True)
# hex gives an opaque quote ID a filename without interpreting path separators.
saved_path = journal / (quote["uuid"].encode().hex() + ".json")
with saved_path.open("x", encoding="utf-8") as saved:
    json.dump(submit_body, saved)
response = requests.post("https://api.testnet.sera.cx/api/v1/swap",
                         json=submit_body, timeout=10)
response.raise_for_status()
swap = response.json()
# Accepted is not chain-confirmed: track trade_id via the authenticated order API.
import { mkdirSync, writeFileSync } from "node:fs";

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");
}
mkdirSync("swap-requests", { recursive: true, mode: 0o700 });
const savedPath = `swap-requests/${Buffer.from(quote.uuid).toString("hex")}.json`;
writeFileSync(savedPath, JSON.stringify(submitBody), { flag: "wx", mode: 0o600 });
const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify(submitBody),
});
if (!result.ok) throw new Error(`swap: ${result.status}; reconcile saved request`);
const swap = await result.json();
// Accepted is not chain-confirmed: track trade_id via the authenticated order API.

Gas Modes

Swap execution gas is included in the quote. The relayer sends the settlement transaction. A separate approve or other user-sent transaction still needs network gas; see Fees & Costs.

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 fee_breakdown in the quote response shows the exact gas cost so there are no surprises. The server computes the adjusted amounts — your frontend simply signs the route_params as-is.

When the Markup Is Skipped

The 0.1% cross-currency markup is operator-gated by the deployment-side env flag ENABLE_ORACLE_SLIPPAGE_CAPTURE. When the flag is off (default), no quote-side markup is applied — every quote, cross- or same-currency, prices straight off the orderbook (gas-mode adjustments still apply). When the flag is on, the markup is applied for cross-currency quotes and dropped in two cases:

  • Same-currency swaps between two pegs of the same fiat currency — for example USDC↔USDT, or two SGD-denominated stablecoins. There is no FX exposure to price in, and the two pegs typically cluster within a basis point of 1:1, so the quote prices straight off the orderbook (subject to the gas-mode adjustment you selected).
  • When the orderbook beats the oracle for a cross-currency pair. Sera quotes the worse of the two prices for the user; when that turns out to be the oracle (i.e. the OB would have given you more), the platform already retains the OB-vs-oracle spread and does not charge an additional markup on top.

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.

Signed Price Bounds and MEV

Quotes and routing are computed off chain. The signed maxInputAmount, minOutputAmount, uuid and deadline constrain settlement, which either respects those bounds or reverts. This reduces exposure to open-ended mempool price discovery; it does not establish immunity to ordering, service disruption or other attacks. Review the signed bounds and see the security model.

Error Handling

Categorized business errors can use detail.error_code; other failures can have a string detail or a validation array. Exact Output has its own detail.code contract. Do not assume every 4xx has the same shape.

Result Action
200 Accepted/cached result; use trade_id to query settlement, not HTTP success alone
400 / 422 Inspect validation, signature and funding errors; reconcile before changing an admitted request
409 May mean submitting, changed-payload conflict, stale quote or a pre-admission permit nonce conflict; inspect the response and original order
410 Quote missing/expired or retained result removed; it is not proof of non-execution
429 Honor Retry-After when present
Timeout / 503 Preserve IDs and the exact signed body; query and retry that body if appropriate

The execution example saves the request before sending (TypeScript uses Node.js). Keep saved files private. Do not rerun the quote/sign/generate flow as recovery from an unknown result. If approve is required, confirm it before obtaining a fresh quote. For permit, sign the returned Permit in addition to the Intent. Re-quote only after resolving the original request. See the complete error and retry rules. The current quote API has no client slippage parameter; never alter returned signed bounds silently.