A dYdX-class decentralised perpetual-futures exchange. Orderbook-matched. USDC-collateralised. Self-custodial. Built for Arbitrum.
Perplex is a hybrid perpetual-futures DEX: an off-chain orderbook matched in Rust for centralised-exchange latency, with every fill settled atomically on-chain in Solidity. Traders keep custody of their USDC; the matching engine never holds funds. The match layer is replaceable — settlement does not trust it.
Three markets ship in v1: BTC-USD, ETH-USD, SOL-USD. Up to 20× leverage, 8h funding cadence, EIP-712 signed batch settlement. The off-chain Pyth Hermes relayer streams live BTC/ETH/SOL prices into the edge in every environment (local, testnet, mainnet) — on-chain settlement adds the verified Pyth pull-feed + Chainlink sanity bound on top. A local MockOracle and MockUSDC make the whole stack runnable on a laptop with make dev-up (or ./scripts/dev-up-all.sh for the one-command path).
- Self-custodial trading. USDC stays in
CollateralVaultuntil the trader signs an EIP-712 fill that the on-chain settlement engine re-verifies. The matching server cannot move funds. - CEX-grade latency. Rust + axum +
tokiomatching engine with a per-marketBTreeMaporderbook. Benchmarks land in the low millions of ops/sec on a single core. - Atomic settlement.
SettlementEngineeither applies every fill in a batch or reverts the whole batch. No partial fills, no torn state. - Live Pyth-driven marks.
perplex-oraclerelayer streams BTC/ETH/SOL prices from Pyth Hermes every ~500 ms into edge state. Mark price on every position, the trade page header, the chart's live candle, and the market list all source the same tick. APythsource pill on the trade header makes the live vs static distinction visible at a glance. - Cross-margin position aggregates.
/v1/positionsreturns total notional, unrealised PnL, used margin, and free collateral computed at read time against the live mark. Funding rate is derived from the orderbook mid vs index and broadcast onfunding.{marketId}every 2 s. - Slippage-aware order ticket. Liquidation preview walks the orderbook side for the requested size and uses the volume-weighted entry, so the
Liquidation @ Nxrow moves with both size and leverage — not just leverage. - Self-trade prevention. The edge matcher skips every maker order keyed to the taker's address. Same wallet can't accidentally cross itself and net to zero.
- Lazy 8h funding. Cumulative funding index per market. Positions settle their funding obligation on next interaction — O(1) per position, O(1) per window. Next-settlement timestamp is aligned to UTC
00:00 / 08:00 / 16:00. - Liquidation + insurance + ADL. Underwater positions close at the oracle mark; penalty flows to
InsuranceFund; auto-deleveraging is the last-resort backstop when the fund is empty. - Real-time UI. Next.js 16 + React 19 + Wagmi v3 frontend with SIWE login, EIP-712 order signing, live orderbook + fills + mark via WebSocket.
- Local-first dev loop.
make dev-upboots Anvil + Postgres + Redis, deploys 11 contracts, seeds prices, mintsMockUSDC— full stack in under a minute. - Observability. Per-binary Prometheus
/metrics, auto-provisioned Grafana counterparty dashboard viamake metrics-up.
A snapshot of what the platform does today versus what stands between it and a live, real-money product — and why.
- Perpetual-futures trading — live orderbook, matching engine, market + limit orders across BTC-USD, ETH-USD, SOL-USD with up to 20× leverage.
- Durable state — orders, positions, fills, balances, public trades, and funding history persist to Postgres and survive a restart with no data loss.
- Liquidations — a keeper detects positions below maintenance margin and force-closes them at the oracle mark; real liquidation prices are shown on every position.
- Risk gate on order entry — orders are rejected when they aren't backed by enough collateral or would exceed the market's max leverage.
- Wallet-verified login — Sign-In-With-Ethereum performs full ECDSA signature recovery, so only the real wallet owner can open a session.
- Fees + rebates — taker fees and maker rebates are charged and settled into balances on every fill.
- Live oracle pricing — BTC/ETH/SOL marks stream from Pyth Hermes in every environment.
| Capability | Why not yet | What it needs |
|---|---|---|
| Run on a public blockchain | Runs on a local development chain, not a public network. | Mainnet deployment and testing remains. |
| Handle real money | Uses test USDC; no real deposits or withdrawals. | A proper security audit first, then point the vault at real USDC on mainnet. |
| Be reachable on the internet | Not yet live or hosted. | The application has not yet been deployed to a public host. |
| Pass a security audit | No external audit has been done. | A proper security audit is required before handling real funds. |
| Deep, real liquidity | Only the in-house bot quotes the book. | External market makers + an incentive program. The fee/rebate rails are already built; this is a business effort, not code. |
| Operate legally | Perpetuals are heavily regulated; no entity, geofencing, or terms in place. | A legal entity in a crypto-friendly jurisdiction, region restrictions, and terms of service. |
| Layer | Stack |
|---|---|
| Smart contracts | Solidity 0.8.24 · Foundry · OpenZeppelin · Solady · pyth-sdk-solidity |
| Matching + edge | Rust 2021 · axum · tokio · tokio-tungstenite · sqlx · redis-rs · jsonwebtoken · k256 |
| Frontend | Next.js 16 (App Router) · React 19 · Wagmi v3 · viem · TanStack Query · Zustand · Tailwind v4 · MSW |
| Infra | Docker Compose · Anvil · Postgres 16 · Redis 7 · Prometheus · Grafana |
| Auth | SIWE (EIP-4361) · JWT (HS256) · EIP-712 typed-data order signing |
| Oracles | Pyth Hermes off-chain stream (every environment, ~500ms) · Pyth pull-feed + Chainlink sanity bound on-chain (testnet/mainnet) · MockOracle available for deterministic tests |
| Target chain | Arbitrum One (mainnet) |
The off-chain layer reads intent, matches, and forwards signed fills. The on-chain layer re-verifies signatures and is the only writer of money state.
flowchart LR
subgraph Client["Client"]
FE["Next.js dApp<br/>Wagmi · viem"]
MM["MetaMask<br/>SIWE + EIP-712"]
end
subgraph OffChain["Off-chain (Rust)"]
EDGE["perplex-edge<br/>axum · JWT · WS"]
MATCH["perplex-matching<br/>per-market book"]
BOT["perplex-cli quote<br/>counterparty bot"]
ORACLE["perplex-oracle<br/>price relayer"]
FUND["perplex-funding<br/>leader-elected cron"]
RISK["perplex-cli risk<br/>liquidation scanner"]
end
subgraph Storage["Storage"]
PG[("Postgres<br/>history")]
REDIS[("Redis<br/>book snap · leases")]
end
subgraph OnChain["On-chain (Solidity / Arbitrum)"]
VAULT["CollateralVault"]
SETTLE["SettlementEngine<br/>EIP-712 verify"]
POS["PositionRegistry"]
MR["MarketRegistry"]
FE_C["FundingEngine"]
OA["OracleAdapter<br/>Pyth + Chainlink"]
LIQ["LiquidationEngine"]
INS["InsuranceFund"]
SYN["SyntheticCounterparty<br/>2-day timelock"]
end
FE --> MM
FE -- "REST / WS" --> EDGE
MM -- "SIWE / EIP-712" --> EDGE
EDGE --> MATCH
BOT --> EDGE
MATCH --> PG
MATCH --> REDIS
EDGE --> PG
EDGE --> REDIS
MATCH -- "signed fills" --> SETTLE
SETTLE --> VAULT
SETTLE --> POS
SETTLE --> MR
ORACLE --> OA
OA --> FE_C
FUND --> FE_C
FE_C --> POS
RISK --> POS
RISK --> LIQ
LIQ --> POS
LIQ --> VAULT
LIQ --> INS
INS --> SYN
classDef chain fill:#fef2f2,stroke:#d63044,color:#7f1d1d
classDef rust fill:#fffbeb,stroke:#d97706,color:#78350f
classDef store fill:#f4f3f8,stroke:#736b8a,color:#3d3656
classDef user fill:#faf5ff,stroke:#7c3aed,color:#4c1d95
class VAULT,SETTLE,POS,MR,FE_C,OA,LIQ,INS,SYN chain
class EDGE,MATCH,BOT,ORACLE,FUND,RISK rust
class PG,REDIS store
class FE,MM user
sequenceDiagram
autonumber
participant U as Trader
participant W as MetaMask
participant E as perplex-edge
participant M as Matching
participant B as Counterparty bot
participant S as SettlementEngine
participant V as CollateralVault
participant P as PositionRegistry
U->>W: Connect + sign SIWE
W->>E: POST /v1/auth/siwe/verify
E-->>U: JWT (HS256)
B->>E: POST /v1/orders (resting bid + ask, signed)
E->>M: insert into book
U->>W: Build order · sign EIP-712
W-->>U: signature (132 hex)
U->>E: POST /v1/orders (taker, signed)
E->>M: match against resting side
M-->>E: Fill[]
E->>S: settle(Fill[]) tx
S->>S: verify maker + taker EIP-712
S->>V: pull initial margin
S->>P: open / update positions
S-->>E: tx hash
E-->>U: WS frame: fill + position update
flowchart TD
Q["Edge submits Fill[]<br/>to SettlementEngine"]
V1{"verify maker sig<br/>(EIP-712)"}
V2{"verify taker sig<br/>(EIP-712)"}
V3{"IMR · max lev · market open<br/>(MarketRegistry)"}
OK["pull margin · open positions<br/>emit Trade event"]
REV["revert whole batch<br/>nothing moves"]
Q --> V1
V1 -- ok --> V2
V2 -- ok --> V3
V3 -- ok --> OK
V1 -- fail --> REV
V2 -- fail --> REV
V3 -- fail --> REV
classDef ok fill:#f0fdf4,stroke:#0fa56a,color:#14532d
classDef bad fill:#fef2f2,stroke:#d63044,color:#7f1d1d
classDef neu fill:#fafafa,stroke:#a1a1aa,color:#3d3656
class OK ok
class REV bad
class Q,V1,V2,V3 neu
Prereqs — Docker Desktop, Foundry (foundryup), Rust 1.82+, Node 20+, pnpm 9, jq, Python 3.9+ (for seed-book.py).
git clone https://github.com/ozpool/Perplex.git perplex
cd perplex
cp .env.example .env
pnpm install --frozen-lockfile
./scripts/dev-up-all.shSpawns one Terminal window per long-lived process:
- Anvil + Postgres + Redis via
make dev-up(contracts deployed,MockUSDCminted, markets seeded). - perplex-edge — REST
:8080, WS:8081, with the Pyth Hermes oracle relayer wired in by default. Boot log includesoracle relayer spawned (Pyth Hermes). - perplex-cli quote — counterparty bot (currently broken under some startup races;
seed-book.pycovers in step 4). - scripts/seed-book.py — Python dev-stub market maker. Pulls live mid from
/v1/markets(which carries the Pyth-overlaidindexPriceX18) and posts a 5-rung ladder per side every 2s. - Prometheus + Grafana via
make metrics-up. - Next.js frontend via
pnpm web:dev:real→http://localhost:3000.
The script polls /v1/orderbook/btc-usd before printing its summary banner, so a green line btc-usd book has N ask level(s) confirms everything came up clean.
Demo wallet (anvil account #5 — import into MetaMask):
addr: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
pk: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
Once SIWE login completes, the edge auto-seeds 100,000 USDC into your vault (seed_dev_vault flag, dev-routes only) so you can place orders immediately — no on-chain deposit needed locally.
If you'd rather spawn each piece by hand:
# 1. Stack
make dev-up
# 2. Edge (oracle relayer included by default; pass --with-oracle=false to skip)
PERPLEX_JWT_SECRET=$(openssl rand -hex 32) \
cargo run -p perplex-edge -- --bind 127.0.0.1:8080 --ws-bind 127.0.0.1:8081
# REST http://localhost:8080
# Docs http://localhost:8080/docs
# 3. Counterparty (the Python stub — perplex-cli quote replaces it once #157 lands)
python3 scripts/seed-book.py
# 4. Metrics
make metrics-up
# Grafana http://localhost:3001 (admin / admin)
# Prometheus http://localhost:9090
# 5. Frontend
cd web && pnpm dev:realAfter bring-up, BTC/ETH/SOL on the trade page should tick every ~500 ms and the MarketHeader Oracle stat carries a green pulsing Pyth pill. Confirm from the CLI:
curl -s http://127.0.0.1:8080/v1/markets \
| jq '.markets[] | {id, idx: .indexPriceX18, vol: .volume24hUsdc, oi: .openInterestUsdc, fr: .fundingRateBps}'Run twice, ~2 s apart — idx changes between calls.
make dev-down # stop anvil + postgres + redis (keep volumes)
make metrics-down # stop prometheus + grafana
make dev-reset # nuke volumes and re-bootstrapA longer playbook (MetaMask import, dev wallet, every terminal) lives in the Notion runbook.
- Position panel empty after a fill — restart the seeder Terminal so its anvil-#1 maker quotes refresh. Self-trade prevention now blocks fills where taker and maker are the same address; this usually means MetaMask is signing as anvil #1 instead of anvil #5. Re-import the demo wallet pk above.
- Order rejected
jwt: ExpiredSignature— the edge process rotated its JWT secret (e.g. you Ctrl-C'd and restarted it). The FE auto-clears the stale bearer and prompts SIWE again; just sign once more. - Trade page shows
staticnext to Oracle — the Pyth Hermes fetch is failing. Check the edge Terminal fororacle source fetch failedwarnings. Re-running./scripts/dev-up-all.shtypically resolves it. Position panel always empty— almost always the self-trade case above. As a sanity check,curl /v1/orderbook/btc-usdand confirm both bids and asks have at least one level fromseed-book.py(anvil account #1, not #5).
contracts/ Solidity sources + Foundry tests (unit, invariant, differential)
src/ 11 contracts (Vault, Registry, Settlement, Funding, Liquidation, Oracle, …)
lib/ forge-std, openzeppelin-contracts, solady, pyth-sdk-solidity
script/ Deploy.s.sol (Anvil + Sepolia)
crates/ Rust workspace
perplex-core Shared types + EIP-712 domain + margin math
perplex-matching BTreeMap orderbook · per-market workers · fill streams
perplex-edge axum REST + WS · JWT · SIWE · OpenAPI (utoipa)
perplex-oracle Pyth Hermes source + drift-triggered relayer
perplex-funding Redis SETNX leader-election cron
perplex-mock-oracle Local replay oracle pusher
perplex-cli One bin, many subcommands: edge / quote / oracle / risk / kill / metrics
perplex-diff-gen Rust JSON fixtures replayed in Solidity diff tests
web/ Next.js 16 frontend
app/(app)/ trade · markets · portfolio · wallet · history
app/(marketing)/ Public landing
lib/ wallet (Wagmi · SIWE) · contracts · ws · api
styles/ tokens.css (dark + orange theme)
docs/ openapi.json · postman.json · margin-math.md
infra/ docker-compose.metrics.yml · Grafana dashboard JSON
scripts/ seed · smoke-deposit · smoke-trade · smoke-liquidate · replay-binance
sdk/ TypeScript SDK (Phase 5)
Makefile Top-level commands
forge test # 164 unit + invariant + differential
cargo test --workspace # Rust crates
cargo clippy --workspace --all-targets -- -D warnings
forge fmt --check && cargo fmt --all -- --checkDifferential tests replay 500 Rust-generated scenarios on-chain with a 256-wei tolerance for Decimal-vs-integer rounding. Invariants run 256 fuzz rounds × 16384 calls each and assert:
- per-market sum of position sizes = 0
- cumulative funding cashflow nets to dust
- insurance fund balance is monotonic in the absence of bad debt
End-to-end smokes:
make dev-deposit # deposit / withdraw / blocked-withdraw path
make dev-trade # place + match + settle
make dev-liquidate # crash price · verify liquidation cascade
make sim-replay # 30-day Binance tape against the counterparty agentREST surface is the source of truth for the frontend — every endpoint is documented in docs/openapi.json, auto-derived via utoipa, and served at http://localhost:8080/docs. A Postman collection is exported to docs/postman.json.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/v1/auth/siwe/nonce |
public | start SIWE login |
POST |
/v1/auth/siwe/verify |
public | finish SIWE, mint JWT |
GET |
/v1/markets |
public | list markets |
GET |
/v1/orderbook/:market_id |
public | orderbook snapshot |
GET |
/v1/trades/:market_id |
public | recent fills |
GET |
/v1/funding/:market_id |
public | funding rate history |
POST |
/v1/orders |
bearer | place signed order |
DELETE |
/v1/orders/:order_id |
bearer | cancel |
GET |
/v1/orders/open |
bearer | open orders |
GET |
/v1/positions |
bearer | open positions |
GET |
/v1/fills |
bearer | private fills |
GET |
/v1/account/balance |
bearer | vault balance |
| Channel | Auth | Payload |
|---|---|---|
orderbook.{marketId} |
public | snapshot + deltas with sequence |
trades.{marketId} |
public | public fills |
oracle.{marketId} |
public | live Pyth Hermes mark-price ticks (~500ms) |
funding.{marketId} |
public | funding rate (bps) + next-settlement nanos (2s ticker) |
user.fills |
bearer | private fills |
user.positions |
bearer | private position diffs |
Subscribe over the framed op protocol:
{ "op": "auth", "token": "<jwt>" } // only for bearer channels
{ "op": "subscribe", "channel": "oracle.btc-usd" }
{ "op": "unsubscribe", "channel": "oracle.btc-usd" }The edge publishes every message verbatim — clients filter by type (e.g. oracle, funding, trade).
- v1.0 (current) — local-first hybrid DEX. 3 markets. Full settle / fund / liquidate / ADL cascade. Self-host with one Make target.
- v1.1 — Arbitrum Sepolia bringup. Pyth Hermes wired live. MegaVault LP backstop. Auto-liquidation watcher. SessionKey UX so MetaMask doesn't prompt per order.
- v2.0 — Arbitrum One mainnet. External audit. More markets. Cross-margin. SDK on npm.
- Every change lands via PR. Contributors do not push to
main. - Branch naming:
feat/<name>,fix/<name>,chore/<name>,docs/<name>. - CI (Foundry + Cargo + devnet smoke) must be green before review.
- Branches are preserved on merge — never
--delete-branch. - Commits use a verb-first imperative subject.
See CONTRIBUTING.md for the full development loop.
BUSL-1.1 (source-available). Converts to MIT once Stage-3 mainnet is stable.