BTCVAR30 prices are submitted in variance, displayed in volatility, and settled in variance.
Thin offchain backend for the matching contracts.
Initial scope:
- one spot market: USDC/cNGN
- one market: BTC convex perp
- one variance market: BTCVAR30-PERP
- one order type: limit order
- one module path:
TradeModule - one executor
- one internal Deribit-backed oracle
- one matching loop
This repo is intentionally narrow. It is not a generic exchange backend.
- accept and persist signed BTC convex perp orders
- expose a minimal API for order entry and book inspection
- expose a minimal API for BTCVAR30 oracle reads
- run a price-time matching loop
- run a BTCVAR30 oracle poller and funding loop
- submit executor payloads for
Matching.verifyAndMatch(...)
- RFQ
- liquidation
- multi-market support
- websocket market data
- a full frontend
- direct onchain execution from Go
cmd/
api/ HTTP API for orders and health checks
matcher/ background matching worker
internal/
api/ HTTP server wiring and handlers
config/ environment configuration
db/ Postgres connection helpers
funding/ BTCVAR30 funding calculation loop
instruments/ instrument metadata and registry
marketdata/ Deribit market data client
matching/ matching loop and orchestration
oracles/ internal oracle services and persistence
orders/ order model and repository contracts
migrations/ database schema
Copy .env.example into your own environment and set the required values.
Important values:
DATABASE_URLAPI_ADDRMATCHER_POLL_INTERVALCHAIN_IDMATCHING_ADDRESSTRADE_MODULE_ADDRESSBTC_PERP_ASSET_ADDRESSCNGN_SPOT_ASSET_ADDRESSCNGN_APR30_2026_FUTURE_ASSET_ADDRESSCNGN_APR30_2026_FUTURE_SUB_ID- optionally
EXPECTED_ORDER_OWNER - optionally
EXPECTED_ORDER_SIGNER EXECUTOR_URL- optionally
EXECUTOR_MANAGER_DATA - optionally
EXECUTOR_MANAGER_DATA_FILE DERIBIT_BASE_URLDERIBIT_WS_URLBTCVAR30_ENABLEDBTCVAR30_PERP_ASSET_ADDRESSBTCVAR30_ORACLE_POLL_MSBTCVAR30_ORACLE_STALE_MSBTCVAR30_FUNDING_INTERVAL_MSBTCVAR30_FUNDING_COEFFBTCVAR30_FUNDING_CAP- optionally
BTCVAR30_ORACLE_SIGNING_KEY
For spot-style USDC/cNGN, the market is enabled when CNGN_SPOT_ASSET_ADDRESS is set. The registry
resolves this instrument by exact (asset_address, sub_id=0) and exposes the canonical market symbol
USDCcNGN-SPOT. Human-readable pair formatting remains in display fields such as
display_name and display_label.
contract_type=spotsettlement_type=spotbase_asset_symbol=USDCquote_asset_symbol=cNGN
Spot order-entry contract is explicit in market metadata as order_entry_spec=usdc_cngn_spot_v1.
That contract is:
- UI price unit:
cNGN per USDC - UI size unit:
USDC notional - UI side meaning:
BUYacquires USDC,SELLdisposes of USDC - Engine price unit:
USDC per cNGN - Engine amount unit:
cNGN amount - Engine side policy: invert the UI side
Formulas:
engine_price = 1 / ui_price
engine_amount = ui_size * ui_price
UI BUY -> engine SELL
UI SELL -> engine BUY
Invariant:
ui_size ≈ engine_amount * engine_price
Submitters may send raw engine fields only, or may additionally send:
order_entry_spec=usdc_cngn_spot_v1ui_intent.sideui_intent.priceui_intent.size
When those UI fields are present, the API rejects the order unless they map back to the submitted engine fields under the exact spot contract formulas and side inversion.
Order and trade responses for spot include a normalized spot_contract echo with:
ui_intentengine_orderbalance_delta
For the physically delivered USDC/cNGN APR-30-2026 future, the market is only enabled when both
CNGN_APR30_2026_FUTURE_ASSET_ADDRESS and CNGN_APR30_2026_FUTURE_SUB_ID are set. The registry
resolves this instrument by exact (asset_address, sub_id) and exposes the canonical market symbol
USDCcNGN-APR30-2026. Human-readable pair formatting remains in display fields such as
display_name and display_label.
contract_type=deliverable_fx_futuresettlement_type=physical_deliverybase_asset_symbol=USDCquote_asset_symbol=cNGN
If EXPECTED_ORDER_OWNER or EXPECTED_ORDER_SIGNER are set, the API rejects orders whose declared owner/signer do not match those configured addresses. The API also validates that action_json.owner, action_json.signer, action_json.subaccount_id, and action_json.nonce match the stored order fields.
EXECUTOR_URL is the endpoint for a separate executor process, likely implemented in
TypeScript with viem, that performs simulation and submits verifyAndMatch(...).
EXECUTOR_MANAGER_DATA lets the matcher attach the exact manager_data hex required by the
executor call. If the blob is too large for an env var, set EXECUTOR_MANAGER_DATA_FILE
instead. That file may contain either the raw hex string or a JSON object with a
manager_data field.
BTCVAR30 is a first-pass internal oracle sourced from Deribit BTC volatility index data.
For v1, the backend polls Deribit and derives:
variance_30d = (vol_30d / 100)^2
The implementation uses Deribit JSON-RPC style market-data methods and keeps the latest signed payload in memory while persisting history to Postgres.
Relevant docs:
Public endpoints:
GET /oracle/btcvar30/latestGET /oracle/btcvar30/history?limit=100
Example latest response:
{
"symbol": "BTCVAR30",
"source": "deribit",
"timestamp": "2026-03-21T09:00:00Z",
"vol_30d": 61.25,
"variance_30d": 0.37515625,
"methodology_version": "deribit-vol-index-v1",
"signature": "sha256:...",
"stale": false
}BTCVAR30-PERP uses the instrument's own conservative mid-price mark and computes funding from:
funding_rate = clamp((mark_price - oracle_variance_30d) * BTCVAR30_FUNDING_COEFF, -BTCVAR30_FUNDING_CAP, BTCVAR30_FUNDING_CAP)
BTCVAR30-PERP is canonical in the backend and is variance-native end to end:
- engine, matching, executor, funding, and persistence operate on 30D implied variance
- canonical internal price is fixed-point variance ticks
- conversion to vol percent is presentation-only
- all prices are variance; volatility is display-only
Example:
displayed variance price = 0.2728
tick size = 0.0001
internal ticks = 2728
displayed vol percent = sqrt(0.2728) * 100 = 52.23%
0.25variance =50%implied volatilityBTCVAR30prices are submitted in variance, not vol points
Canonical API example:
{
"market": "BTCVAR30-PERP",
"limit_price": 0.2728,
"variance_price": 0.2728,
"vol_percent": 52.23,
"price_semantics": "variance"
}Invariant:
pnl = (var_exit - var_entry) * notional
Never:
pnl = (vol_exit - vol_entry) * notional
FAQ:
- Why is the UI in vol if the engine is variance? Traders think in implied vol, but variance gives linear settlement and linear PnL.
- Why does
0.25correspond to50%vol? Becausesqrt(0.25) * 100 = 50. - Why is BTCVAR30 still called a vol perpetual externally? It is marketed as a vol product while the backend settles in variance.
| Concept | Unit |
|---|---|
| Canonical submitted price | variance |
| Internal ticks | 0.0001 variance |
| Display mark | vol percent |
| PnL | variance change |
| Funding | variance space |
Safety rules:
- funding pauses if the oracle is stale
- last known oracle value is preserved when Deribit is unavailable
- stale oracle status is logged explicitly
- no last-trade mark is used
Current limitation:
- there is no existing risk engine in this repo, so position caps / leverage caps are not enforced here yet
Expected request body:
{
"market": "BTCUSDC-CVXPERP",
"asset_address": "0x...",
"module_address": "0x...",
"maker_order_id": "maker-order-id",
"taker_order_id": "taker-order-id",
"actions": [
{
"subaccount_id": "123",
"nonce": "1",
"module": "0x...",
"data": "0x...",
"expiry": "1710000000",
"owner": "0x...",
"signer": "0x..."
}
],
"signatures": ["0x..."],
"order_data": {
"taker_account": "123",
"taker_fee": "0",
"fill_details": [
{
"filled_account": "456",
"amount_filled": "1000000000000000000",
"price": "78000000000000000000",
"fee": "0"
}
],
"manager_data": "0x..."
}
}The executor may return an empty 2xx response or JSON like:
{
"accepted": true,
"tx_hash": "0x..."
}Expected local stack:
- Go 1.24+
- PostgreSQL 16+
Suggested flow:
- Start Postgres.
- Apply migrations:
go run ./cmd/migrate- Run the API:
env $(cat .env.example | xargs) go run ./cmd/api- Run the matcher:
env $(cat .env.example | xargs) go run ./cmd/matcherFor a cleaner local env, export the variables from .env.example or use your usual dotenv tooling.
Production deploys are expected to run database migrations before the API starts.
This repository encodes that in railway.toml:
- Railway builds both the API binary and the migration binary.
- Railway runs
./migrateas the pre-deploy command. - Railway starts the service only after the migration step succeeds.
DATABASE_URL in Railway should be a reference variable to the Postgres service, for example
${{Postgres.DATABASE_URL}}, rather than a copied literal URL.
For an EOA-owned deployment, set:
EXPECTED_ORDER_OWNER=0xC7bE60b228b997c23094DdfdD71e22E2DE6C9310
EXPECTED_ORDER_SIGNER=0xC7bE60b228b997c23094DdfdD71e22E2DE6C9310Then submit orders whose top-level fields and action_json agree on:
owner_address/action_json.ownersigner_address/action_json.signersubaccount_id/action_json.subaccount_idnonce/action_json.nonce
Example EOA-owned order templates are in:
A helper script is available at:
It posts a crossed taker/maker pair to /v1/orders, but you still need to provide real TAKER_ACTION_DATA, MAKER_ACTION_DATA, TAKER_SIGNATURE, and MAKER_SIGNATURE values for the orders to execute successfully through the onchain matcher.
To reproduce the verified Base dry-run path for BTC convex perp, point the backend at the generated manager data file from the executor repo:
EXECUTOR_MANAGER_DATA_FILE=/tmp/perp-manager-data.jsonThat file can be generated with:
The matcher will then forward the manager_data blob automatically in every executor payload
instead of hardcoding 0x.
The first milestone is one successful matched BTC convex perp trade through TradeModule:
- store two signed crossed orders
- match them offchain
- produce executor payloads from stored signed actions
- send them to the executor
- update both orders on success
For a focused architecture note covering the oracle, funding loop, and enablement flow, see