MINARA

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_tokens without confirm: true returns 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 chains
  • buy_token / sell_token — explicit buy / sell side
  • transfer_token — wallet-to-address transfer

Perpetual futures

  • open_perps_position / close_perps_position
  • minara_perps_wallet_sweep — move balance between sub-accounts
  • minara_perps_wallet_transfer — transfer perps balance
  • minara_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 strategy
  • minara_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 confirm check lenient? In practice confirm is accepted as boolean true, string "true", "1", "yes", or "on". LLM tool-call protocols sometimes stringify booleans, and a strict === true check 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=1 into .zshrc and 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 requestLayer 3swap_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 terminalLayer 4 — gate inspects the shell command, prompts YELLOW
Patch a file so it ends up containing minara swapLayer 4 — gate scans the post-apply content, prompts YELLOW
Deploy a strategy via minara_ss_deployLayer 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=1 in an interactive REPL session. Don't put it in .zshrc or .envrc.
  • ❌ Don't let the LLM reply "confirm" on your behalf. The confirmation is your judgment, not the agent's politeness.

On this page