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. Your logic decides what to trade; rflow owns moving the money. 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
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 โ€” 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 โ€” 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