Fund-moving confirm
Every tool that moves money requires a two-step preview→execute flow. Why it exists, what it covers, and the one exception.
You tell the agent: "swap 100 USDC to ETH." The agent doesn't just do it. First you see:
⏳ Awaiting confirmation — swap details (NOT executed yet):
Sell: 100 USDC (Ethereum)
Buy: ~0.0286 ETH (estimated, 0.5% slippage)
Route: Uniswap V3
Gas estimate: ~$1.20
Please confirm before execution.You read it, decide it matches what you meant, reply "confirm" — and only then does the agent broadcast the transaction.
Why doesn't the agent just do it? Because LLMs misread. "100 USDC" can become "1000 USDC", an address you typed can drift one character, the chain can quietly hop from Ethereum to Base. Once broadcast, none of that is reversible. So every money-moving tool is structurally a two-step: preview first, execute second.
The contract
Every tool handler that moves money gates its wire call on an
explicit confirm: true argument. With confirm absent or
falsy, the handler simulates and returns the preview. With
confirm: true, it broadcasts. This is enforced inside the
handler — at the function level, not the prompt level.
Why a hard contract instead of a polite prompt? Because LLMs can "forget" prompt-level rules under prompt injection, sleight-of-hand from a malicious skill, or a hostile tool result that the LLM treats as instruction. Making confirm part of the function signature means it's not a request — it's a precondition. No matter what the LLM "decides" to do, calling
swap_tokenswithoutconfirm: truereturns a preview, never a transaction.
The implementation lives in
shouldExecuteFundMovingCall.
Every new fund-moving tool is required by
CLAUDE.md §3b
to gate on it; reviewers reject PRs that don't.
Which tools this covers
Tools are marked isFundMoving: true in the tool registry. The
list, grouped by purpose:
Token trading
swap_tokens— DEX swap across chainsbuy_token/sell_token— explicit buy / sell sidetransfer_token— wallet-to-address transfer
Perpetual futures
open_perps_position/close_perps_positionminara_perps_wallet_sweep— move balance between sub-accountsminara_perps_wallet_transfer— transfer perps balanceminara_wallet_fund_perp— fund the perps account from spot (USDC deposit)
Autopilot — turning autonomous trading on or off
minara_autopilot_enable/minara_autopilot_disable
The autopilot toggle is fund-moving because flipping it on is the operator authorizing the agent to trade on its own. Once enabled, individual autopilot trades inherit that authorization — they don't re-prompt for every trade.
n8n workflows
minara_workflow_activate/minara_workflow_deactivate
Same idea: activating a workflow is the moment you say "this automation is approved to run on its own."
Strategy studio — your own strategy
minara_ss_deploy/minara_workflow_studio_deploy
Deploying is a one-time authorization — see Strategy deployment is a
one-time authorization
below. (minara_ss_marketplace_subscribe just clones a strategy into your
list. It is CONFIRM_ONCE, not fund-moving, and does not start trading; the
live-start actions are below.)
Running another creator's strategy live
minara_top_strategies_apply_start— run a featured Top Strategies strategyminara_ss_marketplace_sub_start/_sub_stop— start / stop a subscribed marketplace strategy
These start or stop autonomous trading with the user's funds and have no
author-time approval moment, so they run the two-step confirm just like
autopilot (apply_start / sub_start are MANUAL_ONLY; sub_stop is
ALWAYS_CONFIRM).
Direct exchange placement
- All
hyperliquid_*place / cancel / withdraw endpoints
If you're adding a new fund-moving tool, mark it
isFundMoving: true. Code review will block the PR if you don't.
The two-step flow in practice
A turn-by-turn walkthrough, from the agent's call to your reply:
Turn 1
LLM: swap_tokens({
sell_token: "USDC",
sell_amount: 100,
buy_token: "ETH",
chain: "ethereum"
})
→ handler runs shouldExecuteFundMovingCall(args) → false
(because confirm is absent)
→ handler simulates the trade, builds a preview, returns:
{
"confirmed": false,
"preview": {
"sell": "100 USDC on ethereum",
"buy": "~0.0286 ETH (estimated, 0.5% slippage)",
"route": "Uniswap V3",
"gas_estimate_usd": 1.20,
"expires_at": "2026-05-14T18:30:00Z"
},
"next_step": "⏳ Awaiting confirmation — show the user
the details and ask whether to proceed. After they
confirm, call this tool again with the same args
plus confirm: true."
}
LLM translates the preview into a natural-language summary
and asks: "Confirm this swap?"
Turn 2 (you reply "confirm")
LLM: swap_tokens({
sell_token: "USDC",
sell_amount: 100,
buy_token: "ETH",
chain: "ethereum",
confirm: true
})
→ shouldExecuteFundMovingCall(args) → true
→ handler signs + broadcasts the transaction
→ returns {tx_hash: "0x...", executed: true}Why is the
confirmcheck lenient? In practiceconfirmis accepted as booleantrue, string"true","1","yes", or"on". LLM tool-call protocols sometimes stringify booleans, and a strict=== truecheck would force the user to confirm twice. We'd rather be permissive on the value type than break a confirmed flow. The escape hatch is intentional — but it only matters if the LLM has already chosen to pass a truthy value, which only happens after the human said yes.
Strategy deployment is a one-time authorization
minara_ss_deploy works differently. It runs a single two-step
confirm at deploy time — you see the full strategy config (sub-
account, execution mode, symbol allowlist, leverage, allocation,
drawdown caps) and decide once.
After that, the strategy's lifecycle methods —
_strategy_start, _strategy_stop,
_strategy_deployment_config_update — do not go
through shouldExecuteFundMovingCall again. They're scheduling
operations on an already-authorized strategy, not new money
movements.
Running another creator's strategy is different: there is no author-time
authorization, so _marketplace_sub_start / _marketplace_sub_stop (and the
Top Strategies minara_top_strategies_apply_start) DO run the two-step
confirm, like enabling autopilot.
Why this exception? If every algorithmic trade required a human confirm, the strategy is just a manual trading script. The point of strategy-studio is to delegate within constraints you set upfront. Deploy is the moment you authorize the envelope; after that, the strategy is bounded by drawdown caps, allocation limits, and the symbol allowlist baked into its deployment config, not by a per-trade prompt.
This exception applies only to strategy-studio. Don't try to mimic the pattern elsewhere — the rest of the tool surface is strict two-step.
MINARA_SKIP_FUND_CONFIRM — the operator-level bypass
There is one escape hatch for non-interactive contexts. The env
var MINARA_SKIP_FUND_CONFIRM=1 makes
shouldExecuteFundMovingCall always return true. When to use
it, when never to:
Reasonable uses (a handful of legitimate cases):
- Backtests. The caller is a Python harness; there's no human to read the preview, and the run is sandboxed against historical data anyway.
- Workflow engine execution. The workflow was already approved at activate time. The execution loop is dispatching on an authorized workflow, not asking the user to re-confirm each step.
- CI smoke tests. The whole point is to drive the fund-moving path and verify the wire call.
Never use it for:
- Interactive REPL sessions. This bypasses the last line of defense protecting you.
- "It's annoying to confirm every time" on a production box. That's turning the safety off.
- Shell rc files. Don't drop
export MINARA_SKIP_FUND_CONFIRM=1into.zshrcand forget about it. The next time you start the agent it's running unguarded.
The full env-var reference is at Environment variables → MINARA_SKIP_FUND_CONFIRM.
Layer 3 vs Layer 4 — who catches what
The most common confusion: when is the gate Layer 3 (this page) versus Layer 4 (the script-risk gate)? Quick reference:
| You ask the agent to… | Caught by |
|---|---|
| "Swap 100 USDC to ETH" — direct trade request | Layer 3 — swap_tokens handler enforces preview → confirm |
Run a Python script containing subprocess.run(["minara","swap",…]) | Layer 4 — script-risk gate sees fund-moving CLI, prompts YELLOW |
Run cast send 0x… 1ether from terminal | Layer 4 — gate inspects the shell command, prompts YELLOW |
Patch a file so it ends up containing minara swap | Layer 4 — gate scans the post-apply content, prompts YELLOW |
Deploy a strategy via minara_ss_deploy | Layer 3 — one-time confirm at deploy; lifecycle methods don't re-prompt |
Short version: Layer 3 catches direct fund-moving tool calls from the agent. Layer 4 catches fund-moving code inside the scripts and commands the agent runs. They cover different parts of the same threat and work in parallel.
Operator checklist
- ✅ When you see a preview, actually read it. Verify the destination, the chain, the amount, and the slippage match what you asked for. The two-step exists so you can catch your own mistakes too.
- ✅ When deploying a strategy, audit the deployment config carefully. It's the only time you'll be asked.
- ✅ When activating a workflow, read the workflow definition end-to-end before clicking yes. Subsequent execution of that workflow won't re-ask.
- ❌ Don't set
MINARA_SKIP_FUND_CONFIRM=1in an interactive REPL session. Don't put it in.zshrcor.envrc. - ❌ Don't let the LLM reply "confirm" on your behalf. The confirmation is your judgment, not the agent's politeness.