Skip to content

feat(skills): add treasury-liquidation skill with gas-aware sweeps - #123

Open
AsDaria wants to merge 2 commits into
zeriontech:mainfrom
AsDaria:feat/treasury-liquidation-skill
Open

AsDaria wants to merge 2 commits into
zeriontech:mainfrom
AsDaria:feat/treasury-liquidation-skill

Conversation

@AsDaria

@AsDaria AsDaria commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Adds a two-phase multi-chain treasury skill:

  1. Drain — send every token above a floor from wallet A to wallet B across all chains as raw transfers, so asset identity and cost basis survive and the sale can happen later in B.
  2. Liquidate — convert a wallet's whole multi-chain portfolio into USDC on one destination chain, keeping a gas reserve everywhere.

They compose (drain A→B, then liquidate B) or run independently.

Why this isn't just a copy of a working script

Two structural gas bugs showed up across real multi-chain runs. Both are fixed here rather than shipped:

Stage A now reserves gas for the stage-B bridge that follows it

Every swap spends the chain's native token, and stage B then needs one more transaction to bridge the proceeds out. Sweeping a chain until its gas runs out converts sellable tokens into an unsendable pile of USDC/WETH — strictly worse than not sweeping, because the original assets are gone too. The bridge fails not_enough_base_asset_balance and the value sits there.

gas.affordable_swaps() caps each chain's sweep at what it can fund while still paying for the bridge, defers surplus rows lowest-value-first (with a log line, never silently), and skips chains that cannot fund even one bridge — with a top-up hint instead of a confusing downstream failure.

Observed twice:

  • 44 swaps on robinhood spent $12 of ETH (~$0.27 each) and left $1,893 of WETH unbridgeable
  • a polygon sweep attempted 3 swaps while holding $0.00 of POL

Gas reserves are now USD-aware

A native-unit reserve is worth cents on cheap chains — 1 POL ≈ $0.20, 1 XDAI ≈ $1.00, 1 S ≈ $0.03. Combined with a flat "skip if balance < $5" test, a chain passes the check, bridges nearly everything home, and is left holding a reserve too small to broadcast anything. Its residue is then stranded until someone tops it up by hand.

reserve_for(chain, price) keeps the larger of the per-chain native figure and MIN_RESERVE_USD ($2.50). Stage C applies MIN_BRIDGE_USD to the amount actually leaving (balance minus reserve) rather than the pre-reserve balance. Stage D's native swap and drain.py take the same fix.

The cost of both guards is a few dollars of gas retained per chain. That's the intended trade: a chain with gas can be retried, a chain without gas cannot.

Calibration note

DEFAULT_SWAP_GAS_USD is rounded up from the observed ~$0.27/swap. An earlier $0.12 estimate would have permitted 96 swaps on the $12 robinhood balance and reproduced the exact failure the cap exists to prevent — so the constant is deliberately conservative. Overestimating leaves a little dust; underestimating strands a whole chain's proceeds.

Contents

file role
SKILL.md workflow, staging rationale, what the CLI cannot do
references/gotchas.md failure modes that are silent, expensive, and not inferable from CLI output
scripts/preflight.py auth check — an invalid API key makes every command hang forever with no error
scripts/inventory.py wallet vs DeFi positions, unreachable chains, broken price feeds
scripts/drain.py phase 1 — raw sends A→B
scripts/liquidate.py phase 2 — 4-stage sweep + bridge → USDC
scripts/verify.py on-chain truth via RPC, never CLI output
scripts/gas.py per-chain reserves and gas affordability

Testing

  • All scripts compile (python3 -m py_compile)
  • affordable_swaps() regression-checked against both real failures: robinhood $12 → caps at 38 swaps (below the 44 that broke it); polygon $0.00 → 0, chain skipped up front
  • Reserve math verified across cheap-gas and ETH-denominated chains

Scripts are stdlib-only (no new dependencies) and contain no addresses or credentials beyond the public USDC mainnet contract in verify.py.

🤖 Generated with Claude Code

AsDaria and others added 2 commits September 1, 2026 22:59
Adds a two-phase multi-chain treasury skill: drain every token above a
floor from one wallet to another via raw transfers (preserving asset
identity and cost basis), then liquidate a wallet's whole portfolio into
USDC on a single destination chain.

Both structural gas bugs found in real runs are fixed rather than shipped:

Stage A now reserves gas for the stage-B bridge that follows it. Each swap
spends the chain's native token and stage B needs one more transaction to
bridge the proceeds out, so sweeping until gas runs out converts sellable
tokens into an unsendable pile of USDC/WETH -- worse than not sweeping,
since the original assets are gone too. gas.affordable_swaps() caps each
chain's sweep at what it can fund while still paying for the bridge,
defers surplus rows lowest-value-first, and skips chains that cannot fund
even one bridge. Calibrated on an observed ~$0.27/swap: 44 swaps on
robinhood ate $12 of ETH and stranded $1,893 of WETH behind them.

Gas reserves are now USD-aware. A native-unit reserve is worth cents on
cheap chains (1 POL ~ $0.20, 1 S ~ $0.03), so a chain would pass the flat
"$5 of balance" test, bridge nearly everything home, and be left unable to
broadcast -- stranding its residue until a manual top-up.
reserve_for(chain, price) keeps the larger of the native figure and
MIN_RESERVE_USD, and stage C applies MIN_BRIDGE_USD to the amount actually
leaving rather than the pre-reserve balance. drain.py takes the same fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h deposits

Field-tested on the first live run of the gas-aware sweep: a chain was
deliberately topped up with native gas, the bridge reported delivered, and
eth_getBalance confirmed 132 POL on-chain -- while the positions API still
returned $0.00 native, so the affordability check false-skipped the chain.
The same lag undersized a bridge read from positions right after a sweep
($3 carried instead of $100). Skip message and gotchas now say to confirm
a zero reading via RPC before trusting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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