Getting Started
Quick start for OBSDN API integration
Use this guide to register a signer, fetch market metadata, and submit your first order.
Overview
OBSDN is a perpetual futures exchange with off-chain matching and on-chain settlement.
- Perpetual contracts have no expiry.
- Orders match off-chain.
- Settlement and custody are handled on-chain.
Base URL
https://api.obsdn.tradeAll successful responses are wrapped in a {"data": ..., "request_id": "..."} envelope. The examples below show the unwrapped payloads — read the actual data field on the response.
Integration Flow
1. Register signer -> 2. Store API key -> 3. Fund account -> 4. TradeStep 1: Register a Signer
A signer is a wallet authorized to sign orders for the sender. You need:
- A sender wallet — owns the funds
- A signer wallet — authorized to sign orders on the sender's behalf
Both wallets sign EIP-712 typed data; see the Signing Guide for the exact Register (sender) and DelegatedSigner (signer) payloads.
curl -X POST https://api.obsdn.trade/auth/signers \
-H "Content-Type: application/json" \
-d '{
"sndr_addr": "0xYourSenderWalletAddress",
"signer_addr": "0xYourSignerWalletAddress",
"sndr_sig": "0x...",
"signer_sig": "0x...",
"nonce": "1234567890000000000",
"msg": "Register signer",
"nm": "My Trading Bot"
}'nonce is a uint64 and must be sent as a JSON string (the value exceeds JavaScript's safe integer range). The msg field must match what the sender wallet signed.
The response (under data) returns the first API key pair for this signer:
{
"api_key": {
"api_key": "obsdn_abc123...",
"api_secret": "secret_xyz789...",
"nm": "My Trading Bot",
"sndr": "0x...",
"signer": "0x...",
"crt_ts": "1734000000000000000",
"exp_ts": "1765536000000000000",
"is_ro": false
}
}Save api_secret immediately — it is shown only once. See Authentication for full credential management.
Step 2: Fetch Available Markets
GET /markets is unauthenticated and is a good first call to verify connectivity:
curl https://api.obsdn.trade/marketsThe full envelope (one market shown; live response includes every listed market):
{
"data": {
"mkts": [
{
"idx": "1",
"mkt_id": "BTC-PERP",
"disp_name": "BTC-PERP",
"disp_base_sym": "BTC",
"enabled": true,
"visible": true,
"post_only": false,
"base_sym": "BTC",
"quote_sym": "USDC",
"base_incr": "0.0001",
"price_incr": "1",
"max_lev": "20",
"min_sz": "0.0001",
"vol_24h": "256.5447",
"qvol_24h": "20289781.4592",
"chg_24h": "1360",
"hi_24h": "80636",
"lo_24h": "78295",
"last_px": "79751",
"oi": "16082.4627",
"fund_intv": "3600000000000",
"next_fund_ts": "1777885200000000000",
"pred_fund_rt": "0.000016",
"mark_px": "79751",
"idx_px": "79694.122815249997074715",
"oi_cap": "0",
"tags": ["crypto"],
"icon_url": "https://assets-staging.obsdn.trade/images/icons/1775321507167-btc.svg"
}
]
},
"request_id": "245d998f-e8de-4900-a9f2-0fe40623c971"
}Numeric fields are returned as decimal strings to preserve precision. Timestamps (next_fund_ts, fund_intv) are nanoseconds. Subsequent examples in this guide strip the data / request_id envelope for brevity — see the markets endpoint reference for the full schema.
Step 3: Authenticate Requests
Protected REST endpoints are authenticated with three headers and an HMAC signature:
| Header | Value |
|---|---|
x-api-key | API key identifier from Step 1 |
x-api-timestamp | Current Unix time in seconds (string) |
x-api-signature | base64(HMAC-SHA256(secret, timestamp + method + path + body)) |
The signature payload concatenates the timestamp, the uppercase HTTP method, the URL path (no scheme, host, or query string), and the raw request body. Use the API secret as the HMAC key. Requests are rejected if the timestamp is more than 5 seconds off the server clock, so regenerate the signature for every call.
TS=$(date +%s)
METHOD=GET
URL_PATH=/portfolio
BODY=""
SIG=$(printf '%s%s%s%s' "$TS" "$METHOD" "$URL_PATH" "$BODY" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary \
| base64)
curl "https://api.obsdn.trade$URL_PATH" \
-H "x-api-key: $API_KEY" \
-H "x-api-timestamp: $TS" \
-H "x-api-signature: $SIG"See Authentication for managing keys.
Step 4: Fund the Account
Deposits happen on-chain — there is no REST endpoint for moving funds onto the exchange. Call deposit on the Obsdn contract; the credit appears on the sender's portfolio once the deposit event is confirmed on-chain. Poll GET /portfolio or subscribe to the WebSocket portfolio channel to detect when the balance updates.
The Obsdn contract exposes two overloads:
/// Deposit collateral for the caller (msg.sender)
function deposit(address token, uint128 amountX18) external;
/// Deposit collateral on behalf of another main account
function deposit(address recipient, address token, uint128 amountX18) external;tokenmust be a registered collateral asset. Fetch the supported tokens (and their on-chain addresses + decimals) viaGET /assets.amountX18is the deposit size in 18-decimal precision, regardless of the token's native decimals. The contract converts to the token's raw amount internally; values that don't round-trip cleanly are rejected.- The recipient must be a Main account (sub-accounts and vaults can't be funded directly).
- You must
approve(obsdn, rawAmount)on the ERC-20 first —rawAmountis the deposit size in the token's native decimals, not 18.
Fetch the Obsdn contract address and chain metadata at runtime via GET /chain/config, or look it up on the Deployed Contracts page.
import { Contract, parseUnits } from "ethers"
const obsdn = new Contract(OBSDN_ADDRESS, obsdnAbi, signer)
const usdc = new Contract(USDC_ADDRESS, erc20Abi, signer)
const amount = "1000" // 1,000 USDC
const usdcDecimals = await usdc.decimals() // 6 for USDC
const rawAmount = parseUnits(amount, usdcDecimals) // 1_000_000_000
const amountX18 = parseUnits(amount, 18) // 1_000_000_000_000_000_000_000
// Approve the exchange to pull `rawAmount` of USDC. .wait() blocks
// until the tx is mined; otherwise the deposit below races the approval.
const approveTx = await usdc.approve(OBSDN_ADDRESS, rawAmount)
await approveTx.wait()
const depositTx = await obsdn["deposit(address,uint128)"](USDC_ADDRESS, amountX18)
await depositTx.wait()Step 5: Place Your First Order
Place a limit buy order for BTC-PERP. Field names use the wire format from PlaceOrderRequest; enums are sent as their full proto names (e.g., ORDER_SIDE_BUY, not BUY).
Recompute $TS and $SIG for this request — the prehash now becomes ${TS}POST/orders${BODY}, which differs from the GET /portfolio example in Step 3:
BODY='{"mkt_id":"BTC-PERP","sd":"ORDER_SIDE_BUY","ot":"ORDER_TYPE_LIMIT","sz":0.001,"px":40000,"tif":"TIME_IN_FORCE_GTC","po":false,"ro":false,"stp":"SELF_TRADE_PREVENTION_UNSPECIFIED","stop_t":"STOP_TYPE_UNSPECIFIED","stop_px_type":"STOP_PRICE_TYPE_UNSPECIFIED","await":false,"nonce":"1700000000000000000","sig":"0x..."}'
TS=$(date +%s)
SIG=$(printf '%s%s%s%s' "$TS" "POST" "/orders" "$BODY" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary \
| base64)
curl -X POST https://api.obsdn.trade/orders \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-H "x-api-timestamp: $TS" \
-H "x-api-signature: $SIG" \
-d "$BODY"Required fields:
| Field | Description |
|---|---|
mkt_id | Market identifier (e.g., BTC-PERP) |
sd | Side: ORDER_SIDE_BUY, ORDER_SIDE_SELL |
ot | Order type: ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET, ORDER_TYPE_STOP, ORDER_TYPE_TWAP |
sz | Order size (number; in base asset units) |
px | Limit price (number; in quote asset units) |
tif | Time-in-force: TIME_IN_FORCE_GTC, TIME_IN_FORCE_IOC, TIME_IN_FORCE_FOK, TIME_IN_FORCE_GTT |
po | Post-only: reject if the order would take liquidity |
ro | Reduce-only: only reduce existing position size |
stp | Self-trade prevention — SELF_TRADE_PREVENTION_UNSPECIFIED (engine applies _CANCEL_TAKER), _CANCEL_TAKER, _CANCEL_MAKER, _CANCEL_BOTH |
stop_t | Stop subtype — STOP_TYPE_UNSPECIFIED (non-stop order), STOP_TYPE_STOP_LOSS, STOP_TYPE_TAKE_PROFIT |
stop_px_type | Trigger reference — STOP_PRICE_TYPE_UNSPECIFIED (non-stop order), STOP_PRICE_TYPE_LAST, STOP_PRICE_TYPE_MARK |
await | If true, the response blocks until the order is matched/rested; if false, returns once accepted |
nonce | Unix nanoseconds; included in the EIP-712 payload |
sig | EIP-712 signature from the registered signer |
Optional fields:
| Field | Description |
|---|---|
cl_oid | Client order ID |
stop_px | Trigger price for stop orders (required when stop_t is set) |
exp_ts | Unix nanoseconds; required when tif is TIME_IN_FORCE_GTT |
sched_ts | Unix nanoseconds; scheduled execution time for TWAP sub-orders |
See Order Types and Signing Guide for payload construction.
Step 6: Inspect Account State
Fetch consolidated portfolio state (balances, positions, margin) using the same signed-header recipe from Step 3 — recompute $TS and $SIG because the prehash (${TS}GET/portfolio) differs from the previous request:
TS=$(date +%s)
SIG=$(printf '%s%s%s%s' "$TS" "GET" "/portfolio" "" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary \
| base64)
curl https://api.obsdn.trade/portfolio \
-H "x-api-key: $API_KEY" \
-H "x-api-timestamp: $TS" \
-H "x-api-signature: $SIG"Order Types
| Wire value | Description |
|---|---|
ORDER_TYPE_LIMIT | Execute at specified price or better |
ORDER_TYPE_MARKET | Execute immediately at best available price |
ORDER_TYPE_STOP | Trigger when stop price is reached; subtype is stop_t (STOP_TYPE_STOP_LOSS or STOP_TYPE_TAKE_PROFIT) |
ORDER_TYPE_TWAP | Time-weighted execution split across scheduled sub-orders |
See Order Types for execution behavior and flags.
Time in Force
| Wire value | Description |
|---|---|
TIME_IN_FORCE_GTC | Good Till Canceled — remains until filled or canceled |
TIME_IN_FORCE_IOC | Immediate or Cancel — fill what's possible, cancel remainder |
TIME_IN_FORCE_FOK | Fill or Kill — fill completely or cancel entirely |
TIME_IN_FORCE_GTT | Good Till Time — expires at exp_ts (nanoseconds) |
Real-Time Updates
Connect to WebSocket for live market data and private updates:
const ws = new WebSocket("wss://pulse.obsdn.trade/ws")
ws.onopen = () => {
// Public channel — no auth needed
ws.send(
JSON.stringify({
op: "sub",
channel: "book",
params: { market: "BTC-PERP" },
})
)
// Authenticate before subscribing to private channels.
// timestamp = Unix seconds (string); rejected if more than 60s off the server clock.
// signature = base64(HMAC-SHA256(api_secret, `${api_key},${timestamp}`)).
ws.send(
JSON.stringify({
op: "auth",
params: {
key: "your-api-key",
timestamp: "1734567890",
signature: "your-ws-signature",
},
})
)
// Private channel — requires auth
ws.send(JSON.stringify({ op: "sub", channel: "order" }))
}
ws.onmessage = (event) => {
console.log(JSON.parse(event.data))
}WebSocket auth uses the same HMAC scheme as REST, but with a simpler prehash (${api_key},${timestamp}) sent inside the auth message instead of HTTP headers. See WebSocket Overview for the full signature recipe, channels, and reconnect behavior.
Next Steps
- Authentication — signer setup and request headers
- Signing Guide — EIP-712 payloads for orders, registration, and withdrawals
- Order Types — order behavior, TIF, and flags
- WebSocket Overview — market data and private streams
- Error Handling — API errors and retry guidance
- Markets API — market metadata
- Orders API — order management