When to Use
Use this skill when a crypto trading bot holds working capital and you need a defensible answer to "how much should be reachable by the trading key right now, and what moves when it isn't?" Keeping 100% of capital in exchange hot wallets or online keys exposes it to counterparty collapse (FTX), hot wallet compromise, and API key theft. The engine turns a balance snapshot plus the trading key's permission set into an auditable sweep, refill, or hold proposal, and escalates a mandated hot-wallet ceiling separately from an ordinary rebalance.
It is a stateless, point-in-time policy evaluator. It proposes; it never signs, broadcasts, or tracks a transaction.
When NOT to Use
- As the executor. The engine returns a proposal. Signing, broadcasting, confirming, and retrying are the caller's job, and the caller owns transfer idempotency — feed settled results back through
pending_transfer_*rather than assuming the engine remembers anything. - As a compliance determination for client assets. The mandated caps below apply to client virtual assets held by a licensed platform. If you custody customer funds, the binding number is your regulator's, and its measurement basis (Korea marks monthly on a trailing-year average KRW value, not on a live snapshot) may not be the instantaneous ratio this engine computes.
- As a per-venue exposure control. The engine models one aggregate hot balance. Ten exchanges each at 15% is a 15% hot ratio and ten separate counterparty failure points. Cap per-venue exposure separately.
- On unmarked or multi-currency balances. Inputs are USD-denominated. The engine has no price source and cannot detect a stale or inconsistent mark, so a ratio computed from mixed-timestamp marks is garbage the engine will happily act on.
- As a full API key audit.
is_api_key_securemeans "no fund-moving permission is enabled among the three checked". It says nothing about key age, rotation, storage, or scope creep — seeapi-key-least-privilege-audit-tool.
Prerequisites
- Balances marked to USD at a single consistent timestamp:
hot_wallet_usd,cold_vault_usd,warm_buffer_usd. All must be finite and non-negative. - Unsettled transfers already submitted:
pending_transfer_to_cold_usd,pending_transfer_to_hot_usd. Omitting these is the single most expensive mistake here — a scheduled audit that cannot see its own in-flight sweep will re-propose it on every run. - The trading key's actual permission set, read from the exchange rather than assumed. Binance's Get API Key Permission endpoint returns
enableWithdrawals,enableInternalTransfer,permitsUniversalTransfer, andipRestrict; all four matter. - A threshold band. The defaults (
target_hot_ratio=0.15,max_hot_ratio_threshold=0.25,min_hot_ratio_threshold=0.05) are engineering defaults with no regulatory basis. Setregulatory_max_hot_ratioif a mandated ceiling binds you.
Workflow
- Validate before computing. Every balance is checked for finiteness and non-negativity, and the total for strict positivity. This is not ceremony:
NaNcomparesFalseagainst every threshold, so an unvalidated corrupt balance falls through to a confidentPORTFOLIO_BALANCED. The engine raisesHotColdWalletError(aValueErrorsubclass) instead. - Net in-flight transfers before deciding. Total = Hot + Cold + Warm. Effective Hot = Hot − pending-to-cold + pending-to-hot. Pending transfers are still inside the treasury, so they change the hot balance the treasury is converging to without changing the denominator. Decisions use the effective ratio; the report carries both —
current_hot_ratiofor the audit trail,effective_hot_ratiofor the decision. - Audit every fund-moving permission, not just withdrawals.
enableWithdrawals=falseis not sufficient:enableInternalTransferandpermitsUniversalTransfereach move funds on their own. Any one of the three triggersCRITICAL_SECURITY_ALERT, halts rebalancing, and returns a zero transfer — you do not route treasury movements through a key you are about to revoke. An unassessedapi_key_ip_restrictedis reported as unassessed rather than counted as hardened. - Compare on unrounded ratios. Rounding the ratio before the threshold test lets a 25.004% breach round to exactly 25.00% and pass a strict
> 25%control. Rounding is applied to the reported figures only. - Size the transfer, then cap it at what the funding wallet actually holds. A sweep can only move coins settled and uncommitted in the hot wallet; a refill can only draw cold that is not already committed to an in-flight refill. When the requirement exceeds availability the proposal is capped and
is_transfer_fully_fundablegoesFalse— never hand an automated executor an instruction that cannot settle. - Escalate a mandated breach distinctly. With
regulatory_max_hot_ratioset, the ceiling binds ahead of the engine's own band: the sweep trigger becomesmin(max_hot_ratio_threshold, cap)and the sweep targetmin(target_hot_ratio, cap). Breaching the cap yields statusREGULATORY_HOT_CAP_BREACHand aCRITICALlog line, not an ordinaryREBALANCE_REQUIRED.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Re-proposing a sweep that is already in flight. Chain settlement is slow and this engine is stateless. Run the audit every minute against raw balances and an automated executor will fire the same $150k sweep sixty times before the first one confirms. Pass
pending_transfer_to_cold_usdso the engine proposes the increment: with $40k already moving out of a $300k hot wallet, it asks for $110k, and the two transfers sum to exactly the $150k a single sweep would have moved. - Double-counting a broadcast transfer. A pending amount must still be counted in its source wallet — an unsettled sweep is part of
hot_wallet_usd, an unsettled refill part ofcold_vault_usd. Most balance feeds debit a transfer the moment it is broadcast; passing it as pending as well subtracts it twice, collapsing the effective ratio and provoking a spurious refill in the opposite direction. The engine rejects the incoherent case (pending > source balance) rather than acting on it, but a partial double-count within the balance is still yours to avoid. - Treating
enableWithdrawals=falseas "the key cannot move funds". It cannot reach an external address, butenableInternalTransferandpermitsUniversalTransferstill shift balances between the operator's own account types and products — enough for an attacker to strand funds where the bot cannot trade them, and enough to defeat a withdrawal-whitelist control. - Rounding the ratio before testing it against the cap. A treasury at 25.004% is over a 25% cap. Round-then-compare silently reports it as balanced, and the error is invisible precisely at the boundary the control exists to defend.
- Proposing a refill the cold vault cannot fund. Ratio arithmetic says "move $120,000 from cold to hot" regardless of whether cold holds $120,000. Check fundability, or the executor fails mid-run in a state neither wallet expects.
- Assuming 15%/25%/5% is a standard. It is not. No US regulator prescribes a numeric split — NYDFS 23 NYCRR Part 200 imposes custody and segregation duties without a percentage. The numbers that are mandatory are far stricter and apply to client assets: 2% (Hong Kong), 5% (Japan), 20% (South Korea). Running the defaults while holding client assets in those jurisdictions is a licence problem, not a tuning choice.
- Configuring a band that cannot converge. A 30% target under a 25% cap proposes a "fix" that leaves the treasury at 30% — still breaching the cap that triggered it. Likewise a 5% operating floor under a 2% mandated ceiling guarantees a permanent breach every time the treasury refills to its own floor. Both configurations are rejected at construction.
- Reading one aggregate ratio as counterparty safety. The engine's denominator is the whole treasury. It cannot tell 15% on one exchange from 15% spread across ten.
Verification
- Hot $300,000 / Cold $700,000 (total $1M, 30% > 25%) ⟹
SWEEP_TO_COLDof $150,000 down to the 15% target. Hot $30,000 / Cold $970,000 (3% < 5%) ⟹REFILL_HOT_FROM_COLDof $120,000. - Re-run the first case with
pending_transfer_to_cold_usd=150_000.0and confirmHOLD_BALANCESwith a $0 proposal,current_hot_ratio == 0.30andeffective_hot_ratio == 0.15. With $40,000 pending instead, confirm an incremental $110,000. - Submit Hot $250,040 / Cold $749,960 (25.004%) and confirm
SWEEP_TO_COLDof $100,040 — notHOLD_BALANCES. - Submit
hot_wallet_usd=float("nan")and confirmHotColdWalletErrorrather thanPORTFOLIO_BALANCED; submit a negative balance and confirm the same. - Set only
api_key_universal_transfer_enabled=Trueand confirmCRITICAL_SECURITY_ALERTwithis_api_key_secureFalse. - Request a $120,000 refill from a $20,000 vault and confirm the proposal is capped at $20,000 with
is_transfer_fully_fundableFalse. - Construct
HotColdWalletManagerEngine(target_hot_ratio=0.30, max_hot_ratio_threshold=0.25)and confirmHotColdWalletError. - Pass
pending_transfer_to_cold_usdgreater thanhot_wallet_usdand confirmHotColdWalletErrorrather than a spurious refill in the opposite direction; likewise Hot $1e308 / Cold $1e308, which overflows the total. - Run
python -m unittest discover -s skills/hot-cold-wallet-split-for-trading-bots/scriptsand confirm a 100% pass rate.