Swap Trading¶
Network & Compatibility
| Resource | Value |
|---|---|
| API base URL | https://api.sera.cx/api/v1 |
| Chain | Ethereum mainnet (chainId 1) |
| Sera contract | 0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198 |
| Vault contract | 0xC7d4Fd2638e6630C8C61329878676b88A8A24D43 |
| SOR contract | 0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18 |
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, and many 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)
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.sera.cx/api/v1/swap/quote",
json={
"from_token": "0xDcAE...285b", # USDC address
"to_token": "0xd3Bd...779", # 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.sera.cx/api/v1/swap/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
from_token: "0xDcAE...285b", // USDC address
to_token: "0xd3Bd...779", // 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)
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 signfee_breakdown— Gas cost details:gas_cost_usdandgas_cost_from_tokenexpires_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": 1, # Mainnet
"verifyingContract": "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198", # 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: 1, // Mainnet
verifyingContract: "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198", // 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:
const result = await fetch("https://api.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 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 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:
- GBP → USD
- USD → SGD
This happens atomically — either all legs succeed or none do.
Reduced MEV Exposure¶
Sera swaps are not executed like public AMM swaps. Quote generation, routing, and matching happen off-chain in Sera's Web2 engine, and Ethereum is used only as the final settlement layer.
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, swaps have reduced exposure to the usual sandwich-attack path for open-ended AMM market orders.
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" — nonce or quote state changed between quote and submit. 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 |