Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Command trade prep

Template: command-trade-prep ยท category: relayers ยท risk: money_moving

A deposit into a watched address fires a project-owned node script that quotes a trade (amount, min_out, deadline, freshness window); rflow then signs, simulates, rechecks and sends it. The quote script never signs or sends: it has no private key and no RPC handle. Runnable sibling: examples/command-trade-prep.

When to use it

  • keeper/trading flows where sizing lives in your own code or a pricing API
  • any event-driven send whose parameters must be computed, not hardcoded
  • the money-moving counterpart of command-decision

Generate it

rflow new --template command-trade-prep
# or into an existing project (needs a signer; adds the `trader` relayer):
rflow add workflow command-trade-prep

Inputs

keytypedefault
project_namestringcommand-trade-prep
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
token_name / token_address / token_decimalscontract / address / intUSDC / USDC mainnet / 6
watchedaddresszero placeholder (replace it)
trade_targetaddresszero placeholder (replace it)
min_deposittoken_amount100 (whole tokens)
confirmationsint12 (blocks a deposit must be deep before it counts)
quote_ttlint120 seconds
gas_max_coststring0.02 ether

Generated YAML (the shape)

# recipe: partial
rflow_version: 1
name: command-trade-prep
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
    confirmations: 12
 
# DEV ONLY raw mnemonic (from .env) - swap in a production signer
signer:
  raw:
    mnemonic: ${RAW_DANGEROUS_MNEMONIC}
 
relayers:
  trader:
    networks: [ethereum]
 
contracts:
  USDC:
    abi: ./abis/erc20.json
    addresses:
      ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 
constants:
  watched: "0x0000000000000000000000000000000000000000"       # replace the placeholder
  trade_target: "0x0000000000000000000000000000000000000000"  # replace the placeholder
 
workflows:
  command-trade-prep:
    trigger: 
      event:      # deposits >= min_deposit INTO the watched address 
        contract: USDC
        name: Transfer
        where: "${{ trigger.args.value >= wei('100', 6) and lower(trigger.args.to) == lower(constants.watched) }}"
        confirmations: 12   # a reorged deposit can never trigger a trade 
    steps: 
      - id: gate   # rflow reads the relayer's balance (the command never does) #
        read: { function: "balanceOf(address)", args: ["${{ relayers.trader.address }}"] } 
      - id: quote  # your script quotes: amount, min_out, deadline, valid_until 
        command: { run: "node ./scripts/quote.js", timeout: 10s, output: json, ... } 
      - id: trade  # rflow signs + simulates + rechecks + sends 
        if: "${{ steps.quote.output.should_send == true }}"
        send_transaction: 
          function: "transfer(address,uint256)"
          args: ["${{ constants.trade_target }}", "${{ steps.quote.output.amount }}"] 
          simulate: true
          assert_sim: ["${{ sim.ok }}"] 
          gas: { limit_from_simulation: true, multiplier: 1.2, max_cost: "0.02 ether" } 
          recheck: "${{ now() < steps.quote.output.valid_until }}"
          wait_for: confirmed
    on_failure: dead_letter

The 'trade' is modelled as an ERC20 transfer so the template is self-contained; in a real deployment the same shape drives e.g. Router.swapExactTokensForTokens(...) with the quoted min_out/deadline/path as args.

Required env vars

DATABASE_URL, the RPC env var, RAW_DANGEROUS_MNEMONIC. rflow new fills .env with a freshly generated DEV-ONLY mnemonic. Swap in a production signer before real funds ride on this config. node must be on PATH.

Safety defaults (all generated)

  • pre-flight simulation + assert_sim: ["${{ sim.ok }}"]
  • gas cap: limit_from_simulation + max_cost, so a fat-fingered price never drains gas
  • recheck: now() < valid_until โ€” a run parked/restarted past the quote's freshness window is dropped, never broadcast stale
  • the trigger waits confirmations: 12 before firing, so a reorg cannot orphan the deposit AFTER the trade is sent
  • confirmations: 12 on the network, wait_for: confirmed
  • on_failure: dead_letter

This path is fully automated (no approval gate; that would defeat a trading keeper). If your flow can wait for a human, add an approval: block to the trade step exactly as treasury-sweep-approval does.

Run it locally

docker compose up -d
rflow validate
# rehearse without touching a chain (`to` in the fixture is your watched address):
rflow test command-trade-prep --fixture fixtures/deposit-event.json
rflow start

Production checklist

  1. replace the watched and trade_target zero placeholders
  2. replace the raw dev mnemonic with a production signer
  3. make quote.js call your real pricing source and enforce YOUR slippage
  4. size gas_max_cost and min_deposit for the network you deploy on
  5. add a spend budget / rate limit around the workflow when budgets exist in your project, and consider concurrency: if quotes must not overlap

Common modifications

  • drive a router/DEX call instead of transfer (use the quoted min_out and deadline as args)
  • add an approval: gate for semi-automated desks
  • notify: on every executed trade for a human audit trail
  • tighten quote_ttl for fast markets; the recheck makes staleness a no-op