@mellow-finance/cli
v0.3.0
Published
A CLI built with Ink and React
Keywords
Readme
Mellow CLI
A dual-mode command-line interface for interacting with Mellow Core Vaults: deposit and redeem queues for LPs, and a payload queue for curators to build, review and execute batched vault operations, including rebalances. Supports an interactive terminal UI (TUI) for exploratory use and a non-interactive mode for scripting and automation.
Quick Start
# Install
npm install -g @mellow-finance/cli
# Set your private key
export MELLOW_CLI_USER_PK=0x<your-64-char-hex-private-key>
# Launch TUI (interactive mode)
mellow-cli --vault earnETH
# Run a single command (non-interactive)
mellow-cli overview --vault earnETH --non-interactiveInstallation
Requires Node.js >= 20.
Install the CLI globally from npm:
npm install -g @mellow-finance/cliThis makes the mellow-cli command available system-wide. Verify the installation:
mellow-cli --helpUpgrading
To upgrade to a newer version, re-run the same install command:
npm install -g @mellow-finance/cliBuilding from source
If you want to contribute or run the latest unreleased code:
# Clone the repository
git clone <repo-url>
cd mellow-cli
# Install dependencies
npm install
# Build the TypeScript source
npm run build
# The built binary is available at ./dist/cli.js
# You can also run it via the package name:
npx mellow --vault earnETHNote for contributors: If you later want to switch from a source build to the published npm package, remove any locally linked binary first:
rm "$(which mellow-cli)" npm install -g @mellow-finance/cliThis is needed because
npm installfrom source registers the binary differently than the published package, andnpm uninstallwon't clean it up.
For development with live reload:
npm run devEnvironment Variables
| Variable | Required | Description |
|---|---|---|
| MELLOW_CLI_USER_PK | For --non-interactive and --signer private_key | Your Ethereum private key in hex format, with or without the 0x prefix. In TUI mode it is optional: a valid key skips the wallet-connection prompt, and anything else falls back to it |
| MELLOW_API_TOKEN | For most batch | Curator API token for the payload queue's endpoints, used when --api-token is not given. Among non-interactive invocations, batch rebalance-list and batch oracle-report --dry-run need none |
| MELLOW_API_BASE_URL | No | Mellow API base URL, without a trailing slash. Defaults to https://api.mellow.finance |
The private key must be a 64-character hexadecimal string (32 bytes). The 0x prefix is optional — bare hex, as most key-export tools emit it, is accepted and prefixed for you — and surrounding whitespace is trimmed. The CLI derives your wallet address from this key automatically.
How the key interacts with wallet selection depends on the mode:
- TUI mode, no
--signer— a valid key selects the Private Key signer and the "Select wallet connection method" screen is skipped. If the variable is unset, empty, or malformed, that screen opens instead, with the format error shown above the list so WalletConnect or Ledger stays reachable. - TUI mode,
--signer private_key— the key is required and a malformed one is a fatal error, because the signer was named explicitly. --non-interactive— there is no screen to fall back to, so a missing or malformed key is a fatal error whenever a signer is needed.
MELLOW_API_BASE_URL points every API call — the vault list and the curation endpoints alike — at a different environment. It exists because the payload queue's endpoints (batch, batch rebalance and batch oracle-report included) are deployed to staging only for now:
export MELLOW_API_BASE_URL=https://api.stage.mellow.financeSet it for the whole invocation, not per call: the vault you resolve and the payload queue you build against must come from the same environment.
Setting the variable:
# Linux / macOS
export MELLOW_CLI_USER_PK=0xabc123...def456
export MELLOW_CLI_USER_PK=abc123...def456 # the 0x prefix is optional
# Windows (PowerShell)
$env:MELLOW_CLI_USER_PK = "0xabc123...def456"
# Windows (cmd)
set MELLOW_CLI_USER_PK=0xabc123...def456Security note: Avoid committing your private key to version control or shell history. Consider using a
.envfile (excluded from git) or a secrets manager.
Curator API token
Among non-interactive invocations, every batch operation is authenticated except batch rebalance-list and batch oracle-report --dry-run: create a token in the curation UI, then supply it either way:
# As a flag, one invocation at a time
mellow-cli batch --vault strETH --api-token <your-token> --non-interactive
# Or as an environment variable, for every invocation
export MELLOW_API_TOKEN=<your-token>The token is read fresh from the flag or the environment on every request and is never written to disk. Among non-interactive invocations, two need no token: batch rebalance-list, which only reads the unauthenticated options endpoint, and batch oracle-report --dry-run, which only reads the unauthenticated new-oracle-report endpoint. Both let you explore before you have a token.
Security note: the same rule applies here as to your private key — do not commit the token or put it in shell history you share.
<your-token>above is a placeholder, not a real credential.
Commands
The CLI covers the full LP lifecycle (overview, my-position, deposit, redeem) plus the payload queue (batch), through which curators build, review and execute a vault's own operations.
Breaking change: the top-level
rebalancecommand is gone. Rebalancing is nowmellow-cli batch rebalance, one of the payload queue's subcommands, and it queues the calls rather than executing them — nothing moves on-chain until you runmellow-cli batch submit.rebalance listis nowmellow-cli batch rebalance-list.
| Command | Description |
|---|---|
| overview | Display vault APY, TVL, and allocation breakdown |
| my-position | Display your vault position and pending requests |
| deposit | Deposit assets into a vault queue |
| deposit cancel | Cancel a pending deposit request |
| deposit claim | Claim processed deposit shares |
| redeem | Redeem vault shares into a withdrawal queue |
| redeem claim | Claim all claimable redemption requests |
| batch | Inspect the payload queue (curator) |
| batch list | Inspect the payload queue (curator) |
| batch add-call | Append a catalogued vault operation to the payload queue, discovered step by step |
| batch add-raw | Append a hand-written call to the payload queue |
| batch remove | Remove one item from the payload queue |
| batch clear | Remove every item from the payload queue |
| batch submit | Submit the payload queue through an executing account |
| batch export | Export the payload queue as a Safe Transaction Builder document |
| batch rebalance | Queue a move of assets between the vault, its subvaults and swap modules (curator) |
| batch rebalance-list | List rebalance sources, destinations and assets (curator; needs no API token) |
| batch oracle-report | Queue the candidate oracle report for the vault (curator) |
account and account logout additionally report and clear the connected signer.
--help is per command: mellow-cli --help prints the command overview and the global flags, and mellow-cli <command> [subcommand] --help prints that command's own flags, subcommands and examples. Command help needs no --vault and makes no network call, so mellow-cli batch rebalance --help works before you know which vault you want.
Arguments
| Flag | Short | Description |
|---|---|---|
| --vault | -v | Vault identifier (required for all commands) |
| --queue | -q | Queue identifier (required for deposit and redeem commands) |
| --amount | -a | Amount to deposit, redeem or batch rebalance (human-readable, e.g. "1.5"). For batch rebalance also accepts wei:<integer> and max |
| --api-token | | Curator API token for the payload queue; falls back to MELLOW_API_TOKEN |
| --signer | -s | Signer type: private_key, walletconnect, calldata or ledger (auto-detected by default) |
| --via-safe | | Safe address to route transactions through (proposes to the Safe multisig). Refused on every batch invocation, a bare batch included: no batch path uses the routing it selects, and the queue picks its own executing account |
| --via-mellow-account | | MellowAccount address to route transactions through (batches via multicall). Refused on every batch invocation too, for the same reason |
| --fork-url | -f | Fork RPC URL, overriding all chain transports |
| --non-interactive | | Run in non-interactive mode with tagged stdout output |
| --help | -h | Show help: the global overview, or a command's own help when a command is given |
Payload queue (batch) arguments
| Flag | Used by | Description |
|---|---|---|
| --subvault | add-call | Subvault index to append an operation to; omit to list the vault's subvault indexes on the chain this append targets |
| --call-index | add-call | Index of the catalogued operation within the subvault; omit to list the subvault's operations |
| --input | add-call | One value per declared input, in declared order; repeat the flag once per input |
| --to | add-raw | Call target address (required) |
| --data | add-raw | Calldata as 0x-prefixed hex (required) |
| --value | add-raw | Native value as a decimal string, defaults to 0 |
| --chain | add-call, add-raw, rebalance, rebalance-list | The chain to act on, as a network name (base) or a chain id (8453). Accepted on exactly those four subcommands; see --chain |
| --item | remove | Item id, or its 0-based position in the current queue (required). The position is exactly the # column of the TUI listing; the id is on that listing's call-detail screen |
| --account | submit | Executing account address, when several could execute the queue |
| --file | export | Path to write the document to. With --non-interactive or off a TTY it defaults to standard output; on an interactive invocation it prefills the destination prompt, which otherwise defaults to payload-queue.json |
| --source | rebalance (required), rebalance-list (optional narrowing) | Source address or label ("vault", "subvault:0", "swapmodule:1") |
| --destination | rebalance (required), rebalance-list (optional narrowing) | Destination, same forms as --source |
| --asset | rebalance (required) | Asset address, or symbol when exactly one asset carries it |
| --dry-run | oracle-report | Print the candidate report and stop, queueing nothing. Turns the invocation non-interactive, and needs no API token |
| --force | oracle-report | Queue the report even when it is blocked: it would revert, or validation was unavailable. Cannot be combined with --dry-run |
| --timestamp | oracle-report | Unix timestamp in seconds to price the report at, which must be a positive whole second no later than now |
| --reward-asset | oracle-report | Reward asset symbol, passed together with --reward-amount |
| --reward-amount | oracle-report | Reward amount as a decimal string, passed together with --reward-asset |
All four of --source, --destination, --asset and --amount are required for batch rebalance: there is no interactive selection in non-interactive mode. They are validated before the signer connects, so a missing flag never costs you a Ledger unlock or a WalletConnect scan — the error names every flag that is missing.
batch rebalance-list requires none of them -- only --vault. With --non-interactive it only reads: it never connects a signer, and --source/--destination merely narrow the listing. Without --non-interactive it opens the rebalance wizard (see TUI Mode), whose later steps do sign.
--vault
Accepts any of the following:
- Address -- full Ethereum address (e.g.
0x1234...abcd) - Slug / ID -- vault identifier from the API (e.g.
earnETH) - Symbol -- share token symbol (e.g.
earnETH)
The CLI fetches all available vaults and matches against address, slug, and symbol. If no match is found, the CLI lists all available vaults and exits.
--queue
Accepts any of the following:
- Address -- full queue contract address (e.g.
0xABC...DEF) - Asset symbol -- the token symbol accepted by the queue (e.g.
WETH,weth)
Queue matching is case-insensitive for asset symbols. If multiple queues match the same symbol, the CLI asks you to specify by address instead.
--source and --destination
Accepts either of the following:
- Address -- the vault, subvault or swap module contract address. Always unambiguous, and the form to fall back to.
- Label --
vault,subvault:<index>orswapmodule:<index>.
Labels are case-insensitive, and the server's own labels are accepted as written -- separators (:, spaces, -, _, parentheses) are all equivalent. So every column below selects the same option:
| Shorthand | Server label (copy it out of the TUI table) | Also works |
|---|---|---|
| subvault:0 | Subvault 0 | SUBVAULT 0, subvault-0 |
| swapmodule:1 | SwapModule (Subvault 1) | swap_module:1 |
| vault | Vault | VAULT |
On a multi-chain vault the same index exists on several chains -- strETH has a Subvault 0 on ethereum, plasma and arbitrum. A label matching more than one option resolves to the one on the chain this rebalance targets, which is what --chain names, else the chain the queue is pinned to, else the vault's home chain (see --chain). A label that is still undecidable -- the targeted chain holding either no match or more than one -- is rejected as ambiguous, and the error lists every candidate with its chain so you can pass an address instead. A label matching exactly one option, on a chain other than the targeted one, is refused with not on the chain this rebalance targets, naming both chains and suggesting --chain <name|id> to target the option's own.
--asset
Accepts either of the following:
- Address -- the token contract address.
- Symbol -- e.g.
WETH,weth, matched case-insensitively, and only when exactly one movable asset carries that symbol.
The candidate list is whatever the API reports as movable between the selected source and destination, together with the source-side balance of each. An asset whose on-chain metadata could not be read has no symbol and can only be selected by address.
--amount
For deposit and redeem, a human-readable decimal (1.5).
For batch rebalance, three forms:
| Form | Meaning |
|---|---|
| 1.5 | Human units, scaled by the asset's decimals. Rejected if the amount is more precise than the asset can represent, or if the asset's decimals could not be read |
| wei:1500000000000000000 | Raw token units, no scaling. The escape hatch for assets with unreadable decimals |
| max | The whole source-side balance the API reported. Errors if that balance could not be read -- it never silently means zero |
--chain
Accepts either of the following:
- Network name -- one of the nine networks the CLI knows:
ethereum,base,holesky,hoodi,monad,arbitrum,plasma,hyperevm,mezo. - Chain id -- the decimal id of one of those same nine, e.g.
8453. Anything else, an id the CLI does not know included, is rejected with the whole list of known networks.
--chain is read by exactly four invocations -- batch add-call, batch add-raw, batch rebalance and batch rebalance-list. Passing it to any other command or subcommand is an error, not a silent no-op as an unknown flag would once have been; on batch submit the refusal adds that a submit executes on the chain the queued items already carry. It is also refused whenever the invocation will run interactively, since the TUI screens take the chain from the row you select -- pass --non-interactive alongside it.
Precedence, on the three subcommands that read the payload queue (add-call, add-raw, rebalance):
- the chain
--chainnames, when it is given; - otherwise the chain the queue is already pinned to, which is the chain of the items it holds;
- otherwise the vault's own home chain.
A --chain that disagrees with a queue already holding items is refused, naming both chains: one queue submits on one chain, so submit or clear it before appending elsewhere. batch rebalance-list never reads the queue, so its own precedence is only --chain, else the vault's home chain.
The queue itself is always read and written through the curation API's home-chain URLs, whichever chain its items execute on, and batch submit takes its chain from those items rather than from a flag.
TUI Mode (Interactive)
TUI mode is the default when the CLI detects an interactive terminal. Launch it by running any command without --non-interactive:
# Open the command palette (entry screen)
mellow-cli --vault earnETH
# Open directly on a specific screen
mellow-cli overview --vault earnETH
mellow-cli deposit --vault earnETH
mellow-cli redeem --vault earnETH
mellow-cli my-position --vault earnETH
mellow-cli batch --vault strETH
mellow-cli batch rebalance --vault strETHCommand Palette
When launched without a specific command, the TUI opens an entry screen with a command palette. Type to filter commands by prefix (e.g. /d shows /deposit, /deposit cancel, /deposit claim; /b shows every /batch entry, including /batch rebalance, /batch rebalance-list and /batch oracle-report). Navigate with arrow keys and press Enter to select.
/batch, /batch list, /batch add-call, /batch add-raw, /batch remove, /batch clear, /batch submit, /batch export and /batch oracle-report all open the same Payload Queue screen. /batch add-call, /batch add-raw and /batch oracle-report open on their own step; /batch, /batch list, /batch remove, /batch clear, /batch submit and /batch export all open on the queue listing. /batch submit and /batch export each continue by themselves once that listing lands -- /batch submit into the submit flow, /batch export into the destination prompt -- so the queue each acts on is the one it just showed you. An empty queue stays on the listing and reports itself as empty instead, for both. On the listing itself, "s" starts the submit and "e" starts the export.
/batch rebalance-list opens the same Rebalance screen as /batch rebalance, whose first step already lists every source interactively -- that step is the discovery view in TUI mode. There is no read-only listing screen: continuing past that step is the ordinary rebalance wizard, and --source/--destination are ignored here (they narrow the --non-interactive listing only).
TUI Navigation
- Arrow keys -- navigate table rows and command suggestions
- Enter -- select an item or confirm an action
- q -- exit the application. Ignored while a screen is accumulating typed characters into a field, where the
qgoes into the field instead: the command palette, the Payload Queue screen's five text-entry steps (select-call,input-fields,raw-target,raw-value,raw-calldata), and the amount step of the Deposit, Redeem and Rebalance screens.qis also inert throughout the signer selector and throughout the connecting and connection-error phases, where each screen owns its own input - Escape -- exit the application, or leave the current screen
- Escape / Backspace -- on the multi-step Rebalance screen, walk back exactly one step (Escape only on the amount step, where Backspace edits the field)
- K / J -- on the Payload Queue screen's listing step, move the selected item up or down (reordering is interactive-only; there is no non-interactive equivalent)
- s -- on the Payload Queue screen's listing step, jump to the submit step; refused while the listing is loading or failed, on an empty queue, and while a
K/Jreorder is still unconfirmed - a -- on the Payload Queue screen's listing step, jump to add a call; refused while the listing is loading or failed
- d -- on the Payload Queue screen's listing step, remove the highlighted item; refused while the listing is loading or failed, and on an empty queue. It asks for confirmation first, naming the item's position, id and target, and nothing is deleted until Enter accepts it
- C -- on the Payload Queue screen's listing step, clear every item from the queue; refused while the listing is loading or failed, and on an empty queue. It asks for confirmation first, naming the item count. Opening the screen as
/batch clearasks for that same confirmation as soon as the listing lands - Enter -- on the Payload Queue screen's listing step, open the highlighted item's call detail; refused while the listing is loading or failed, and on an empty queue. Unlike
s, an unconfirmedK/Jreorder does not refuse it - v -- on the Payload Queue screen's call-detail step, switch between formatted and raw values. Raw mode widens the argument tree's leaves, the
Targetfield from a shortened address to the wholetransaction.to-- itself cut to the value column when it exceeds it, which a 42-character address never does -- and theCalldatafield from its 4-byte selector to as much of the hex string as the value column holds, cut with a trailing..., which all but the shortest calldata is;Id,Value,Forwardsand the warning lines read the same in either mode. The key is always live on this step, and its hint accompanies a resolved call's detail view, de-emphasised there when switching would change nothing on screen: that is, when both modes render every detail field identically and either they render the whole tree identically or the terminal is too short to leave the tree a single row. A call the queue no longer holds carries novhint at all -- that panel is the noticeThis call is no longer in the queueover a loneEsc for the queue. The whole tree is compared, so a difference confined to tree lines currently scrolled off screen still counts as a change. On the screen's five text-entry steps (select-call,input-fields,raw-target,raw-value,raw-calldata) a typedvgoes into the field being edited rather than being consumed - Escape / Backspace -- on the Payload Queue screen's call-detail step, return to the queue listing. The call detail owns the key, so it never exits the CLI here
- Up / down arrows -- on the Payload Queue screen's call-detail step, scroll the argument tree instead of moving a row cursor
- r -- retry what failed, on an error panel that offers it
TUI Screens
| Screen | Shows | |---|---| | Entry | Vault metadata and command palette | | Overview | APY, TVL, allocation breakdown table | | My Position | Share balance, USD value, all pending/claimable requests | | Deposit | Deposit queue table with inline deposit/cancel actions | | Redeem | Redeem queue table with inline redeem actions | | Payload Queue | One screen over the queue's lifecycle: list (reorderable), add a catalogued call, add a raw call, queue the oracle report, remove, clear, submit, export | | Rebalance | Four-step wizard: source, destination, asset, amount, then a confirmation that appends the encoded calls to the payload queue -- nothing executes from here |
The listing's columns, and the call detail behind them
The Payload Queue screen's listing step renders four columns, #, Call, Status and Chain.
#is the item's 0-based position in the queue's current order, which is exactly whatbatch remove --item <position>takes. The item's full id is not in the listing; it is on the call-detail screen, for thebatch remove --item <id>form.Callis one line describing the call, cut to the column with a trailing.... An item with no resolved target reads(no target), checked before anything else. Otherwise it is the backend's interpolated intent when that intent has parameters to fill in, then(cannot decode inner call)with the subvault's own address when a subvault call's inner call did not decode, then the decodedContract(0x123...abc).function(...)signature, then the target address followed by the 4-byte selector, or the address alone when the item carries no calldata. For asubvault_callthe line describes the wrapped inner call, not theSubvault.callwrapper.Statuscarries one of three marks, never a blank cell. For asubvault_callthat carries metadata it is read from the warnings the sharedgetCallBatchItemWarningsreports for it:!when it reports at least one (a missing Merkle proof, or an inner call that did not decode),✓when it reports none. Every other row is–, "not inspected": the kind and the metadata are both checked before any warning is read, so adirect,rebalance,swapororacle_reportrow is–, and so is asubvault_callwhosemetadatais missing -- its Merkle proof was never reported either way. Read the three marks narrowly --!means "this subvault call has a warning", not "this item has a problem",✓means "this subvault call reported no warning", not "this item is fine", and–means "nothing was inspected", not "this item is fine". The warning text itself is on the call-detail screen.Chainis the chain the item executes on, named from the item's ownchain_id: one of the nine networks the CLI knows by name, and the bare numeric id for anything else. The cell is 8 characters wide, so a name or an id longer than that is cut with a trailing...-- which no known network's name is, but a long unknown chain id can be.batch listprints the same value in full.
Press Enter on a row to open its call detail. It shows the resolved call's signature, or the same one-line description the Call column shows when it has none, then the decoded argument tree, then the queued item's own Id, Target, Value and Calldata, and the warning lines. A subvault_call whose wrapper arguments.value parses to a non-zero amount carries one extra Forwards line for it, because the wrapper's own Value is structurally 0 while the inner call it names is the payable one. Target and Calldata follow the value mode -- shortened address and 4-byte selector in formatted mode; in raw, the whole transaction.to (cut to the value column only if it exceeds it, which a 42-character address never does) and as much of the calldata as the value column holds, cut with a trailing ... -- while Id, Value and Forwards are the same in both. v switches that mode for the tree and for those two fields at once, the up and down arrows scroll the tree, and Esc returns to the listing.
batch list's non-interactive output is seven columns, id | kind | target | value | intent | warning | chain, one [ROW] per item, with the same INVALID token. chain was appended last, so a consumer reading the first six fields positionally keeps working and one that counts fields must expect seven. It carries the same value the TUI's Chain column shows -- the item's own chain_id as a known network's name or the bare numeric id -- but printed in full: only the TUI cuts it to its column. Its warning column reads the same shared getCallBatchItemWarnings the TUI's ! reads, so both surfaces flag the same items: a subvault_call whose metadata reports a missing Merkle proof or an inner call that did not decode prints INVALID, and every other item -- a subvault_call carrying no metadata included -- prints an empty cell. The TUI's – never appears here: this column is INVALID or empty.
On the Payload Queue screen's listing step, reordering with K/J calls the same reorder endpoint a curator would otherwise have no way to reach non-interactively; a rejected move falls back to the server's own ordering. On the Rebalance screen, rows that cannot be selected are shown dimmed rather than hidden, with the reason stated under the table. On the source step nothing is pinned yet, so the only reason a source row is dimmed is the API calling its route invalid -- a source on any chain the API lists can be picked, and picking it is what fixes the chain for the rest of the wizard. From the destination step on, a row off that chain is dimmed too, with not on the chain this rebalance targets, checked before the API's own verdict. --chain is not read here: the interactive chain always comes from the source row you select. The confirmation step lists every encoded call and any warnings before appending them to the queue.
Non-Interactive Mode
Activate non-interactive mode by passing --non-interactive or by piping stdout (the CLI auto-detects non-TTY environments).
Each command below shows the full argument list:
# View vault overview
mellow-cli overview --vault earnETH --non-interactive
# View your position
mellow-cli my-position --vault earnETH --non-interactive
# Deposit 1.5 WETH into a queue
mellow-cli deposit --vault earnETH --queue WETH --amount 1.5 --non-interactive
# Cancel a pending deposit
mellow-cli deposit cancel --vault earnETH --queue WETH --non-interactive
# Claim processed deposit shares
mellow-cli deposit claim --vault earnETH --queue WETH --non-interactive
# Redeem 1.5 shares into a withdrawal queue
mellow-cli redeem --vault earnETH --queue WETH --amount 1.5 --non-interactive
# Claim processed redemption assets
mellow-cli redeem claim --vault earnETH --queue WETH --non-interactivePayload queue (curator)
Every batch operation except rebalance-list and oracle-report --dry-run needs a curator API token (see Curator API token). Both exceptions hold among non-interactive invocations, which is every invocation in this section. The curation endpoints are staging-only for now, so MELLOW_API_BASE_URL must be set too.
export MELLOW_API_BASE_URL=https://api.stage.mellow.finance
export MELLOW_API_TOKEN=<your-token>
# Inspect the queue -- a bare `batch` does the same as `batch list`
mellow-cli batch --vault strETH --non-interactive
# Append a catalogued operation, discovered one flag at a time:
mellow-cli batch add-call --vault strETH --non-interactive
# -> lists the subvault indexes on the chain this append targets
mellow-cli batch add-call --vault strETH --subvault 0 --non-interactive
# -> lists that subvault's catalogued operations
mellow-cli batch add-call --vault strETH --subvault 0 --call-index 3 --non-interactive
# -> lists the operation's declared inputs
mellow-cli batch add-call --vault strETH --subvault 0 --call-index 3 \
--input 1000 --input 0xabc --non-interactive
# -> supplies every input, so this one appends the item
# Append a hand-written call the catalogue does not cover
mellow-cli batch add-raw --vault strETH --to 0xTarget --data 0xdeadbeef --non-interactive
# Append on a chain other than the vault's own, while the queue is still empty
mellow-cli batch add-raw --vault strETH --chain base \
--to 0xTarget --data 0xdeadbeef --non-interactive
# Remove one item, by id or by its position in the listing
mellow-cli batch remove --vault strETH --item 0 --non-interactive
# Remove everything
mellow-cli batch clear --vault strETH --non-interactive
# Submit the whole queue through the account the backend selects
mellow-cli batch submit --vault strETH --non-interactive
# Or write it as a Safe Transaction Builder document instead of submitting
mellow-cli batch export --vault strETH --file queue.json --non-interactive
# Print the candidate oracle report without queueing it, needing no API token
mellow-cli batch oracle-report --vault strETH --dry-run
# Queue the same report as a payload-queue item
mellow-cli batch oracle-report --vault strETH --non-interactivebatch add-call's first three invocations above only list a choice and change nothing; only the last, which supplies every declared input, appends an item. batch submit picks the executing account itself, from whichever accounts the backend confirms hold the roles the queue needs -- --via-safe, --via-mellow-account and --signer calldata are refused here, since the queue's own routing replaces them (see Curator operations). The backend derives roles only for the items it can, so a queue mixing a catalogued call with a hand-written batch add-raw item still yields an executing account; batch submit names the kinds no roles were derived for before it routes the batch. --via-safe and --via-mellow-account are refused on every other batch invocation as well, since no batch path uses the routing they select; --signer calldata is refused only on batch submit, batch rebalance and batch oracle-report, so it still browses the queue with no wallet.
batch submit reads the queue before it connects a wallet, so a missing API token, an empty queue, an unreachable backend or a queue holding items on two different chains is reported first, with no wallet involved. A Ledger unlock or a WalletConnect scan is only asked for once the queue has been read and the chain its items execute on is known.
That chain is the one every queued item carries -- --chain is refused here -- and it is what the submit runs against: the wallet is connected on it, each transaction is stamped with it, the Safe Transaction Service coverage check is made against it, and the archived record names it. --signer ledger and --signer walletconnect are refused when it is not the vault's home chain (see Curator operations), and --fork-url is refused whenever it is not the home chain, since a fork stands in for the home chain: fork the queue's chain instead, or drop the flag.
Reordering the queue before submission is interactive-only (see TUI Mode): there is no --non-interactive equivalent, since reordering is a review action a curator does by eye against the live listing.
Rebalance (queue producer)
batch rebalance and batch rebalance-list are two batch subcommands, not a separate command. batch rebalance-list reads only the unauthenticated options endpoint and needs no API token; batch rebalance appends to the queue and needs one, same as every other write subcommand.
export MELLOW_API_BASE_URL=https://api.stage.mellow.finance
# List everything selectable, before choosing anything -- no API token needed
mellow-cli batch rebalance-list --vault strETH --non-interactive
# Narrow the listing to one source, then to a pair (which adds the movable assets)
mellow-cli batch rebalance-list --vault strETH --source subvault:0 --non-interactive
mellow-cli batch rebalance-list --vault strETH \
--source subvault:0 --destination vault --non-interactive
# Queue a pull of 0.5 WETH from Subvault 0 back into the vault
mellow-cli batch rebalance --vault strETH --api-token <your-token> \
--source subvault:0 --destination vault \
--asset WETH --amount 0.5 --non-interactive
# Queue a push, addressing the subvault directly and in raw units
mellow-cli batch rebalance --vault strETH --api-token <your-token> \
--source vault --destination 0x90c983DC732e65DB6177638f0125914787b8Cb78 \
--asset WETH --amount wei:100000000000000000 --non-interactive
# Queue a move of the whole source-side balance
mellow-cli batch rebalance --vault strETH --api-token <your-token> \
--source "SwapModule (Subvault 1)" --destination subvault:1 \
--asset wstETH --amount max --non-interactive
# Queue a move on a chain other than the vault's own -- both sides must live there,
# and the vault option itself exists only on the home chain
mellow-cli batch rebalance --vault strETH --api-token <your-token> --chain plasma \
--source subvault:0 --destination subvault:1 \
--asset WETH --amount 0.5 --non-interactive
# Review, then execute, what batch rebalance queued
mellow-cli batch list --vault strETH --non-interactive
mellow-cli batch submit --vault strETH --api-token <your-token> --non-interactivebatch rebalance only appends calls to the payload queue: it never signs or submits anything itself. Run batch submit (or batch export on a chain with no Safe Transaction Service, see Curator operations) to actually execute what it queued.
Output Tag Schema
Non-interactive output is line-oriented and machine-parseable. Each line is prefixed with a tag indicating its category:
| Tag | Stream | Description | Example |
|---|---|---|---|
| [INFO] | stdout | Progress or status message | [INFO] Resolving vault earnETH... |
| [TX] | stdout | Transaction hash (one per submitted tx) | [TX] 0xabc123... |
| [OK] | stdout | Success confirmation | [OK] Deposit completed |
| [ERROR] | stderr | Error with actionable detail | [ERROR] Insufficient balance: you have 0.5 WETH but 1.0 WETH is required |
| [DATA] | stdout | Key-value pair | [DATA] apy: 4.52% |
| [TABLE] | stdout | Table header (pipe-separated columns) | [TABLE] Asset \| Address \| Pending \| Claimable |
| [ROW] | stdout | Table data row (pipe-separated values) | [ROW] WETH \| 0x000...000 \| 1.500 \| 0.000 |
Every line is exactly one line: multi-line text from the API or from a viem error is collapsed to a single space, so no consumer ever sees an untagged continuation line.
Note:
[TX]is part of the tag schema but no command emits it today -- every write command reports its hash as[INFO] Transaction hash: 0x…. Parse that line, as the example below does.
Parsing example (bash):
# Extract the transaction hash
mellow-cli deposit --vault earnETH --queue WETH --amount 1.0 --non-interactive \
| grep '^\[INFO\] Transaction hash:' \
| cut -d' ' -f4batch rebalance-list output
batch rebalance-list prints one [TABLE] per section and closes with [OK], so a consumer can
tell a complete listing from a truncated one. Sources and destinations carry
label | type | address | chain | valid_route | invalid_reason; the assets table, printed only
when both sides are given, carries symbol | address | decimals | balance. A cell nothing fills
-- a null invalid_reason, symbol, decimals or balance -- is empty, so a row ending in an empty
cell ends with a trailing | .
valid_route and invalid_reason are the API's own verdict, printed verbatim for every row
whatever chain it is on: the listing no longer overrides a side-chain row with a refusal of its
own. --chain does not filter the tables either -- it only decides which chain a --source or
--destination value is resolved against.
mellow-cli batch rebalance-list --vault strETH --non-interactive
[INFO] Rebalance options for strETH (0x277C6A642564A91ff78b008022D65683cEE5CCC5), keyed by home chain 1, with selections resolved on chain 1
[INFO] sources (21):
[TABLE] label | type | address | chain | valid_route | invalid_reason
[ROW] Vault | vault | 0x277C6A642564A91ff78b008022D65683cEE5CCC5 | ethereum (1) | yes |
[ROW] Subvault 0 | subvault | 0x90c983DC732e65DB6177638f0125914787b8Cb78 | ethereum (1) | yes |
[ROW] Subvault 1 | subvault | 0x893aa69FBAA1ee81B536f0FbE3A3453e86290080 | ethereum (1) | yes |
... 6 more ethereum subvault rows (Subvault 2 through Subvault 7) ...
[ROW] Subvault 0 | subvault | 0xbbF9400C09B0F649F3156989F1CCb9c016f943bb | plasma (9745) | yes |
... 11 more rows: the remaining side-chain subvaults and every swap module ...
[INFO] destinations (21):
[TABLE] label | type | address | chain | valid_route | invalid_reason
[ROW] Vault | vault | 0x277C6A642564A91ff78b008022D65683cEE5CCC5 | ethereum (1) | yes |
[ROW] Subvault 0 | subvault | 0x90c983DC732e65DB6177638f0125914787b8Cb78 | ethereum (1) | yes |
... 19 more destination rows, the side chains among them carrying the API's verdict too ...
[OK] Listed 21 sources, 21 destinationsNarrowing to a pair tightens valid_route and adds the movable assets:
mellow-cli batch rebalance-list --vault strETH --source subvault:0 --destination vault --non-interactive
[INFO] Rebalance options for strETH (0x277C6A642564A91ff78b008022D65683cEE5CCC5), keyed by home chain 1, with selections resolved on chain 1
[DATA] source: Subvault 0 [subvault:0] 0x90c983DC732e65DB6177638f0125914787b8Cb78 on ethereum
[DATA] destination: Vault [vault] 0x277C6A642564A91ff78b008022D65683cEE5CCC5 on ethereum
[INFO] sources (21):
[TABLE] label | type | address | chain | valid_route | invalid_reason
[ROW] Vault | vault | 0x277C6A642564A91ff78b008022D65683cEE5CCC5 | ethereum (1) | no | already selected as the destination
[ROW] Subvault 0 | subvault | 0x90c983DC732e65DB6177638f0125914787b8Cb78 | ethereum (1) | yes |
... 19 more source rows; the side-chain ones now read `no | on a different chain from the selected destination` ...
[INFO] destinations (21):
[TABLE] label | type | address | chain | valid_route | invalid_reason
[ROW] Vault | vault | 0x277C6A642564A91ff78b008022D65683cEE5CCC5 | ethereum (1) | yes |
[ROW] Subvault 0 | subvault | 0x90c983DC732e65DB6177638f0125914787b8Cb78 | ethereum (1) | no | already selected as the source
... 19 more destination rows ...
[INFO] assets (7):
[TABLE] symbol | address | decimals | balance
[ROW] DVstETH | 0x5E362eb2c0706Bd1d134689eC75176018385430B | 18 | 159.8703003
[ROW] wstETH | 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 | 18 | 69.4888857
[ROW] USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | 6 | 0
[ROW] WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 | 18 | 0.0978674
... 3 more asset rows ...
[OK] Listed 21 sources, 21 destinations, 7 assetsEvery narrowing tightens valid_route: with no selection it reflects permissions only, with one
side chosen it also reflects chain match and same-address exclusion. So with nothing selected a
side-chain subvault is reported yes -- there is nothing yet for its chain to mismatch -- and
once the other side is chosen the rows off that side's chain turn no with the API's own
on a different chain from the selected source/destination. Reading the tables means reading
what the API allows; what you may then select on a given chain is what --chain decides.
Both narrowing flags are resolved against the chain --chain names, else the vault's home chain:
--source subvault:0 on a vault with a Subvault 0 on three chains picks the one on that chain,
and a value resolving only to an option off it fails the listing with the same error
batch rebalance gives, --chain <name|id> remedy included.
batch rebalance output
A run against a mainnet fork, queueing a pull of 0.5 WETH from Subvault 0 into the vault. Nothing is signed and nothing executes -- the calls only land in the payload queue:
[INFO] Resolving rebalance source "subvault:0"…
[INFO] Source: Subvault 0 [subvault:0] 0x90c983DC732e65DB6177638f0125914787b8Cb78 on ethereum
[INFO] Resolving rebalance destination "vault"…
[INFO] Destination: Vault [vault] 0x277C6A642564A91ff78b008022D65683cEE5CCC5 on ethereum
[INFO] Resolving asset "WETH"…
[INFO] Asset: WETH (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), amount: 0.5 WETH
[INFO] Encoding rebalance…
[INFO] Rebalance encoded as 1 call(s):
[INFO] call 1/1: pullAssets: Subvault 0 → Vault (WETH, 0.5)
[INFO] Appending 1 rebalance call(s) to the payload queue…
[OK] Queued 1 rebalance call(s) — 0.5 WETH from Subvault 0 to Vault. Nothing has executed: review them with `batch list` and execute them with `batch submit`A non-fatal [INFO] warning line can appear between the call list and the append, and it never blocks queueing -- the chain state can move between the encode and whenever the queue is eventually submitted:
[INFO] Warning: amount exceeds current source balanceA rebalance is frequently several calls, and they are appended to the queue one at a time. A failure part-way is therefore partial, and the error says exactly what landed and what to do about it:
[ERROR] Rebalance append failed at call 3/4: call(s) 1-2 of 4 are queued as item-a1, item-a2, call(s) 3-4 of 4 are not — review them with `batch list` and drop one with `batch remove --item <id>` before retrying. Cause: …A retry does not resume where the failure left off: batch rebalance re-encodes the whole move from scratch and appends every call again, so retrying as-is would duplicate the calls that already queued. Remove those first with batch remove --item <id>, then either retry batch rebalance or finish the move by hand with batch add-raw.
Curator operations
batch is not an LP feature: every subcommand acts on a vault's own payload queue, and only a curator holding the vault's roles can eventually execute what accumulates in it. batch rebalance queues one class of these operations — moving assets between the vault, its subvaults and their swap modules — but it does not execute anything itself, and every other write subcommand (add-call, add-raw, oracle-report, remove, clear, submit, export) is just as much a curator operation.
Executing the queue needs no credential beyond the API token. batch submit asks the curation backend which accounts hold the roles the queued calls need, and routes the submission through one of them itself — there is no --via-safe or --via-mellow-account flag to pass, and the CLI holds no Safe API key because it never talks to the Safe Transaction Service directly (the curation backend proxies it). If no known account can execute the queue, batch submit says so and names the missing roles rather than guessing. That is not the only thing to watch for: the backend derives roles only for the items it can, and picks the executing account from those alone, so a queue also holding items it derived no roles for still gets an account -- batch submit names those kinds before routing, and the submit either lands or reverts as a whole.
Roles required, by call kind — read from the backend, not computed by the CLI:
| Call kind | Role required | Held on |
|---|---|---|
| Vault-level pull (pullAssets) | PULL_LIQUIDITY_ROLE | The vault |
| Vault-level push (pushAssets) | PUSH_LIQUIDITY_ROLE | The vault |
| Module-level (swap modules) | the subvault verifier's CALLER_ROLE | The vault the verifier points at |
Uncovered chains. Of the nine chains the CLI knows, Holesky, Hoodi and Mezo have no Safe Transaction Service, so batch submit refuses to route a submission through a Safe there, naming the chain before any transaction is signed. The chain checked is the one the queued items execute on, not the vault's home chain, so a queue built on an uncovered side chain is refused even from a covered home chain. batch export writes the same queue as a Safe Transaction Builder document instead, and that works on every chain the CLI knows, uncovered ones included — load the file into a signing UI to execute it there.
Side chains, and what actually works on one. A vault's subvaults may live on several chains, and the API lists all of them. A rebalance on a chain other than the vault's home chain goes through end to end only when every one of these holds:
- The chain is one of the nine the CLI knows —
ethereum,base,holesky,hoodi,monad,arbitrum,plasma,hyperevm,mezo(seesrc/helpers/web3/networks.ts). A chain the vault has an instance on but the CLI does not know —mantle, for instance, which is outside that list entirely — cannot be named by--chainat all, and an item filed under it cannot be submitted. - The move is same-chain. Source and destination must both be on the targeted chain. A cross-chain pair is not a "side chain not supported" case at all: the backend refuses it with 422
chain_mismatch, so no cross-chain transfer is encodable in any mode. - The backend supports the chain too, i.e. it can resolve the chain's RPC, Multicall3 and — for a swap-module leg — the subvault verifier configuration behind the permission reads. Otherwise the encode fails with the backend's own
unsupported_chain. - The submit has a route on that chain.
batch submitrefuses a side-chain queue with--signer ledgerand--signer walletconnect, which hold one chain's connection at a time; use--signer private_key, or, when the executing account is a Safe,batch exportand propose the document from the Safe app. There is no side-chain execution path for a Ledger or WalletConnect EOA, and none for a MellowAccount either: the export escape hatch only helps a Safe.
Options off the targeted chain are still shown (dimmed in the TUI from the destination step on, listed in error messages) so you can see that the subvault exists and what to pass to reach it — --chain <name|id>. batch rebalance-list refuses nothing on its own: it prints the API's verdict for every row, whatever chain it is on.
Staging only. The curation endpoints backing batch are not on production yet, so MELLOW_API_BASE_URL=https://api.stage.mellow.finance is required until they ship. Only core-vault-type vaults have rebalance topology at all; the legacy vault types return an unknown_vault error.
Amount semantics matter for batch rebalance. The API reports the source-side balance it observed, and the chain can move underneath it between the encode and whenever the queue is eventually submitted — hence the non-blocking balance warning. max resolves against the balance the API reported at encode time, not against a fresh read.
Cross-Platform Compatibility
| Platform | Minimum Version | |---|---| | Windows | 10+ | | macOS | 12+ (Monterey) | | Linux | Any modern distribution with Node.js >= 20 |
The CLI runs anywhere Node.js >= 20 is available. TUI mode requires an interactive terminal with ANSI color support. Non-interactive mode works in any environment including CI/CD pipelines, Docker containers, and headless servers.
For Ledger hardware wallet signing on any platform (or simulator-based development without a physical device), see README.md.
License
MIT
