Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates notification fee/runner rendering, tightens simulation gas handling, and cleans up related configuration/docs. It fits into the aggregator/taskengine notification pipeline by moving more summary data derivation into local VM state, extending pricing support for multi-token cost lines, and aligning documentation/configuration with the new behavior.
Changes:
- Adds Runner/Cost rendering for Telegram/email, including multi-token totals, stablecoin shortcuts, and ERC20 price lookups via Moralis.
- Improves simulation fee data by assigning per-chain default gas prices and propagating gas fields into simulated execution logs.
- Cleans up ancillary config/docs by removing dead backup config, updating example YAML/comments, and standardizing
docs/changesentries.
Reviewed changes
Copilot reviewed 22 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/changes/README.md | Adds repo-specific guidance for change-doc naming/format. |
| docs/changes/20260502-notification-cost-line-and-runner.md | Documents the new notification Runner/Cost architecture and rollout. |
| docs/changes/20260416-uniswap-simulation-state-propagation.md | Renamed historical change doc to the date-based convention. |
| docs/changes/20260413-execution-status-redesign.md | Renamed historical change doc to the date-based convention. |
| core/taskengine/vm_runner_rest.go | Switches SendGrid runner fields to use Summary.Runner. |
| core/taskengine/vm_runner_eth_transfer_test.go | Adds simulation gas-field coverage for ETH transfer steps. |
| core/taskengine/vm_runner_eth_transfer.go | Populates simulated ETH-transfer gas/cost metadata. |
| core/taskengine/tenderly_client.go | Sends default gas price to Tenderly and hard-fails anomalous gas echoes. |
| core/taskengine/summarizer_format_test.go | Expands formatter/unit coverage for subject wrapping, runner, and cost lines. |
| core/taskengine/summarizer_format_telegram.go | Renders Runner/Cost metadata and updates Telegram subject code wrapping. |
| core/taskengine/summarizer_format_email.go | Adds/adjusts email Cost rendering and simulation placeholder behavior. |
| core/taskengine/summarizer_deterministic.go | Introduces runner/fee models and computes notification totals from VM state. |
| core/taskengine/summarizer_context_memory.go | Keeps IsSimulation aggregator-owned and populates local Runner/Fees. |
| core/taskengine/summarizer.go | Adds global fee/price-service wiring and direct summarizer construction. |
| core/taskengine/fee_estimator_test.go | Updates mock price service for ERC20 pricing support. |
| core/taskengine/fee_estimator.go | Extends PriceService and nil-guards native token symbol lookup. |
| core/taskengine/engine.go | Wires fee rates and price service into summary generation. |
| core/taskengine/blockchain_constants_test.go | Tests default gas prices and stablecoin lookup tables. |
| core/taskengine/blockchain_constants.go | Adds per-chain default gas prices and stablecoin metadata map. |
| core/services/moralis_service.go | Implements ERC20 USD pricing through Moralis. |
| core/config/config.go | Removes dead backup config fields and derives backup path from db_path. |
| config/aggregator.example.yaml | Updates backup-path comment and removes dead backup_dir example. |
| aggregator/task_engine.go | Stops using fallback pricing and logs nil-price-service degradation. |
| aggregator/rpc_server.go | Removes fallback price service and updates fee-estimate logging/path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+41
to
+58
| var globalFeeRates *config.FeeRatesConfig | ||
|
|
||
| // SetFeeRates sets the global fee rates config used by Summary.Fees population. | ||
| // Engine startup wires this from config.FeeRates. nil falls back to defaults. | ||
| func SetFeeRates(rates *config.FeeRatesConfig) { | ||
| globalFeeRates = rates | ||
| } | ||
|
|
||
| // globalPriceService is the chain price oracle used by Summary.Fees population | ||
| // to convert USD platform fees and value-fee legs into native-token amounts. | ||
| // Wired at engine startup via Engine.SetPriceService → SetPriceService. | ||
| var globalPriceService PriceService | ||
|
|
||
| // SetPriceService sets the global price service used by Summary.Fees population. | ||
| // nil disables USD/native conversion — Total entries that need a price will be | ||
| // emitted with empty USD amounts (formatter renders "$?" placeholder). | ||
| func SetPriceService(svc PriceService) { | ||
| globalPriceService = svc |
Comment on lines
+1542
to
+1552
| for _, c := range fees.Cogs { | ||
| if c == nil || c.Fee == nil { | ||
| continue | ||
| } | ||
| if w, ok := new(big.Int).SetString(c.Fee.Amount, 10); ok { | ||
| gasWei.Add(gasWei, w) | ||
| } | ||
| } | ||
| weiPerEth := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) | ||
| nativeEth := new(big.Float).Quo(new(big.Float).SetInt(gasWei), weiPerEth) | ||
| if fees.ExecutionFee != nil && fees.ExecutionFee.Amount != "" && nativePriceUSD != nil { |
Comment on lines
+1552
to
+1555
| if fees.ExecutionFee != nil && fees.ExecutionFee.Amount != "" && nativePriceUSD != nil { | ||
| if usd, ok := new(big.Float).SetString(fees.ExecutionFee.Amount); ok { | ||
| nativeEth.Add(nativeEth, new(big.Float).Quo(usd, nativePriceUSD)) | ||
| } |
Add per-step gas (gasUsed/gasPrice/totalGasCost) on the step digest and a top-level fees block (executionFee + cogs[] + valueFee) on the summarize request, mirroring the Execution proto's three-component fee model. The fees block is built via the same buildExecutionFee/buildCOGSFromSteps/ buildValueFee helpers the executor uses for Execution.Fee, so summarize fees match persisted execution fees exactly. NewContextMemorySummarizerFromAggregatorConfig now constructs the summarizer directly so it can pass c.FeeRates through. The public NewContextMemorySummarizer(baseURL, authToken) is unchanged for tests.
Tenderly simulations were previously sent with gas_price=0, so the echoed response had total_gas_cost=0, which buildCOGSFromSteps then skipped — and context-memory saw fees.cogs=[] on every simulate_task request. Per-step gasUsed/gasPrice/totalGasCost were also absent, violating the spec that on-chain steps always carry them. Changes: - Add DefaultGasPriceByChain (mainnet 5 gwei, Sepolia 0.5, Base 0.05, Base Sepolia 0.01) + GetDefaultGasPrice helper. - Send GetDefaultGasPrice(chainID) in Tenderly simulate requests. - Throw with diagnostic context when the response carries gas_used but zero/unparseable gas_price; drop the silent "only gas_used" branch. - Populate executionLog.GasUsed/GasPrice/TotalGasCost in the eth_transfer simulation branch (StandardGasCost × per-chain default).
Consume the new body.runner / body.fees fields the context-memory API now
returns (PRD: docs/changes/20260501-summary-runner-and-fees-sections.md):
- Decode `runner` and `fees` from /api/summarize and surface as
Summary.Runner / Summary.Fees.
- Telegram: render Runner address inside the Network/Time block and a
one-line Cost summary right below. Owner intentionally omitted on
Telegram (channel-space). Simulations show "⛽ (cost estimated at
deploy)" instead of fake-precision numbers, since sim gas prices are
conservative chain defaults rather than real network conditions.
- Email: SendGrid template's existing {{runner}} / {{eoaAddress}} now
source from Summary.Runner directly — single source of truth shared
with Telegram, no parallel VM-state derivation. analysisHtml gains a
Cost section with per-step gas bullets + value-fee line for deployed
runs; simulations get the same deploy-time placeholder.
Drops the dead VM-state runner-fallback chain (vm.task / aa_sender /
settings.runner) in vm_runner_rest.go in favor of reading s.Runner.
The Telegram formatter's simulation branch reads s.Workflow.IsSimulation, but Summary.Workflow was populated only when context-memory's response included a body.workflow block. Simulation responses don't, so the flag defaulted to false and the formatter took the deployed-run path — producing a malformed `Cost: + $0.02 platform fee` line when fee fields were partially present. Always populate Summary.Workflow.IsSimulation from vm.IsSimulation (the aggregator already knows whether it ran a simulation). The API response can still enrich workflow Name/Chain/ChainID/RunNumber, but the simulation flag is no longer dependent on response shape.
…anatory The h3 "Cost" heading above "⛽ (cost estimated at deploy)" was redundant visual noise — the placeholder line conveys the same information without the header.
…locally Runner and Fees data was being shipped to context-memory in /api/summarize requests and echoed back in responses, but the aggregator already has every input it needs (vm.task.SmartWalletAddress, vm.task.Owner, vm.ExecutionLogs, config.FeeRates) — context-memory was a pure passthrough adding only round-trip latency and API-shape complexity. This commit removes that round-trip: - Request: drop steps[].gasUsed/gasPrice/totalGasCost and the top-level fees block. Step digest reverts to its pre-Runner/Fees shape. - Response: stop decoding body.runner / body.fees. Those types are removed; ContextMemorySummarizer.feeRates field is removed. - Add buildRunnerFromVM and buildFeesFromVM helpers (alongside the existing builders in summarizer_deterministic.go) that read VM state directly. - Both ComposeSummary (deterministic path) and Summarize (context-memory path) populate Summary.Runner and Summary.Fees via these helpers. - Engine startup wires globalFeeRates via SetFeeRates(config.FeeRates) so both paths see the same configured rates. Net behavior is identical for notification rendering (formatters still read Summary.Runner and Summary.Fees); the data path is shorter and the context-memory API stays focused on natural-language summary fields.
Notifications now render the Cost as a single comma-separated line with the native unit first and USD parenthetical: ⛽ Cost: 0.000003 ETH ($0.01) ⛽ Cost: 0.01 ETH ($25.00), 1.2 USDC ($1.20) Tokens that can't be priced render as "$?" — never block the notification. For simulation runs the line collapses to the static placeholder "⛽ (cost estimated at deploy)" since simulated gas uses conservative chain defaults rather than real network conditions. Drops the per-step bullets, gas-units detail, and value-fee subtitle that the email Cost section used to render — breakdowns live on the dashboard, not in notifications. The chain price service (Moralis with $2,500 ETH fallback) is reused from the existing fee-estimation path. Engine startup wires both SetFeeRates and SetPriceService at package level so ComposeSummary (deterministic) and ContextMemorySummarizer.Summarize share one source of truth. V1 ships the native-token total leg only (gas + executionFee converted to ETH). The Total []*TokenTotal shape supports per-token value-fee legs for V2 once transfer extraction across loop iterations lands — the renderer needs no changes.
V2 of the multi-token Cost line. Walks transfer outputs across the smart wallet's execution logs (including loop iterations), groups by token, multiplies by the workflow's value-fee tier percentage, and emits one TokenTotal per token paid. Native-ETH transfers fold into the existing native entry. Renders as a single comma-separated line: ⛽ Cost: 0.000003 ETH ($0.01), 1.2 USDC ($1.20) USD pricing layers, no hardcoded fallbacks: - Stablecoins (USDC/USDT/DAI/USDS/PYUSD/sDAI/FDUSD/TUSD/GUSD/LUSD/RLUSD/USDG) short-circuit to $1.00 via a chain-keyed Stablecoins map in blockchain_constants.go. Algorithmic/synthetic stablecoins deliberately excluded so depeg events surface correctly. - Other ERC20s use PriceService.GetERC20PriceUSD (new method on the interface), implemented on MoralisService. - When unavailable, USD field stays empty and renderer prints "$?". - FallbackPriceService ($2,500 ETH hardcoded) removed; nil priceService is handled gracefully throughout (executor, fee estimator, summarizer). Tests: LookupStablecoin map coverage; percentOfRaw value-fee math; tokenBucket.toTokenTotal stablecoin shortcut + zero-rounding + missing price service.
…r extraction
For non-stablecoin ERC20 transfers, the per-token Cost line was rendering
the symbol as "?" and defaulting to 18 decimals — fine for tokens that
happen to use 18 decimals, but wrong for any other (USDT lookalikes,
project tokens with 8/9/12 decimals, etc.).
Wire transfer extraction through resolveTokenInfo() which checks:
1. Stablecoins map (fast path, no network)
2. TokenEnrichmentService.GetTokenMetadata (same source the
/api/summarize request uses, so decimals match what context-memory
sees)
3. Fall back to ("?", 18) as a last resort
Now a workflow that transfers PEPE renders the correct decimal count
for PEPE without needing to be in the Stablecoins map.
…ames Adds 20260502-notification-cost-line-and-runner.md covering the staging- branch work that landed Runner+Cost in Telegram/email notifications, removed FallbackPriceService, added the Stablecoins map, extended PriceService with GetERC20PriceUSD via Moralis, and wired transfer extraction (incl. loop iterations) for per-token value-fee legs. Final commit on the stack: 937d1fd. Renames the two earlier sequential-numbered entries to use the date prefix per the canonical change-doc convention: - 0001-execution-status-redesign → 20260413-... - 0002-uniswap-simulation-state-propagation → 20260416-... README updated to drop the "earlier entries stay as-is" carve-out since all entries now share one filename format.
Two related Copilot review findings: - Read-only deployed runs were rendering "0.00000X ETH ($0.02)" — the flat platform fee converted to ETH-equivalent looked like gas, even though no on-chain action ran. Misleading. - When Moralis isn't configured, the platform fee silently disappeared from the rendered Cost line because USD→ETH conversion couldn't proceed. The user's total understated by $0.02 with no signal. Fix: stop folding executionFee into the native-ETH amount. Always emit it as its own USD-denominated TokenTotal entry; renderer special-cases Unit=="USD" to emit "$X platform fee". Resulting renders: ⛽ Cost: 0.000003 ETH ($0.01), $0.02 platform fee (priced + on-chain) ⛽ Cost: 0.000003 ETH ($?), $0.02 platform fee (unpriced) ⛽ Cost: $0.02 platform fee (read-only) Attribution stays clear: gas in ETH, value-fee per token, platform fee in dollars. The platform fee is shown even without Moralis configured, since its USD value is known directly. Tests: TestFormatTelegramFromStructured_PlatformFeeOnly (read-only path) and TestFormatTelegramFromStructured_NoPriceService (unpriced path). Existing RunnerAndFees test updated to expect the separate platform fee.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
backup_dir was only consumed by the migration-time backup. The nested
BackupConfig (Enabled/IntervalMinutes/BackupDir) was never read — periodic
backups are not wired up anywhere. Remove the YAML field, the
/tmp/ap-avs-backup default, and the dead struct. The migrator now writes
to <db_path>_backup automatically, keeping backups co-located with the DB
instead of falling back to /tmp.
blockThe Simulation:/Run #N:/Run Node: prefix now renders inside the code
block alongside the workflow name, restoring the pre-rename behavior
where the whole "Simulation: WorkflowName" chunk was code-wrapped.
docs: added instructions for ./docs/changes
docs: fix backup-path hyphen/underscore mismatch in example yaml
The comment said <db_path>-backup but the code derives <db_path>_backup.
Caught by both Copilot and Claude review on PR #529.
Add per-step gas (gasUsed/gasPrice/totalGasCost) on the step digest and a
top-level fees block (executionFee + cogs[] + valueFee) on the summarize
request, mirroring the Execution proto's three-component fee model. The
fees block is built via the same buildExecutionFee/buildCOGSFromSteps/
buildValueFee helpers the executor uses for Execution.Fee, so summarize
fees match persisted execution fees exactly.
NewContextMemorySummarizerFromAggregatorConfig now constructs the
summarizer directly so it can pass c.FeeRates through. The public
NewContextMemorySummarizer(baseURL, authToken) is unchanged for tests.
Tenderly simulations were previously sent with gas_price=0, so the echoed
response had total_gas_cost=0, which buildCOGSFromSteps then skipped — and
context-memory saw fees.cogs=[] on every simulate_task request. Per-step
gasUsed/gasPrice/totalGasCost were also absent, violating the spec that
on-chain steps always carry them.
Changes:
Add DefaultGasPriceByChain (mainnet 5 gwei, Sepolia 0.5, Base 0.05,
Base Sepolia 0.01) + GetDefaultGasPrice helper.
Send GetDefaultGasPrice(chainID) in Tenderly simulate requests.
Throw with diagnostic context when the response carries gas_used but
zero/unparseable gas_price; drop the silent "only gas_used" branch.
Populate executionLog.GasUsed/GasPrice/TotalGasCost in the eth_transfer
simulation branch (StandardGasCost × per-chain default).
feat: render Runner + Cost from context-memory response
Consume the new body.runner / body.fees fields the context-memory API now
returns (PRD: docs/changes/20260501-summary-runner-and-fees-sections.md):
runnerandfeesfrom /api/summarize and surface asSummary.Runner / Summary.Fees.
one-line Cost summary right below. Owner intentionally omitted on
Telegram (channel-space). Simulations show "⛽ (cost estimated at
deploy)" instead of fake-precision numbers, since sim gas prices are
conservative chain defaults rather than real network conditions.
source from Summary.Runner directly — single source of truth shared
with Telegram, no parallel VM-state derivation. analysisHtml gains a
Cost section with per-step gas bullets + value-fee line for deployed
runs; simulations get the same deploy-time placeholder.
Drops the dead VM-state runner-fallback chain (vm.task / aa_sender /
settings.runner) in vm_runner_rest.go in favor of reading s.Runner.
The Telegram formatter's simulation branch reads s.Workflow.IsSimulation,
but Summary.Workflow was populated only when context-memory's response
included a body.workflow block. Simulation responses don't, so the flag
defaulted to false and the formatter took the deployed-run path —
producing a malformed
Cost: + $0.02 platform feeline when fee fieldswere partially present.
Always populate Summary.Workflow.IsSimulation from vm.IsSimulation (the
aggregator already knows whether it ran a simulation). The API response
can still enrich workflow Name/Chain/ChainID/RunNumber, but the
simulation flag is no longer dependent on response shape.
The h3 "Cost" heading above "⛽ (cost estimated at deploy)" was redundant
visual noise — the placeholder line conveys the same information without
the header.
Runner and Fees data was being shipped to context-memory in /api/summarize
requests and echoed back in responses, but the aggregator already has
every input it needs (vm.task.SmartWalletAddress, vm.task.Owner,
vm.ExecutionLogs, config.FeeRates) — context-memory was a pure
passthrough adding only round-trip latency and API-shape complexity.
This commit removes that round-trip:
fees block. Step digest reverts to its pre-Runner/Fees shape.
removed; ContextMemorySummarizer.feeRates field is removed.
existing builders in summarizer_deterministic.go) that read VM state
directly.
path) populate Summary.Runner and Summary.Fees via these helpers.
so both paths see the same configured rates.
Net behavior is identical for notification rendering (formatters still
read Summary.Runner and Summary.Fees); the data path is shorter and the
context-memory API stays focused on natural-language summary fields.
Notifications now render the Cost as a single comma-separated line with
the native unit first and USD parenthetical:
⛽ Cost: 0.000003 ETH ($0.01)
⛽ Cost: 0.01 ETH ($25.00), 1.2 USDC ($1.20)
Tokens that can't be priced render as "$?" — never block the notification.
For simulation runs the line collapses to the static placeholder
"⛽ (cost estimated at deploy)" since simulated gas uses conservative
chain defaults rather than real network conditions.
Drops the per-step bullets, gas-units detail, and value-fee subtitle
that the email Cost section used to render — breakdowns live on the
dashboard, not in notifications.
The chain price service (Moralis with $2,500 ETH fallback) is reused
from the existing fee-estimation path. Engine startup wires both
SetFeeRates and SetPriceService at package level so ComposeSummary
(deterministic) and ContextMemorySummarizer.Summarize share one source
of truth.
V1 ships the native-token total leg only (gas + executionFee converted
to ETH). The Total []*TokenTotal shape supports per-token value-fee
legs for V2 once transfer extraction across loop iterations lands —
the renderer needs no changes.
V2 of the multi-token Cost line. Walks transfer outputs across the smart
wallet's execution logs (including loop iterations), groups by token,
multiplies by the workflow's value-fee tier percentage, and emits one
TokenTotal per token paid. Native-ETH transfers fold into the existing
native entry. Renders as a single comma-separated line:
⛽ Cost: 0.000003 ETH ($0.01), 1.2 USDC ($1.20)
USD pricing layers, no hardcoded fallbacks:
short-circuit to $1.00 via a chain-keyed Stablecoins map in
blockchain_constants.go. Algorithmic/synthetic stablecoins deliberately
excluded so depeg events surface correctly.
interface), implemented on MoralisService.
is handled gracefully throughout (executor, fee estimator, summarizer).
Tests: LookupStablecoin map coverage; percentOfRaw value-fee math;
tokenBucket.toTokenTotal stablecoin shortcut + zero-rounding + missing
price service.
For non-stablecoin ERC20 transfers, the per-token Cost line was rendering
the symbol as "?" and defaulting to 18 decimals — fine for tokens that
happen to use 18 decimals, but wrong for any other (USDT lookalikes,
project tokens with 8/9/12 decimals, etc.).
Wire transfer extraction through resolveTokenInfo() which checks:
/api/summarize request uses, so decimals match what context-memory
sees)
Now a workflow that transfers PEPE renders the correct decimal count
for PEPE without needing to be in the Stablecoins map.
Adds 20260502-notification-cost-line-and-runner.md covering the staging-
branch work that landed Runner+Cost in Telegram/email notifications,
removed FallbackPriceService, added the Stablecoins map, extended
PriceService with GetERC20PriceUSD via Moralis, and wired transfer
extraction (incl. loop iterations) for per-token value-fee legs.
Final commit on the stack: 937d1fd.
Renames the two earlier sequential-numbered entries to use the date
prefix per the canonical change-doc convention:
README updated to drop the "earlier entries stay as-is" carve-out
since all entries now share one filename format.