Skip to content

Fix/cli book check referrer - #57

Merged
Shawnchee merged 5 commits into
mainfrom
fix/cli-book-check-referrer
Aug 20, 2026
Merged

Shawnchee merged 5 commits into
mainfrom
fix/cli-book-check-referrer

Conversation

@Shawnchee

Copy link
Copy Markdown
Collaborator

Summary

Fixes costly OptionBook routing and attribution issues in the CLI, and releases @thetanuts-finance/cli version 0.3.0.

  • book check now recognizes liquidity when the requested strike appears in any leg of a live multi-leg structure.
  • Preview and fill use the same instrument matcher as book check, including order-independent multi-leg strike selection.
  • book check no longer overstates executable size across multiple makers; it reports price levels and the maximum size of its next command.
  • Structure-only results are presented as explicit alternatives rather than automatically selecting a different payoff based on premium.
  • Sell-side book liquidity no longer produces an RFQ command; the CLI clearly directs users to the dApp until sell execution is supported.
  • Adds --referrer, THETANUTS_REFERRER, and config-file support for OptionBook referral attribution.
  • Generated follow-up commands preserve a one-off referrer, and subcommand help displays global options.
  • book fill warns when no referrer is configured and surfaces referral attribution in its receipt output.
  • Adds regression coverage for liquidity matching, structure handling, sizing, sell-side behavior, referrer propagation, and command-help visibility.

Validation

  • npm run typecheck — passed
  • cd cli && npm run typecheck — passed
  • cd cli && npm run build — passed
  • cd cli && npm test — 12/12 passed
  • Live orderbook verifier passed with no broadcast transactions.

book check matched only strikes[0], so any strike sitting on a
structure's second/third/fourth leg was invisible to it. Roughly half
the live book is multi-leg (spreads, flies, rangers), so it recommended
RFQ for strikes that were live and fillable — routing traders off the
book and forfeiting orderbook credit. The inverse happened too: a
4-strike RANGER whose first leg matched was reported as a fillable
vanilla, with a vanilla ticker and the structure's premium quoted as a
vanilla ask, and the nextStep it printed then failed to resolve.

The root cause was two matchers: check had its own, while preview and
fill used resolveOrderBySelector. Extract one shared predicate into
bookMatch.ts and have both use it, so the commands cannot disagree about
what the book holds. Move check's decision ladder into bookCheck.ts as a
pure function so the invariant is directly testable: check may answer
rfq only when the shared matcher finds nothing at the requested expiry.

Measured against a live 220-order book, 206 of 384 strike positions were
previously unreachable.

Also compare strike vectors as multisets. Makers do not store strikes in
a canonical order — 19 of 310 live orders store them descending — so
element-wise comparison meant --strikes 64500,65000 reported "No live
order matches" while --strikes 65000,64500 filled the same order. Safe
because the fill encodes the signed order's own vector, never the
caller's argument order.

Drop the hardcoded ETH|BTC gate, which left live SOL/DOGE/XRP/BNB/AVAX
orders unreachable, and stop returning RFQ unconditionally for
--direction sell without consulting the book.

recommendation keeps its existing orderbook/rfq values; the new detail
arrives as additive fields (structureMatches, liveExpiries, didYouMean,
cliExecutable) so existing consumers are unaffected.

Two adjacent book.ts fixes ride along, since they touch the same file:

- book fill --dry-run --output json emitted two concatenated JSON
  documents (preview, then calldata), which no JSON parser accepts.
  Machine output is now one object with the preview nested under
  `preview`. Table output is unchanged.
- book fill receipts now surface referrer and referralFeePaid from the
  OrderFilled event, so a fill's attribution can be audited without
  decoding the receipt on a block explorer.
The SDK has supported a referrer address since 0.2.x — ThetanutsClient
takes one and optionBook.fillOrder/previewFillOrder/encodeFillOrder all
resolve `referrer ?? client.referrer ?? ZeroAddress`. The CLI never
passed it, so every book fill went on-chain attributed to address(0)
and silently earned no referral credit.

Thread it at client construction so fill, preview and dry-run calldata
all pick it up with no per-call-site changes. Resolution follows the
existing pattern for rpcUrl and privateKey: --referrer flag, then
THETANUTS_REFERRER, then the config file. Validated with
ethers.isAddress so a bad value fails early naming its source.

book fill warns on stderr when no referrer resolves, including an
explicit zero address — that fill genuinely earns nothing, and the
warning is what makes the loss visible instead of silent. It goes to
stderr so --output json stays machine-parseable.

Unrelated to RFQ's --referral-id, which is a numeric tracking ID rather
than an address and does not participate in OptionBook fee sharing.
Record the book check and --referrer work in the CLI changelog, document
the new check response fields, and add scripts/verify-book-fixes.ts —
an end-to-end verifier that derives every strike and expiry from the
live book at run time, so it stays valid as the book turns over. All of
its fills run --dry-run.

Also document two things that cost real debugging time and are not
discoverable from this repo: the Odette web UI only renders positions
whose referrer matches its own address, and it reads a different indexer
than the SDK, refreshed on a 60-minute cron that the browser pokes after
its own trades but the CLI does not.
Five findings from an adversarial review of the book check output, all
about the gap between what check CLAIMS the book can do and what the
command it recommends actually does.

- `availableSize` summed every matching maker, but preview/fill resolve
  exactly one order. With 5 contracts at $1 and 5 at $2, `--size 8` read
  as "fully available" and then filled 5. Report the ladder as
  `priceLevels`, state what one invocation takes as `nextStepMaxSize`,
  and key `partialFillAvailable` off that instead of the aggregate.
- Structures carrying the requested strike were sorted by whole-structure
  premium and the winner's command promoted to the top-level nextStep. A
  spread, fly, condor and ranger sharing one strike are different
  products, and the strike can be a long, short or middle leg — so
  "cheapest" could open exposure opposite to the vanilla being priced.
  They are now unranked alternatives carrying `legIndex`/`legCount`, and
  the top-level step is prose (`nextStepIsCommand: false`).
- A sell-side match recommended `orderbook` and then handed back an
  `rfq build` command — the exact off-book route that recommendation
  exists to prevent. Sells now return a dApp action. Per-structure steps
  on a sell result no longer emit `book preview` either: preview filters
  to asks, so it cannot preview the bid that matched.
- Structure-only results ignored `--size`, pairing `orderbook` with
  `availableSize: null`. Each match now carries `meetsRequestedSize`.
- A one-off `--referrer` never reached the generated workflow, so a
  copied command silently fell back to address zero. It is now reported
  as `referrer` and baked into every emitted command, and global flags
  appear under Global Options in subcommand help.
@Shawnchee
Shawnchee merged commit be29dcc into main Aug 20, 2026
6 checks passed
@Shawnchee
Shawnchee deleted the fix/cli-book-check-referrer branch August 20, 2026 05:57
@Shawnchee
Shawnchee restored the fix/cli-book-check-referrer branch August 20, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant