Skip to main content

Order Lifecycle Tutorial

This tutorial demonstrates how to build a complete trading script that uses both the GraphQL API (for reading data) and Smart Contracts (for executing trades) on Sera Protocol.

What You’ll Learn

  1. Query market information via GraphQL
  2. Fetch order book depth via GraphQL
  3. Place a limit order via Smart Contract
  4. Monitor order status via GraphQL
  5. Claim proceeds via Smart Contract

Prerequisites

  • Python 3.9+
  • An Ethereum wallet with Sepolia testnet ETH
  • Testnet stablecoins (users are airdropped 10M tokens of each supported stablecoin)

Setup

1. Install Dependencies

pip install web3 requests python-dotenv

2. Create Environment File

Create a .env file with your private key:
# Your wallet private key (NEVER share this!)
PRIVATE_KEY="0x..."

# Sepolia RPC endpoint (optional, has default)
SEPOLIA_RPC_URL="https://0xrpc.io/sep"
The script uses the live EURC/XSGD stablecoin market by default. All testnet users are airdropped 10M tokens of each supported stablecoin.
Never commit your private key to version control. Add .env to your .gitignore.

Contract ABIs

The script uses minimal ABI definitions to interact with the smart contracts. For the complete ABI reference, see the Market Router documentation.
Router ABI (limitBid function):
{
  "name": "limitBid",
  "type": "function",
  "stateMutability": "payable",
  "inputs": [{
    "name": "params",
    "type": "tuple",
    "components": [
      { "name": "market", "type": "address" },
      { "name": "deadline", "type": "uint64" },
      { "name": "claimBounty", "type": "uint32" },
      { "name": "user", "type": "address" },
      { "name": "priceIndex", "type": "uint16" },
      { "name": "rawAmount", "type": "uint64" },
      { "name": "postOnly", "type": "bool" },
      { "name": "useNative", "type": "bool" },
      { "name": "baseAmount", "type": "uint256" }
    ]
  }],
  "outputs": [{ "name": "", "type": "uint256" }]
}
The claimBounty field is reserved for future use. Always set it to 0.
ERC20 ABI (for token approvals):
[
  { "name": "approve", "type": "function", "inputs": [{ "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" }], "outputs": [{ "name": "", "type": "bool" }] },
  { "name": "allowance", "type": "function", "stateMutability": "view", "inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }], "outputs": [{ "name": "", "type": "uint256" }] }
]

The Complete Script

Below is a ~300-line Python script that demonstrates the complete order lifecycle.
#!/usr/bin/env python3
"""
Sera Protocol - Order Lifecycle Demo

This script demonstrates the complete order lifecycle on Sera Protocol:
1. Query available markets (GraphQL)
2. Get current order book depth (GraphQL)
3. Place a limit order (Smart Contract)
4. Monitor order status (GraphQL)
5. Claim proceeds when filled (Smart Contract)
"""

import os
import sys
import time
import requests
from decimal import Decimal
from typing import Optional, Dict, Any, List
from dotenv import load_dotenv
from web3 import Web3
from eth_account import Account

# =============================================================================
# CONFIGURATION
# =============================================================================

load_dotenv()

PRIVATE_KEY = os.getenv("PRIVATE_KEY")
RPC_URL = os.getenv("SEPOLIA_RPC_URL", "https://0xrpc.io/sep")

# Sera Protocol contract addresses (Sepolia testnet)
ROUTER_ADDRESS = "0x82bfe1b31b6c1c3d201a0256416a18d93331d99e"

# EURC/XSGD market - a live stablecoin pair with active trading
MARKET_ADDRESS = "0x2e4a11c7711c6a69ac973cbc40a9b16d14f9aa7e"

# GraphQL API endpoint
SUBGRAPH_URL = "https://api.goldsky.com/api/public/project_cmicv6kkbhyto01u3agb155hg/subgraphs/sera-pro/1.0.9/gn"

# Minimal ABIs
ROUTER_ABI = [
    {
        "name": "limitBid",
        "type": "function",
        "stateMutability": "payable",
        "inputs": [{
            "name": "params",
            "type": "tuple",
            "components": [
                {"name": "market", "type": "address"},
                {"name": "deadline", "type": "uint64"},
                {"name": "claimBounty", "type": "uint32"},
                {"name": "user", "type": "address"},
                {"name": "priceIndex", "type": "uint16"},
                {"name": "rawAmount", "type": "uint64"},
                {"name": "postOnly", "type": "bool"},
                {"name": "useNative", "type": "bool"},
                {"name": "baseAmount", "type": "uint256"},
            ]
        }],
        "outputs": [{"name": "", "type": "uint256"}]
    }
]

ERC20_ABI = [
    {"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"name": "", "type": "bool"}]},
    {"name": "allowance", "type": "function", "stateMutability": "view", "inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
]

# =============================================================================
# GRAPHQL HELPERS
# =============================================================================

def query_subgraph(query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
    """Execute a GraphQL query."""
    response = requests.post(
        SUBGRAPH_URL,
        json={"query": query, "variables": variables or {}},
        headers={"Content-Type": "application/json"}
    )
    result = response.json()
    if "errors" in result:
        raise Exception(f"GraphQL Error: {result['errors'][0]['message']}")
    return result["data"]


def get_market_info(market_id: str) -> Dict[str, Any]:
    """Fetch market information."""
    query = """
    query GetMarket($id: ID!) {
        market(id: $id) {
            id
            quoteToken { id symbol decimals }
            baseToken { id symbol decimals }
            quoteUnit
            minPrice
            tickSpace
            latestPrice
            latestPriceIndex
        }
    }
    """
    return query_subgraph(query, {"id": market_id.lower()})["market"]


def get_order_book(market_id: str) -> Dict[str, List]:
    """Fetch order book depth."""
    query = """
    query GetDepth($market: String!) {
        bids: depths(
            where: { market: $market, isBid: true, rawAmount_gt: "0" }
            orderBy: priceIndex, orderDirection: desc, first: 10
        ) { priceIndex price rawAmount }
        asks: depths(
            where: { market: $market, isBid: false, rawAmount_gt: "0" }
            orderBy: priceIndex, orderDirection: asc, first: 10
        ) { priceIndex price rawAmount }
    }
    """
    return query_subgraph(query, {"market": market_id.lower()})


def get_user_orders(user: str, market_id: str) -> List[Dict]:
    """Fetch user's orders."""
    query = """
    query GetOrders($user: String!, $market: String!) {
        openOrders(
            where: { user: $user, market: $market }
            orderBy: createdAt, orderDirection: desc, first: 20
        ) {
            id priceIndex isBid rawAmount rawFilledAmount claimableAmount status orderIndex
        }
    }
    """
    return query_subgraph(query, {"user": user.lower(), "market": market_id.lower()})["openOrders"]

# =============================================================================
# CONTRACT HELPERS
# =============================================================================

def approve_token(w3, account, token_address, spender, amount):
    """Approve token spending."""
    token = w3.eth.contract(address=Web3.to_checksum_address(token_address), abi=ERC20_ABI)
    if token.functions.allowance(account.address, Web3.to_checksum_address(spender)).call() >= amount:
        return None
    
    tx = token.functions.approve(Web3.to_checksum_address(spender), amount).build_transaction({
        "from": account.address,
        "nonce": w3.eth.get_transaction_count(account.address, 'pending'),
        "gas": 100000,
        "gasPrice": int(w3.eth.gas_price * 1.2)
    })
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    w3.eth.wait_for_transaction_receipt(tx_hash)
    return tx_hash.hex()


def place_limit_bid(w3, account, market, price_index, raw_amount):
    """Place a limit bid order."""
    router = w3.eth.contract(address=Web3.to_checksum_address(ROUTER_ADDRESS), abi=ROUTER_ABI)
    
    params = (
        Web3.to_checksum_address(market),
        int(time.time()) + 3600,  # deadline
        0,                         # claimBounty
        account.address,
        price_index,
        raw_amount,
        True,   # postOnly
        False,  # useNative
        0       # baseAmount
    )
    
    tx = router.functions.limitBid(params).build_transaction({
        "from": account.address,
        "nonce": w3.eth.get_transaction_count(account.address, 'pending'),
        "gas": 500000,
        "gasPrice": int(w3.eth.gas_price * 1.2),
        "value": 0
    })
    
    signed = account.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt

# =============================================================================
# MAIN
# =============================================================================

def main():
    print("Sera Protocol - Order Lifecycle Demo\n")
    
    # Setup
    w3 = Web3(Web3.HTTPProvider(RPC_URL))
    account = Account.from_key(PRIVATE_KEY)
    print(f"Wallet: {account.address}")
    
    # 1. Get market info
    market = get_market_info(MARKET_ADDRESS)
    print(f"Market: {market['baseToken']['symbol']}/{market['quoteToken']['symbol']}")
    
    # 2. Get order book
    depth = get_order_book(MARKET_ADDRESS)
    print(f"Bids: {len(depth['bids'])}, Asks: {len(depth['asks'])}")
    
    # 3. Check existing orders
    orders = get_user_orders(account.address, MARKET_ADDRESS)
    print(f"Your orders: {len(orders)}")
    
    # 4. Place a limit order
    price_index = max(1, int(market["latestPriceIndex"]) - 100)
    raw_amount = 1000
    
    approve_token(w3, account, market["quoteToken"]["id"], ROUTER_ADDRESS, raw_amount * int(market["quoteUnit"]))
    receipt = place_limit_bid(w3, account, MARKET_ADDRESS, price_index, raw_amount)
    print(f"Order placed in block {receipt['blockNumber']}")
    
    # 5. Verify
    time.sleep(3)
    updated_orders = get_user_orders(account.address, MARKET_ADDRESS)
    print(f"Updated orders: {len(updated_orders)}")

if __name__ == "__main__":
    main()

Step-by-Step Breakdown

Step 1: Query Market Info (GraphQL)

First, we fetch market parameters using the GraphQL API:
def get_market_info(market_id: str) -> Dict[str, Any]:
    query = """
    query GetMarket($id: ID!) {
        market(id: $id) {
            id
            quoteToken { id symbol decimals }
            baseToken { id symbol decimals }
            quoteUnit
            minPrice
            tickSpace
            latestPriceIndex
        }
    }
    """
    response = requests.post(SUBGRAPH_URL, json={"query": query, "variables": {"id": market_id.lower()}})
    return response.json()["data"]["market"]
This returns essential information like:
  • Token addresses and symbols
  • quoteUnit for amount conversions
  • minPrice and tickSpace for price calculations

Step 2: Fetch Order Book Depth (GraphQL)

Query current bids and asks:
query = """
query GetDepth($market: String!) {
    bids: depths(
        where: { market: $market, isBid: true, rawAmount_gt: "0" }
        orderBy: priceIndex
        orderDirection: desc
        first: 10
    ) {
        priceIndex
        price
        rawAmount
    }
    asks: depths(
        where: { market: $market, isBid: false, rawAmount_gt: "0" }
        orderBy: priceIndex
        orderDirection: asc
        first: 10
    ) {
        priceIndex
        price
        rawAmount
    }
}
"""

Step 3: Place a Limit Order (Smart Contract)

Connect to the router contract and submit a limit bid:
from web3 import Web3

w3 = Web3(Web3.HTTPProvider(RPC_URL))
router = w3.eth.contract(address=ROUTER_ADDRESS, abi=ROUTER_ABI)

params = (
    market_address,      # market
    deadline,            # Unix timestamp
    0,                   # claimBounty (unused)
    user_address,        # your address
    price_index,         # price book index
    raw_amount,          # quote amount in raw units
    True,                # postOnly
    False,               # useNative
    0                    # baseAmount (not used for bids)
)

tx = router.functions.limitBid(params).build_transaction({...})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)

Step 4: Monitor Order Status (GraphQL)

Poll for order updates:
query = """
query GetOrders($user: String!, $market: String!) {
    openOrders(
        where: { user: $user, market: $market }
        orderBy: createdAt
        orderDirection: desc
    ) {
        priceIndex
        rawAmount
        rawFilledAmount
        claimableAmount
        status
    }
}
"""
Order statuses:
  • open - Order is active on the book
  • partial - Partially filled
  • filled - Completely filled
  • cancelled - Cancelled by user
  • claimed - Proceeds have been claimed

Step 5: Claim Proceeds (Smart Contract)

When your order is filled, claim your tokens:
claim_params = [(
    market_address,
    [(is_bid, price_index, order_index)]  # OrderKey
)]

tx = router.functions.claim(deadline, claim_params).build_transaction({...})

Running the Demo

# Set up environment
export PRIVATE_KEY="0x..."

# Run the script
python order_lifecycle.py
Expected output:
============================================================
  Sera Protocol - Order Lifecycle Demo
============================================================

[1/6] Connecting to Ethereum Sepolia...
  ✓ Connected to chain ID: 11155111
  ✓ Wallet address: 0x...

[2/6] Fetching market info (GraphQL)...
  ✓ Market: TWETH/TUSDC
  ✓ Quote unit: 1000

[3/6] Fetching order book depth (GraphQL)...
  BIDS                 |                 ASKS
  100.0000 @ 9950000   |   10000 @ 100.0100

[4/6] Checking your existing orders (GraphQL)...
  Found 5 order(s)

[5/6] Placing a limit bid order (Smart Contract)...
  Transaction sent: 0x1db79ea...
  ✓ Order placed in block 9802274

[6/6] Verifying order status (GraphQL)...
  Status: pending

Key Takeaways

OperationAPI UsedDescription
Read market dataGraphQLFast, no gas cost
Read order bookGraphQLReal-time depth
Read order statusGraphQLPolling for updates
Place ordersSmart ContractRequires gas + approval
Cancel ordersSmart ContractVia OrderCanceler
Claim proceedsSmart ContractVia Router.claim()
Use GraphQL for all read operations (free and fast). Only use smart contracts when you need to modify state (place/cancel/claim).

Next Steps