Skip to content

Repository files navigation

Perplex

Perplex

A dYdX-class decentralised perpetual-futures exchange. Orderbook-matched. USDC-collateralised. Self-custodial. Built for Arbitrum.

LicenseSolidityRustNext.jsFoundryStatus


What it is

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).


Capabilities

  • Self-custodial trading. USDC stays in CollateralVault until 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 + tokio matching engine with a per-market BTreeMap orderbook. Benchmarks land in the low millions of ops/sec on a single core.
  • Atomic settlement.SettlementEngine either applies every fill in a batch or reverts the whole batch. No partial fills, no torn state.
  • Live Pyth-driven marks.perplex-oracle relayer 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. A Pyth source pill on the trade header makes the live vs static distinction visible at a glance.
  • Cross-margin position aggregates./v1/positions returns 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 on funding.{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 @ Nx row 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-up boots Anvil + Postgres + Redis, deploys 11 contracts, seeds prices, mints MockUSDC — full stack in under a minute.
  • Observability. Per-binary Prometheus /metrics, auto-provisioned Grafana counterparty dashboard via make metrics-up.

Status

A snapshot of what the platform does today versus what stands between it and a live, real-money product — and why.

What works today

  • 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.

What does not work yet — and why

CapabilityWhy not yetWhat it needs
Run on a public blockchainRuns on a local development chain, not a public network.Mainnet deployment and testing remains.
Handle real moneyUses 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 internetNot yet live or hosted.The application has not yet been deployed to a public host.
Pass a security auditNo external audit has been done.A proper security audit is required before handling real funds.
Deep, real liquidityOnly 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 legallyPerpetuals are heavily regulated; no entity, geofencing, or terms in place.A legal entity in a crypto-friendly jurisdiction, region restrictions, and terms of service.

Tech stack

LayerStack
Smart contractsSolidity 0.8.24 · Foundry · OpenZeppelin · Solady · pyth-sdk-solidity
Matching + edgeRust 2021 · axum · tokio · tokio-tungstenite · sqlx · redis-rs · jsonwebtoken · k256
FrontendNext.js 16 (App Router) · React 19 · Wagmi v3 · viem · TanStack Query · Zustand · Tailwind v4 · MSW
InfraDocker Compose · Anvil · Postgres 16 · Redis 7 · Prometheus · Grafana
AuthSIWE (EIP-4361) · JWT (HS256) · EIP-712 typed-data order signing
OraclesPyth Hermes off-chain stream (every environment, ~500ms) · Pyth pull-feed + Chainlink sanity bound on-chain (testnet/mainnet) · MockOracle available for deterministic tests
Target chainArbitrum One (mainnet)

Architecture

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
Loading

Trade lifecycle — one fill, end to end

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
Loading

Settlement guarantee

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
Loading

Quickstart

Prereqs — Docker Desktop, Foundry (foundryup), Rust 1.82+, Node 20+, pnpm 9, jq, Python 3.9+ (for seed-book.py).

One-command bring-up (recommended)

git clone https://github.com/ozpool/Perplex.git perplex
cd perplex
cp .env.example .env
pnpm install --frozen-lockfile
./scripts/dev-up-all.sh

Spawns one Terminal window per long-lived process:

  1. Anvil + Postgres + Redis via make dev-up (contracts deployed, MockUSDC minted, markets seeded).
  2. perplex-edge — REST :8080, WS :8081, with the Pyth Hermes oracle relayer wired in by default. Boot log includes oracle relayer spawned (Pyth Hermes).
  3. perplex-cli quote — counterparty bot (currently broken under some startup races; seed-book.py covers in step 4).
  4. scripts/seed-book.py — Python dev-stub market maker. Pulls live mid from /v1/markets (which carries the Pyth-overlaid indexPriceX18) and posts a 5-rung ladder per side every 2s.
  5. Prometheus + Grafana via make metrics-up.
  6. Next.js frontend via pnpm web:dev:realhttp://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.

Manual bring-up (advanced)

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. Frontendcd web && pnpm dev:real

Verifying live oracle

After 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.

Tear down

make dev-down # stop anvil + postgres + redis (keep volumes)
make metrics-down # stop prometheus + grafana
make dev-reset # nuke volumes and re-bootstrap

A longer playbook (MetaMask import, dev wallet, every terminal) lives in the Notion runbook.

Troubleshooting

  • 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 static next to Oracle — the Pyth Hermes fetch is failing. Check the edge Terminal for oracle source fetch failed warnings. Re-running ./scripts/dev-up-all.sh typically resolves it.
  • Position panel always empty — almost always the self-trade case above. As a sanity check, curl /v1/orderbook/btc-usd and confirm both bids and asks have at least one level from seed-book.py (anvil account #1, not #5).

Repo layout

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

Testing

forge test# 164 unit + invariant + differential
cargo test --workspace # Rust crates
cargo clippy --workspace --all-targets -- -D warnings
forge fmt --check && cargo fmt --all -- --check

Differential 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 agent

API

REST 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.

MethodPathAuthPurpose
POST/v1/auth/siwe/noncepublicstart SIWE login
POST/v1/auth/siwe/verifypublicfinish SIWE, mint JWT
GET/v1/marketspubliclist markets
GET/v1/orderbook/:market_idpublicorderbook snapshot
GET/v1/trades/:market_idpublicrecent fills
GET/v1/funding/:market_idpublicfunding rate history
POST/v1/ordersbearerplace signed order
DELETE/v1/orders/:order_idbearercancel
GET/v1/orders/openbeareropen orders
GET/v1/positionsbeareropen positions
GET/v1/fillsbearerprivate fills
GET/v1/account/balancebearervault balance

WebSocket channels

ChannelAuthPayload
orderbook.{marketId}publicsnapshot + deltas with sequence
trades.{marketId}publicpublic fills
oracle.{marketId}publiclive Pyth Hermes mark-price ticks (~500ms)
funding.{marketId}publicfunding rate (bps) + next-settlement nanos (2s ticker)
user.fillsbearerprivate fills
user.positionsbearerprivate 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).


Roadmap

  • 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.

Contributing

  • 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.


License

BUSL-1.1 (source-available). Converts to MIT once Stage-3 mainnet is stable.

About

Perplex — dYdX-class decentralised perpetuals exchange. Off-chain Rust matching engine + on-chain Solidity settlement on Arbitrum. Pyth oracles, 8h funding, liquidation + ADL backstop, EIP-712 signed batch settlement.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages