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¶
- Query market information via GraphQL
- Fetch order book depth via GraphQL
- Place a limit order via Smart Contract
- Monitor order status via GraphQL
- 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¶
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"
Note
The script uses the live EURC/XSGD stablecoin market by default. All testnet users are airdropped 10M tokens of each supported stablecoin.
Warning
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.
View Minimal ABIs Used
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" }]
}
Note
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.
View Full Script
#!/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
quoteUnitfor amount conversionsminPriceandtickSpacefor 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 bookpartial- Partially filledfilled- Completely filledcancelled- Cancelled by userclaimed- 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¶
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¶
| Operation | API Used | Description |
|---|---|---|
| Read market data | GraphQL | Fast, no gas cost |
| Read order book | GraphQL | Real-time depth |
| Read order status | GraphQL | Polling for updates |
| Place orders | Smart Contract | Requires gas + approval |
| Cancel orders | Smart Contract | Via OrderCanceler |
| Claim proceeds | Smart Contract | Via Router.claim() |
Tip
Use GraphQL for all read operations (free and fast). Only use smart contracts when you need to modify state (place/cancel/claim).
Next Steps¶
- Markets Query Reference - Full market query options
- Orders Query Reference - Order tracking queries
- Router Contract Reference - All router functions