Skip to content

Commit 6f062f6

Browse files
committed
fix: render platform fee separately in Cost line, not folded into ETH
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.
1 parent a8b70de commit 6f062f6

4 files changed

Lines changed: 85 additions & 11 deletions

File tree

core/taskengine/summarizer_deterministic.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,11 +1549,12 @@ func buildTotalsFromVM(vm *VM, fees *FeesInfo) []*TokenTotal {
15491549
}
15501550
weiPerEth := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
15511551
nativeEth := new(big.Float).Quo(new(big.Float).SetInt(gasWei), weiPerEth)
1552-
if fees.ExecutionFee != nil && fees.ExecutionFee.Amount != "" && nativePriceUSD != nil {
1553-
if usd, ok := new(big.Float).SetString(fees.ExecutionFee.Amount); ok {
1554-
nativeEth.Add(nativeEth, new(big.Float).Quo(usd, nativePriceUSD))
1555-
}
1556-
}
1552+
// Platform (execution) fee is rendered as its own USD entry below — NOT
1553+
// folded into the native-ETH amount. Keeping it separate ensures (a) the
1554+
// fee is shown even when no price service is configured, (b) read-only
1555+
// runs that paid only the platform fee don't display a misleading
1556+
// "0.00000X ETH" gas-equivalent line, and (c) attribution stays clear
1557+
// (this much was gas, this much was the platform charge).
15571558

15581559
// 2. Value-fee per-token aggregation. tier_percentage × tx_value in each
15591560
// transferred token's units, summed across loop iterations.
@@ -1611,6 +1612,18 @@ func buildTotalsFromVM(vm *VM, fees *FeesInfo) []*TokenTotal {
16111612
}
16121613
out = append(out, entry)
16131614
}
1615+
1616+
// Platform (execution) fee — appended last as a USD-denominated entry.
1617+
// Renderer special-cases Unit=="USD" to emit "$X platform fee" instead of
1618+
// the standard token-amount format. Always shown when non-zero so the
1619+
// user sees what they paid even when no price service is configured.
1620+
if fees.ExecutionFee != nil {
1621+
amount := trimTrailingZeros(strings.TrimSpace(fees.ExecutionFee.Amount))
1622+
if amount != "" && amount != "0" {
1623+
out = append(out, &TokenTotal{Amount: amount, Unit: "USD", USD: amount})
1624+
}
1625+
}
1626+
16141627
return out
16151628
}
16161629

