PairPilot
PairPilot helps traders understand and resolve long/short pairs whose execution did not finish as planned. One ticket for a two-leg perp trade on dreamDEX, Somnia Tokyo testnet (chain 50383): long one asset, short another (ETH, BTC, SOL, HYPE). When only one leg fills, PairPilot shows what actually filled and what is still open, says what to do next, and unwinds the rest through a reviewed close. The headline proof is a live recovery: run 5 below.
How to test
Live app: https://pairpilot-delta.vercel.app. Pick the path that fits your time. Paths A and B need no wallet, no install and no funds.
A. Recovery in the browser, no wallet (2 min)
- Open the live app. It starts in Simulation: fixed prices, labeled, nothing is sent anywhere.
- In Pair ticket, set Failure scenario to Second leg partially fills. Press Preview pair, then Execute simulation.
- The pair shows Incomplete. Check the two leg cards (requested vs filled), Next step, and the Active pair monitor (actual open sizes, marked value, allocation now vs plan).
- Press Close remaining exposure. The close review lists the actual quantities and price bounds; nothing runs until you press Execute simulated close.
- The pair reads Flat. The Closed pair summary shows planned vs filled, average fill prices and the zero read-back. The Activity tab keeps the record; Export JSON downloads the journal.
- Try First close finds no liquidity: the first close gets no fill and the pair stays Residual exposure; a second reviewed close ends flat. Reset simulation starts over.
B. Check the live proofs, no wallet (2 min)
- Open any explorer link in Live testnet proof. Each pair has its open and close transactions on Somnia Tokyo.
- In the live app, switch to Testnet: the strip shows live order books and funding read from chain, with no wallet connected.
- With Node.js 24+ (
npm ci first), re-check any pair from chain data. Neither command needs a key or sends anything; verify saves its report to evidence/pair-proof-<id>.json:
npm run verify -- evidence/testnet-pair-8ab9e2fb-8185-4d1c-9bfe-59081193117a.json checks receipts, signer, owner, fills and price bounds, and that the owner's position was zero right after the pair's last order (exit code 0 when everything passes);
npm run summary -- evidence/testnet-pair-8ab9e2fb-8185-4d1c-9bfe-59081193117a.json prints realized PnL, fees and funding.
C. Open and close your own pair on testnet (about 15 min)
There is no shared test account: a published key would let anyone trade it. You use your own wallet; the funds are free testnet paper money.
- A browser wallet (MetaMask or similar) with a fresh address. This is the signer: it signs orders and pays gas.
- Margin. In the dreamDEX testnet app, sign in with a Somnia Account wallet (a Legacy Account cannot link bots) and join Perps Arena at /competition for 1,000 paper USDso. On /perps, use Perps ⇆ Spot if the USDso is not yet in the Perps account. This app wallet is the owner: it holds margin and positions.
- Link your signer and fund its gas. In the dreamDEX app, open the wallet, choose Link a bot (or Manage > Bot access), paste your browser wallet's address, keep Perps ticked, leave Send it … for gas on, and press Add bot. The signer needs about 1 STT on Somnia Tokyo (chain 50383); add more with Top up on the Bots tab. (Link a Bot docs.) Faucet STT from Shannon (chain 50312, testnet.somnia.network) does not pay Tokyo gas unless bridged.
- Connect. In the live app, switch to Testnet and press Connect wallet. It adds Somnia Tokyo to the wallet if needed. PairPilot then finds the app wallet that linked your signer; press Trade this app wallet. The Execution panel shows owner margin, signer gas and both markets' permissions.
- Open a pair. Pick a pair (ETH/BTC, SOL/BTC and HYPE/SOL were all traded live), keep 50 USDso, press Preview pair, tick the dedicated-account box and press Sign & open testnet pair. Approve the two orders in your wallet. To test recovery, reject the second signature: the first leg stays open and PairPilot shows what to do next.
- Reconcile and close. Reconcile re-reads receipts and positions and sends nothing. Close pair (or ) shows the review; press . The pair reads only after both positions read zero on chain; the summary then lists fills, fees and funding.
More detail on each check: docs/BROWSER-WALKTHROUGH.md. The in-app Setup tab checks gas, margin and permissions for your connected wallet.
Demo video (49 s): evidence/demo/pairpilot-demo.webm. Part 1 is labeled SIMULATION: a HYPE/SOL pair whose second signature is rejected, then the close review and recovery to flat. Part 2 is TESTNET, read-only: live HYPE/SOL books and setup. Live signed trades are listed below with explorer links.
The problem
A relative-value trader opens a long/short pair as two separate orders. If one fills and the other fails, partially fills or times out, the trader is left with unhedged exposure, an unclear state and manual recovery work. Retrying blindly can double a position.
What PairPilot does
- Plan the pair. About 50/50 notional, sized in exact integers from each market's decimals, tick, lot and minimum. Pools and metadata are discovered from the dreamDEX SDK at runtime. Default total 50 USDso, cap 200.
- Check before signing. Fresh books for the selected pair; stale or mismatched reads, and depth too thin inside the price bound, are refused. Margin, existing positions, orders and stops are checked; for a linked app wallet, both pools' operator permissions too. Both orders are simulated.
- Send two bounded IOC orders, one after another. The intent is saved before signing and the hash before waiting. Unfilled quantity is cancelled by the venue, never resent.
- Read back what happened. Fills are decoded from receipts and checked against on-chain positions. An unknown outcome blocks new orders until reconciled; reconcile sends nothing.
- Monitor the open pair. While a pair is open, the Trade view re-reads the owner's position, mark, average entry, marked value, price-only unrealized PnL and funding owed for the journal's own two markets, every 15 s and one read at a time. A position that differs from what the pair recorded is flagged and not counted as the pair's PnL.
- Recover explicitly. Each state gets one plain next step. The close review shows each leg's actual open quantity and price bound before anything is signed. A residual below the market minimum stays visible; nothing is reported flat until positions read zero.
- Review the closed pair. Planned versus filled quantities, average fill prices from receipts, realized price PnL, fees and funding settled in the pair's own receipts, and the final zero read-back. Funding and net say whose they are: pair-exclusive only when the indexer shows each market was the pair's alone from open to close, otherwise conditional, or not given when other activity shared a market. Any figure that cannot be read shows as unavailable, never as 0.
It runs in two ways, both against the same engine: a browser dashboard (keys stay in your wallet extension; the server has no signing endpoint) and a local CLI. Either one trades its own account or a dreamDEX app wallet that linked it through Link a Bot (operator placeOrderFor).
Live testnet proof (Somnia Tokyo, chain 50383, 2026-10-22)
Every row below was independently re-verified from chain data: receipt success, sender = signer, placeOrderFor owner = app wallet, decoded fills = journal fills, fills inside the price bound, explorer page present, and the owner's position zero right after the pair's last order in each market it traded, decoded from MarginBank PositionUpdated events in the pair's own receipts. That check gives the same answer whenever it runs. The app wallet has traded again since, so today's positions are printed for information only.
| # | Path | Pair | What happened | Proof |
|---|
| 1 | CLI, linked app wallet | ETH/BTC | Opened paired (0.0103 ETH / 0.0003 BTC), reconciled, closed, flat | cli-operator-proof-cd37d484.json |
| 2 | Browser, user-signed, linked | ETH/BTC | Opened paired (0.0207 ETH / 0.00061 BTC), closed, flat | browser-operator-proof-bda58fa7.json |
| 3 | Browser, user-signed, linked | SOL/BTC | Opened paired (0.23 SOL / 0.0003 BTC), closed, flat | browser-operator-proof-43dc36bf.json |
| 4 | CLI, linked app wallet | HYPE/SOL | Opened paired (0.31 HYPE / 0.23 SOL), reconciled, closed, flat | hype-sol-operator-proof.json |
| 5 | CLI, linked, injected fault | HYPE/ETH | HYPE long filled; ETH short deliberately withheld (--inject=withhold-second-leg, labeled in the journal) → incomplete → reconcile (no orders) → close review unwound HYPE only → flat | recovery-injected-hype-eth-proof.json |
Transactions (explorer https://explorer.testnet.somnia.network/tokyo/tx/…):
- ETH/BTC CLI: open ETH · open BTC · close ETH · close BTC
- ETH/BTC browser: open ETH · open BTC · close ETH · close BTC
- SOL/BTC browser: open SOL · open BTC · close SOL · close BTC
- HYPE/SOL CLI: open HYPE · · ·
What each live pair made or lost
Read-only, from the pairs' own receipts and the dreamDEX indexer: evidence/pair-summaries.json (npm run summary -- <journal>). USDso; funding paid is positive, received is negative. Net = realized − fees − funding paid. This is each pair's result, not the account's equity change.
| # | Pair | Realized price PnL | Fees | Funding paid | Funding attribution | Net |
|---|
| 1 | ETH/BTC CLI | −0.009178 | 0 | 0 | pair-exclusive | −0.009178 |
| 2 | ETH/BTC browser | +0.056405 | 0 | −0.007416 | pair-exclusive | +0.063821 |
| 3 | SOL/BTC browser | +0.005972 | 0 | 0 | pair-exclusive | +0.005972 |
| 4 | HYPE/SOL CLI | −0.0194 | 0 | 0 | pair-exclusive | −0.0194 |
| 5 | HYPE/ETH recovery | +0.0144 | 0 | 0 | pair-exclusive (ETH: no position) | +0.0144 |
- Realized PnL: for every close, the figure from receipt fills equals the indexer's own MarginBank figure (
crossCheck in the file).
- Fees and funding are MarginBank events in the pairs' own receipts. Fees are 0 because no receipt holds a fee or rebate event. Funding settles when the owner's position changes, so pair 2's two close receipts hold its only settlements; the other pairs' receipts hold none.
- "Pair-exclusive" means the indexer lists only the pair's own transactions as the owner's position changes in that market between open and close, starting and ending at zero, and no funding settlement in other transactions. An indexer that has not yet listed the pair's own transactions counts as unverified, never as a verified zero.
Accounts: app wallet (owner, margin and positions) 0x436dAE2EF8e8eECE9960b3C3a9DA6731a2D6c1F5; CLI signer 0x39f4aebEff78aB72c2FDA3b34c7993608AA79C50; browser signer 0xd41C88eE843dcCBe6E8AD3D62e4c33Ccb6C9787b. How the owner was identified without guessing: operator-link.json. Re-verify any journal yourself: npm run verify -- <journal.json> (read-only).
Run it
Node.js 24+ and npm.
npm ci
npm run dev # http://127.0.0.1:3000 (dashboard) /home (overview) /setup (guide)
Start in Simulation: pick a pair, a failure scenario, preview and execute. Prices and fills there are fixed and labeled. Testnet reads live books and signs with your wallet; see docs/BROWSER-WALKTHROUGH.md. After you connect, PairPilot looks up which app wallets linked your wallet and offers to trade one.
CLI (signs with a local key in ignored .env.local):
npm run wallet:init && npm run wallet:status # create a dedicated key, then fund it with Tokyo STT
npm run trade -- check --owner=0x<app wallet> --pair=HYPE/SOL # read-only readiness
npm run trade -- preview --owner=0x<app wallet> --pair=HYPE/SOL
npm run trade -- simulate --owner=0x<app wallet> # eth_call of both legs; nothing signed
npm run trade -- open --owner=0x<app wallet> # within 40 s of preview; signs two orders
npm run trade -- reconcile --owner=0x<app wallet> # sends nothing
npm run trade -- close --owner=0x<app wallet> # unwinds the JOURNAL's markets
Omit --owner to trade the key's own account. npm run summary -- <journal.json> prints a closed-pair summary from receipts and the indexer (read-only, no key). Runbooks: docs/TESTNET.md, docs/HYPE-SOL-WALKTHROUGH.md.
Checks: npm test (116 unit tests), npm run test:browser (41 Playwright tests at 375/768/1280 px, using mock wallets and injected offline reads, never real keys), npm run typecheck, npm run build, npm run doctor (read-only network check).
Limits, stated plainly
- Not atomic. The two orders are sequential. One can fill without the other; PairPilot makes that visible and recoverable, it does not prevent it. Price moves before recovery can cost money; a slippage bound is not a loss limit.
- Reduce-only is client-side. dreamDEX SDK 0.32.1 ordinary orders have no reduce-only flag. PairPilot re-reads the position right before each close, but another bot, manual trade or liquidation on the same account can race it. Use an account dedicated to PairPilot.
- Dust can remain. A leg left below the market minimum cannot be closed by any order; it stays visible as residual exposure.
- Equal notional is not a hedge guarantee. It is not beta-neutral; funding, correlation and liquidation still apply.
- Monitor figures are estimates. Unrealized PnL is (mark − the owner's average entry) × size, before fees and funding; a close fills at book prices. Entry and funding are per owner and market, so they are attributed to the pair only while the owner's position equals the pair's record, and funding owed on an open pair is always labeled conditional. In the summary, funding is pair-exclusive only after the indexer check above; if the indexer lags or cannot be read, net is labeled conditional, and if another trade shared a market, no net is given for it. In the browser, testnet monitoring needs the wallet connected; simulation marks are fixed and fees and funding are not simulated.
- Testnet only. No mainnet, no stops or leverage changes, no automation. Own-wallet signed orders are implemented but not live-proven: the one-time voucher claim reverts on-chain for that key (see feedback draft), so all live proofs use a linked app wallet.
- Journals are local: browser journals in local storage, CLI journals in ignored
.pairpilot/. A send interrupted before its hash is saved needs a manual nonce check; PairPilot never guesses or retries.
Evidence index
Live proofs are listed above. Supporting material:
- Status ledger: what was verified, how, and what was not.
- Read-only doctor: chain 50383, 21 markets, metadata for ETH, BTC, SOL, HYPE.
- Preflights and transaction simulations before each live run:
evidence/operator-preflight-*.json, evidence/operator-simulation-*.json.
- Voucher claim attempts: one reverted claim (
0x3e31dcc2) and one simulation that sent nothing.
- Simulation traces and browser test report: local and labeled, with synthetic hashes.
Repository map
src/core/ engine, planner, pairs, journals · src/adapters/ dreamDEX venue, operator (placeOrderFor, permissions, owner discovery), simulator, voucher · app/ dashboard, overview, setup · scripts/ CLI, verifier, demo recorder · tests/ Playwright · evidence/ proofs · docs/ runbooks and design notes · feedback/ integration findings (drafts, not submitted).
Sources and their limits: docs/SOURCES.md.