Skip to content

feat(rebalance): alert when the market maker needs cNGN, on share not on idle USDC - #70

Merged
robertleifke merged 1 commit into
mainfrom
feat/rebalance-inventory-check
Sep 18, 2026
Merged

robertleifke merged 1 commit into
mainfrom
feat/rebalance-inventory-check

Conversation

@robertleifke

Copy link
Copy Markdown
Contributor

What

pnpm rebalance check [--alert] reads the market maker's subaccount, values both legs through the same HyperFX feed a rebalance would trade at, and says whether to act.

The failure it watches for is the bid side going dark. The market maker sells cNGN for USDC and never the other way, so cNGN drains while USDC piles up, and the first visible symptom is an order book with no bids — by which point the venue has already stopped quoting one side.

The trigger is a share, not an absolute

I built it first on "idle USDC over $200". Run against live state, it said:

valued      USDC $310.00 / cNGN $347.80
action      rebalance
  - idle USDC is $310, at or over the $200 threshold

That book is balanced and slightly cNGN-heavy. Converting more USDC would have made the imbalance worse. The alert was wrong the first time it ran, which is how an operator learns to ignore one.

So the condition is cNGN's share of inventory value:

condition action
cNGN under CNGN_FLOOR_USD ($100) urgent — bids about to go dark
cNGN under CNGN_MIN_SHARE (35%) of value rebalance — convert some USDC
USDC within 20% of the $800 halt noted in the message

Same live state now reads cNGN 52.9%, action none. inventory.test.ts pins those exact balances as healthy so it stays fixed.

Why an alert rather than an automated loop

This is the part worth reviewing. Withdrawals pay out only to the subaccount owner — the action data is (asset, amount) with no recipient field — and assertWithdrawalPolicy refuses any withdrawal whose signer is not the owner:

action.signer must be action.owner; session-key withdrawals are not supported

So USDC leaving sub 15 lands at the market maker's wallet, signed by the market maker's key. No delegation to the rebalance signer is possible, and the operator is unavoidably in the loop for that one step. check makes it reliably prompted rather than remembered.

Automating it properly means the market maker doing the withdrawal itself — it already holds the key and already knows when USDC is piling up — which is a change to the Go service and separate work.

Granting this service kms:Sign on the MM key would also work and should not be done: KMS grants aren't partial, so it would confer full market-maker authority (cancel every order, withdraw everything), a wider blast radius than the executor separation this key exists to preserve.

Verification

  • ./scripts/verify.sh nodeall checks passed
  • 15 tests; the new ones assert each threshold fires, plus an empty subaccount is treated as empty rather than lopsided, and the cNGN leg is valued at the rate rather than counted in tokens (136,832 cNGN is a six-figure balance and about $100)
  • --alert refuses to run without ALERT_WEBHOOK_URL rather than logging and exiting 0 — an alert path that reaches nobody while reporting success is the failure mode this repo keeps finding
  • Webhook payload matches the canary's {text, content}, so one webhook serves Slack and Discord

Not wired to a scheduler

Deliberately. It's a command; where it runs — the ops box timer alongside numo-settlement-canary.timer, or somewhere else — is an ops decision, and the interval wants a few days of real readings behind it.

🤖 Generated with Claude Code

… on idle USDC

The failure this watches for is the bid side going dark. The market maker's
flow is one-directional -- it sells cNGN for USDC -- so cNGN drains while USDC
piles up, and the first visible symptom is an order book with no bids. By then
the venue has stopped quoting one side.

`pnpm rebalance check [--alert]` reads the subaccount, values both legs through
the same HyperFX feed a rebalance would trade at, and says whether to act.

The trigger is cNGN's SHARE of inventory value, not an absolute USDC figure.
The first version alerted on "idle USDC over $200" and, run against live state,
fired on sub 15 holding $310 USDC against $348 of cNGN -- a balanced book where
converting would have worsened the imbalance. Caught by running it, not by
reading it. A threshold that is wrong the first time it runs is one an operator
learns to ignore, so `inventory.test.ts` pins those exact balances as healthy.

  cNGN under $100                  urgent    bids about to go dark
  cNGN under 35% of inventory      rebalance convert some USDC
  USDC within 20% of the $800 halt noted in the message

`--alert` refuses to run without ALERT_WEBHOOK_URL rather than logging and
exiting 0, and posts the canary's `{text, content}` shape so one webhook serves
Slack and Discord.

Why this is an alert and not an automated loop: withdrawals pay out only to the
subaccount owner -- the action data is `(asset, amount)` with no recipient --
and assertWithdrawalPolicy refuses any withdrawal whose signer is not the owner
("session-key withdrawals are not supported"). USDC leaving sub 15 therefore
lands at the market maker's wallet, signed by the market maker's key, and no
delegation to the rebalance signer is possible. Automating it properly means the
market maker doing the withdrawal itself, which is a change to the Go service.
Granting this service kms:Sign on the MM key would also work and should not be
done: KMS grants are not partial, so it would confer full market-maker
authority and undo the separation this key exists to preserve.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@robertleifke
robertleifke merged commit e49627f into main Sep 18, 2026
1 check passed
@robertleifke
robertleifke deleted the feat/rebalance-inventory-check branch September 18, 2026 05:25
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