core/taskengine/summarizer_format_email.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ func buildFeesSectionHTML(s Summary) string {
347347
if t == nil || t.Amount == "" || t.Amount == "0" {
348348
continue
349349
}
350+
// USD-unit entries are the platform fee — render as "$X platform fee".
351+
if t.Unit == "USD" {
352+
parts = append(parts, fmt.Sprintf("$%s platform fee", html.EscapeString(t.Amount)))
353+
continue
354+
}
350355
usd := "$?"
351356
if t.USD != "" {
352357
usd = "$" + t.USD

core/taskengine/summarizer_format_telegram.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ func formatTelegramCostLine(s Summary) string {
264264
if t == nil || t.Amount == "" || t.Amount == "0" {
265265
continue
266266
}
267+
// USD-unit entries are the platform fee — render as "$X platform fee"
268+
// (the dollar amount is already canonical; no need for the parenthetical).
269+
if t.Unit == "USD" {
270+
parts = append(parts, fmt.Sprintf("$%s platform fee", html.EscapeString(t.Amount)))
271+
continue
272+
}
267273
usd := "$?"
268274
if t.USD != "" {
269275
usd = "$" + t.USD

core/taskengine/summarizer_format_test.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,8 @@ func TestFormatTelegramFromStructured_RunnerAndFees(t *testing.T) {
10351035
Fees: &FeesInfo{
10361036
ExecutionFee: &FeeAmount{Amount: "0.020000", Unit: "USD"},
10371037
Total: []*TokenTotal{
1038-
{Amount: "0.000011", Unit: "ETH", USD: "0.03"},
1038+
{Amount: "0.000003", Unit: "ETH", USD: "0.01"},
1039+
{Amount: "0.02", Unit: "USD", USD: "0.02"}, // platform fee — renders as "$0.02 platform fee"
10391040
},
10401041
},
10411042
}
@@ -1061,12 +1062,12 @@ func TestFormatTelegramFromStructured_RunnerAndFees(t *testing.T) {
10611062
t.Errorf("runner address should not be <code>-wrapped, got:\n%s", out)
10621063
}
10631064

1064-
// Cost line: native-token first, USD parenthetical, ⛽ leading emoji.
1065-
if !strings.Contains(out, "⛽ <b>Cost:</b> 0.000011 ETH ($0.03)") {
1066-
t.Errorf("missing multi-token Cost line in:\n%s", out)
1065+
// Cost line: native gas first, then platform fee as a separate "$X platform fee".
1066+
if !strings.Contains(out, "⛽ <b>Cost:</b> 0.000003 ETH ($0.01), $0.02 platform fee") {
1067+
t.Errorf("missing combined gas+platform Cost line in:\n%s", out)
10671068
}
1068-
if strings.Contains(out, "(~") || strings.Contains(out, "platform fee") || strings.Contains(out, "<b>Value fee:</b>") {
1069-
t.Errorf("Telegram should not render gas-units / 'platform fee' / Value fee line, got:\n%s", out)
1069+
if strings.Contains(out, "(~") || strings.Contains(out, "<b>Value fee:</b>") {
1070+
t.Errorf("Telegram should not render gas-units detail or Value fee line, got:\n%s", out)
10701071
}
10711072
if strings.Contains(out, "(cost estimated at deploy)") {
10721073
t.Errorf("deployed run should not show simulation placeholder, got:\n%s", out)
@@ -1081,6 +1082,55 @@ func TestFormatTelegramFromStructured_RunnerAndFees(t *testing.T) {
10811082
t.Logf("Telegram render:\n%s", out)
10821083
}
10831084

1085+
// TestFormatTelegramFromStructured_PlatformFeeOnly covers the read-only path:
1086+
// no on-chain steps, only the platform fee. Renders as "$0.02 platform fee"
1087+
// with no gas-equivalent ETH line that could be misread as gas.
1088+
func TestFormatTelegramFromStructured_PlatformFeeOnly(t *testing.T) {
1089+
summary := Summary{
1090+
Subject: "Run #1: Read-Only Workflow successfully completed",
1091+
Status: "success",
1092+
Network: "Sepolia",
1093+
Workflow: &WorkflowInfo{IsSimulation: false},
1094+
Runner: &RunnerInfo{SmartWallet: "0x8Ee38eB323c14a1752DABDA1cca9661AEE377017"},
1095+
Fees: &FeesInfo{
1096+
ExecutionFee: &FeeAmount{Amount: "0.02", Unit: "USD"},
1097+
Total: []*TokenTotal{
1098+
{Amount: "0.02", Unit: "USD", USD: "0.02"},
1099+
},
1100+
},
1101+
}
1102+
out := FormatForMessageChannels(summary, "telegram", nil)
1103+
if !strings.Contains(out, "⛽ <b>Cost:</b> $0.02 platform fee") {
1104+
t.Errorf("expected platform-fee-only Cost line, got:\n%s", out)
1105+
}
1106+
if strings.Contains(out, " ETH (") {
1107+
t.Errorf("read-only run should not render an ETH line, got:\n%s", out)
1108+
}
1109+
}
1110+
1111+
// TestFormatTelegramFromStructured_NoPriceService covers Moralis-not-configured:
1112+
// gas renders with $? for USD; platform fee still shows as the canonical $X.
1113+
func TestFormatTelegramFromStructured_NoPriceService(t *testing.T) {
1114+
summary := Summary{
1115+
Subject: "Run #1: Workflow successfully completed",
1116+
Status: "success",
1117+
Network: "Sepolia",
1118+
Workflow: &WorkflowInfo{IsSimulation: false},
1119+
Runner: &RunnerInfo{SmartWallet: "0x8Ee38eB323c14a1752DABDA1cca9661AEE377017"},
1120+
Fees: &FeesInfo{
1121+
ExecutionFee: &FeeAmount{Amount: "0.02", Unit: "USD"},
1122+
Total: []*TokenTotal{
1123+
{Amount: "0.000003", Unit: "ETH", USD: ""}, // unpriced — renders "$?"
1124+
{Amount: "0.02", Unit: "USD", USD: "0.02"},
1125+
},
1126+
},
1127+
}
1128+
out := FormatForMessageChannels(summary, "telegram", nil)
1129+
if !strings.Contains(out, "⛽ <b>Cost:</b> 0.000003 ETH ($?), $0.02 platform fee") {
1130+
t.Errorf("expected unpriced ETH + platform fee, got:\n%s", out)
1131+
}
1132+
}
1133+
10841134
// TestFormatTelegramFromStructured_MultiToken_USDPlaceholder verifies the
10851135
// multi-token comma-separated render and the "$?" placeholder for unpriceable
10861136
// entries. Mirror of the format spec in the PRD's Rendering section.

0 commit comments

Comments
 (0)