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

Spend tracking — what rflow actually paid

Budgets cap what a send may spend; spend tracking records what rflow did spend. It is a durable, receipt-derived ledger (rflow.native_spend) written at every send's terminal settle: native gas burned (by workflow, relayer, network), native value moved, and gas lost to reverted transactions.

Three surfaces over one ledger, so the numbers can never diverge:

  • a CLI: rflow spend (grouped summary) and rflow spend export (raw entries, CSV/JSON),
  • a read-only JSON API: GET /api/spend (auth-gated like every /api/* route),
  • a Spend view in the history explorer (GET / → Spend).

And one enforcement hook: a budget's max_gas_spend cap refuses further sends once the recorded gas spend in its window reaches the cap.

What is captured

When a send reaches a terminal state where a transaction landed on-chain, rflow reads the receipt (the relayer's journaled receipt when it carries gas numbers, otherwise one eth_getTransactionReceipt over the read providers) and records:

columnmeaning
gas_usedThe receipt's gasUsed.
effective_gas_priceThe receipt's effectiveGasPrice (falls back to the legacy gasPrice on pre-EIP-1559 chains).
gas_spend_weigas_used × effective_gas_price, plus the receipt's l1Fee when present (the OP-stack L1 data fee Base/Optimism debit on top of the L2 execution fee); this is what the chain actually charged.
value_weiThe native value the send moved (from the journaled send value). Zero for reverted sends: a revert burns gas but transfers nothing.
revertedWhether the transaction reverted on-chain (its gas is the reverted loss subtotal).

Every row is attributed to the workflow, run, step, relayer, network and chain id of the send, plus the transaction hash and a recorded_at timestamp. The three query dimensions (workflow / relayer / network) are indexed.

Capture limits:

  • Best-effort, never blocking. A failed receipt fetch logs a warning and never fails the step: a gap in the ledger is possible; a broken send because of the ledger is not.
  • Idempotent. Rows are keyed UNIQUE (step_run_id, tx_hash) and inserted with ON CONFLICT DO NOTHING, so a crash-rerun of the same terminal settle never double-counts.
  • Only landed transactions. Confirmed sends and reverted sends (status FAILED with a receipt) are recorded; dropped / expired / replaced sends never landed. Sends with wait_for: none | submitted settle before a receipt exists: their spend is recorded only if a receipt is already available at settle time, so fire-and-forget sends can be under-counted.
  • Native currency only. ERC20 flows are budget territory (max_token_value); there is no fiat conversion.
  • Top-ups are recorded too. A rflow relayers topup send lands under the synthetic workflow name relayers.topup (attributed to the funding relayer), alongside its rflow.operator_audit row (rflow relayers history).

Retention

rflow.native_spend deliberately has no foreign key onto the run journal: like the operator audit trail, it survives retention pruning of the runs it describes. Pruning old runs never touches recorded spend.

Budget integration — max_gas_spend

A budget may declare a gas circuit breaker:

rflow_version: 1
name: solver
 
config:
  port: 3947
  db_connection: ${DATABASE_URL}
 
networks:
  - name: base
    chain_id: 8453
    rpc: ${BASE_RPC}
 
signer:
  raw:
    mnemonic: ${RAW_DANGEROUS_MNEMONIC}
 
relayers:
  solver:
    networks: [base]
 
budgets: 
  solver-gas: 
    window: 24h
    max_gas_spend: 1 ether        # wei literal, like max_native_value 
    scope: 
      relayers: [solver] 
 
workflows:
  rebalance:
    trigger:
      cron: { expression: "*/5 * * * *" }
    # sends in this workflow charge the budget (scope must match too)
    budgets: [solver-gas]
    steps:
      - id: rebalance
        send_transaction:
          network: base
          relayer: solver
          to: "0x000000000000000000000000000000000000dEaD"
          value: "${{ wei('0.1', 18) }}"

Before every in-scope send broadcasts, rflow sums the recorded gas spend inside the budget's scope and window; once that total has reached the cap the send fails budget_exceeded: same failure taxonomy, same before-any-broadcast guarantee as the value caps.

Recorded-spend semantics, on purpose. Unlike max_native_value this is not a reservation: actual gas is only known at the receipt, and charging unreliable pre-send estimates would either falsely refuse sends or leave phantom spend in the window. The cap is a circuit breaker over settled truth, not a precise limiter: sends already in flight when the cap is reached can overshoot it by their own gas before their receipts land. Size the cap with that headroom in mind.

CLI

rflow spend                                        # grouped summary: by workflow / relayer / network + totals
rflow spend --workflow treasury-sweep --since 30d  # filter any dimension + time range
rflow spend --relayer solver --network base --json # exact wei, machine-readable
rflow spend export --format csv                    # raw entries (stable columns), stdout
rflow spend export --format json --since 7d        # { "entries": [...] }

Human tables format amounts as the native unit; --json and export carry exact wei (decimal strings; uint256 does not fit JSON numbers). The CSV columns are a stable contract: recorded_at,workflow,relayer,network,chain_id,tx_hash,gas_used,effective_gas_price,gas_spend_wei,value_wei,reverted,run_id,step_run_id.

API

GET /api/spend accepts workflow, relayer, network, since (24h, 30d or RFC3339) and limit (entries page size, default 50, max 500) as query parameters and returns:

{
  "summary": {
    "by_workflow": [ { "key": "treasury-sweep", "tx_count": 12, "gas_spend_wei": "…", "value_wei": "…", "reverted_count": 0, "reverted_gas_wei": "0" } ],
    "by_relayer":  [ { "key": "solver", "…": "…" } ],
    "by_network":  [ { "key": "base", "…": "…" } ],
    "total":       { "tx_count": 12, "gas_spend_wei": "…", "value_wei": "…", "reverted_count": 0, "reverted_gas_wei": "0" }
  },
  "entries": [ { "recorded_at": "…", "workflow": "…", "relayer": "…", "network": "…", "chain_id": 8453, "tx_hash": "0x…", "gas_spend_wei": "…", "value_wei": "…", "reverted": false, "run_id": "…", "step_run_id": "…" } ]
}

The summary always aggregates the full filtered ledger; limit only pages the entries. The endpoint is gated by the same bearer auth as every /api/* route and served Cache-Control: no-store.

Explorer

The Spend view in the embedded explorer shows the same summary: KPI tiles (gas spend, value moved, reverted loss, send count), one table per dimension (clicking a row filters to it) and the newest settled sends with explorer links. Filters (workflow / relayer / network / time range) live in the URL hash like every other view.