feat(rebalance): alert when the market maker needs cNGN, on share not on idle USDC - #70
Merged
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
CNGN_FLOOR_USD($100)CNGN_MIN_SHARE(35%) of valueSame live state now reads
cNGN 52.9%,action none.inventory.test.tspins 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 — andassertWithdrawalPolicyrefuses any withdrawal whose signer is not the owner: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.
checkmakes 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:Signon 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 node→ all checks passed--alertrefuses to run withoutALERT_WEBHOOK_URLrather than logging and exiting 0 — an alert path that reaches nobody while reporting success is the failure mode this repo keeps finding{text, content}, so one webhook serves Slack and DiscordNot 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