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; operators also need to know what rflow did spend. Spend tracking is a durable, receipt-derived ledger (rflow.native_spend) written at every send's terminal settle, answering: how much native gas did rflow burn — by workflow, by relayer, by network — how much native value moved, and how much gas was lost to reverted transactions?

It ships as three surfaces over one ledger, so the numbers can never diverge:

  • a CLIrflow spend (grouped summary) and rflow spend export (raw entries, CSV/JSON),
  • a read-only JSON APIGET /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) — 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 discipline — honest limits included:

  • Best-effort, never blocking. The ledger is operator accounting, not part of the money path: 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, so there is nothing to record. 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 in the ledger under the synthetic workflow name relayers.topup (attributed to the funding relayer), so relayer-funding gas shows up in the same summaries — 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, spend history is an accounting record that 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:

budgets:
  solver-gas:
    window: 24h
    max_gas_spend: 1 ether        # wei literal, like max_native_value
    scope:
      relayers: [solver]

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 pre-send estimates are unreliable (nodes over- and under-estimate, prices move between estimate and inclusion, L2 fee components are invisible pre-send). Charging estimates would either falsely refuse sends or leave phantom spend in the window. The cap is therefore 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 in the ledger. 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.