economics: backhaul is the inter-region leg, a real model line (#288 review)
The home box meters a hopped request's bytes once, priced as egress; the edge→client leg is what that price approximates, and the extra cost is the home→edge inter-region transfer, which the model had no line for. RateCard gains inter_region_gb_usd (default 0.02; a saved scenario without it reads 0, never a silent price), the page's row is 'Backhaul (inter-region leg)' added to the model total and still in no account's cost or revenue — pinned by a PriceAccount identity test. Also: JSON errors on the JSON door, zero-backhaul accounts omitted from the report, the CLI uses the package stdout seam and newTable, the meter doc no longer claims the meters stay out of statements.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9 files changed,
+105
−62
economics.go+21 −20economics_backhaul_test.go+6 −5internal/console/economicsbackhaul_test.go+23 −4internal/console/economicspass.go+27 −20internal/console/templates/economics.html+8 −4internal/console/templates/economics_accounts.html+1 −1internal/economics/scenario.go+10 −1internal/usage/usage.go+7 −6main.go+2 −1
diff --git a/economics.go b/economics.go| index f611b9d8e..982362921 100644 |
| --- a/economics.go |
| +++ b/economics.go |
| @@ -25,7 +25,6 @@ import ( |
| "sort" |
| "strconv" |
| "strings" |
| - "text/tabwriter" |
| "time" |
| ) |
| @@ -146,22 +145,22 @@ const economicsBackhaulUsage = `usage: carlos economics backhaul [-period mtd|la |
| Shows what serving through region edges costs the platform this period: |
| the part of every account's egress that was backhauled from its home box |
| (usage.EdgeRow's hop subset — inside the egress totals, never on top), |
| -priced at the rate card's egress $/GB and ABSORBED — it is in nobody's |
| -bill (#284). Operator only; a non-operator's token gets the same bare 404 |
| +priced at the rate card's inter-region $/GB (the extra leg; the home box's |
| +bytes are already egress) and ABSORBED — it is in nobody's bill (#284). Operator only; a non-operator's token gets the same bare 404 |
| an unknown route gets. |
| ` |
| type backhaulReport struct { |
| - Period string `json:"period"` |
| - CoveredDays int `json:"covered_days"` |
| - PeriodDays int `json:"period_days"` |
| - EgressGB float64 `json:"egress_gb"` |
| - BackhaulGB float64 `json:"backhaul_gb"` |
| - BackhaulReq1k float64 `json:"backhaul_requests_1k"` |
| - ShareOfEgress float64 `json:"share_of_egress"` |
| - EgressGBUSD float64 `json:"egress_gb_usd"` |
| - AbsorbedUSD float64 `json:"absorbed_usd"` |
| - Accounts []backhaulAccount `json:"accounts"` |
| + Period string `json:"period"` |
| + CoveredDays int `json:"covered_days"` |
| + PeriodDays int `json:"period_days"` |
| + EgressGB float64 `json:"egress_gb"` |
| + BackhaulGB float64 `json:"backhaul_gb"` |
| + BackhaulReq1k float64 `json:"backhaul_requests_1k"` |
| + ShareOfEgress float64 `json:"share_of_egress"` |
| + InterRegionGBUSD float64 `json:"inter_region_gb_usd"` |
| + AbsorbedUSD float64 `json:"absorbed_usd"` |
| + Accounts []backhaulAccount `json:"accounts"` |
| } |
| type backhaulAccount struct { |
| @@ -211,22 +210,24 @@ func runEconomicsBackhaul(args []string) { |
| log.Fatal("carlos economics backhaul works through a console, and this shell is in direct bucket mode\n" + |
| "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). Unset the store env and run: carlos auth login -console <url>.") |
| } |
| + printUsing(stdout, sess) |
| rep, err := sess.economicsBackhaul(context.Background(), *period) |
| if err != nil { |
| log.Fatalf("economics backhaul: %v", err) |
| } |
| - printBackhaulReport(os.Stdout, rep) |
| + printBackhaulReport(stdout, rep) |
| } |
| func printBackhaulReport(stdout io.Writer, rep backhaulReport) { |
| - fmt.Fprintf(stdout, "backhaul %s (%d/%d days covered): %.2f GB of %.2f GB egress (%.1f%%), %.1fk requests — ≈ $%.2f at $%.3f/GB, absorbed\n", |
| - rep.Period, rep.CoveredDays, rep.PeriodDays, rep.BackhaulGB, rep.EgressGB, rep.ShareOfEgress*100, rep.BackhaulReq1k, rep.AbsorbedUSD, rep.EgressGBUSD) |
| - w := tabwriter.NewWriter(stdout, 0, 4, 2, ' ', 0) |
| + fmt.Fprintf(stdout, "backhaul %s (%d/%d days covered): %.2f GB of %.2f GB egress (%.1f%%), %.1fk requests — inter-region leg ≈ $%.2f at $%.3f/GB, absorbed\n", |
| + rep.Period, rep.CoveredDays, rep.PeriodDays, rep.BackhaulGB, rep.EgressGB, rep.ShareOfEgress*100, rep.BackhaulReq1k, rep.AbsorbedUSD, rep.InterRegionGBUSD) |
| + if len(rep.Accounts) == 0 { |
| + fmt.Fprintln(stdout, "no account backhauled anything this period") |
| + return |
| + } |
| + w := newTable(stdout) |
| fmt.Fprintln(w, "ACCOUNT\tNAME\tEGRESS GB\tBACKHAUL GB\tSHARE\tREQ (k)\tABSORBED") |
| for _, a := range rep.Accounts { |
| - if a.BackhaulGB == 0 && a.BackhaulReq1k == 0 { |
| - continue |
| - } |
| fmt.Fprintf(w, "%s\t%s\t%.2f\t%.2f\t%.1f%%\t%.1f\t$%.2f\n", a.Account, a.Name, a.EgressGB, a.BackhaulGB, a.ShareOfEgress*100, a.BackhaulReq1k, a.AbsorbedUSD) |
| } |
| w.Flush() |
diff --git a/economics_backhaul_test.go b/economics_backhaul_test.go| index 29a4d7b6a..1077f86ae 100644 |
| --- a/economics_backhaul_test.go |
| +++ b/economics_backhaul_test.go |
| @@ -9,19 +9,20 @@ func TestPrintBackhaulReport(t *testing.T) { |
| var b strings.Builder |
| printBackhaulReport(&b, backhaulReport{ |
| Period: "mtd", CoveredDays: 3, PeriodDays: 10, EgressGB: 14, BackhaulGB: 5, BackhaulReq1k: 7, |
| - ShareOfEgress: 5.0 / 14, EgressGBUSD: 0.09, AbsorbedUSD: 0.45, |
| + ShareOfEgress: 5.0 / 14, InterRegionGBUSD: 0.02, AbsorbedUSD: 0.10, |
| Accounts: []backhaulAccount{ |
| {Account: "bab", Name: "ops", EgressGB: 10, BackhaulGB: 2.5, BackhaulReq1k: 7, ShareOfEgress: 0.25, AbsorbedUSD: 0.225}, |
| - {Account: "bad", EgressGB: 0}, |
| }, |
| }) |
| out := b.String() |
| - for _, want := range []string{"backhaul mtd (3/10 days covered): 5.00 GB of 14.00 GB egress (35.7%)", "$0.45 at $0.090/GB, absorbed", "bab", "ops", "25.0%"} { |
| + for _, want := range []string{"backhaul mtd (3/10 days covered): 5.00 GB of 14.00 GB egress (35.7%)", "inter-region leg ≈ $0.10 at $0.020/GB, absorbed", "bab", "ops", "25.0%"} { |
| if !strings.Contains(out, want) { |
| t.Errorf("missing %q in:\n%s", want, out) |
| } |
| } |
| - if strings.Contains(out, "bad") { |
| - t.Errorf("an account with no backhaul was listed:\n%s", out) |
| + var empty strings.Builder |
| + printBackhaulReport(&empty, backhaulReport{Period: "7d", PeriodDays: 7}) |
| + if !strings.Contains(empty.String(), "no account backhauled anything") { |
| + t.Errorf("empty report printed a bare header:\n%s", empty.String()) |
| } |
| } |
diff --git a/internal/console/economicsbackhaul_test.go b/internal/console/economicsbackhaul_test.go| index c32185392..e158d5a11 100644 |
| --- a/internal/console/economicsbackhaul_test.go |
| +++ b/internal/console/economicsbackhaul_test.go |
| @@ -17,7 +17,7 @@ import ( |
| func TestBackhaulReportMaths(t *testing.T) { |
| v := &EconomicsView{ |
| Period: "mtd", CoveredDays: 3, PeriodDays: 10, |
| - Scenario: economics.Scenario{RateCard: economics.RateCard{EgressGBUSD: 0.09}}, |
| + Scenario: economics.Scenario{RateCard: economics.RateCard{EgressGBUSD: 0.09, InterRegionGBUSD: 0.02}}, |
| Names: map[string]string{"bab": "ops"}, |
| Meters: map[string]map[string]float64{ |
| "bab": {usage.MeterEgressGB: 10, usage.MeterBackhaulGB: 2.5, usage.MeterBackhaulRequests1k: 7}, |
| @@ -29,13 +29,14 @@ func TestBackhaulReportMaths(t *testing.T) { |
| if rep.EgressGB != 14 || rep.BackhaulGB != 5 || rep.BackhaulReq1k != 7 { |
| t.Fatalf("totals = %+v", rep) |
| } |
| - if math.Abs(rep.ShareOfEgress-5.0/14) > 1e-9 || math.Abs(rep.AbsorbedUSD-0.45) > 1e-9 || rep.EgressGBUSD != 0.09 { |
| + if math.Abs(rep.ShareOfEgress-5.0/14) > 1e-9 || math.Abs(rep.AbsorbedUSD-0.10) > 1e-9 || rep.InterRegionGBUSD != 0.02 { |
| t.Fatalf("share/absorbed = %v/%v", rep.ShareOfEgress, rep.AbsorbedUSD) |
| } |
| - if len(rep.Accounts) != 3 || rep.Accounts[0].Account != "bab" || rep.Accounts[1].Account != "bae" || rep.Accounts[2].Account != "bad" { |
| + // bad has egress but no backhaul: counted in the totals, no row. |
| + if len(rep.Accounts) != 2 || rep.Accounts[0].Account != "bab" || rep.Accounts[1].Account != "bae" { |
| t.Fatalf("order = %+v", rep.Accounts) |
| } |
| - if rep.Accounts[0].Name != "ops" || math.Abs(rep.Accounts[0].ShareOfEgress-0.25) > 1e-9 || math.Abs(rep.Accounts[1].ShareOfEgress-0.625) > 1e-9 || rep.Accounts[2].ShareOfEgress != 0 { |
| + if rep.Accounts[0].Name != "ops" || math.Abs(rep.Accounts[0].ShareOfEgress-0.25) > 1e-9 || math.Abs(rep.Accounts[1].ShareOfEgress-0.625) > 1e-9 || math.Abs(rep.Accounts[0].AbsorbedUSD-0.05) > 1e-9 { |
| t.Fatalf("accounts = %+v", rep.Accounts) |
| } |
| } |
| @@ -88,3 +89,21 @@ func TestAPIEconomicsBackhaulGate(t *testing.T) { |
| t.Fatalf("report = %+v", rep) |
| } |
| } |
| + |
| +// The whole "priced nowhere" claim in one assertion: an account prices |
| +// identically with and without the backhaul meters present. |
| +func TestBackhaulMetersNeverPriceAnAccount(t *testing.T) { |
| + sc := economics.DefaultScenario() |
| + base := map[string]float64{usage.MeterEgressGB: 40, usage.MeterRequests1k: 900, usage.MeterMemGBHours: 800, usage.MeterAppMonths: 3} |
| + with := map[string]float64{} |
| + for k, v := range base { |
| + with[k] = v |
| + } |
| + with[usage.MeterBackhaulGB] = 25 |
| + with[usage.MeterBackhaulRequests1k] = 500 |
| + a := economics.PriceAccount(sc, "bab", base, 12.5) |
| + b := economics.PriceAccount(sc, "bab", with, 12.5) |
| + if a != b { |
| + t.Fatalf("backhaul meters changed an account's price:\n%+v\n%+v", a, b) |
| + } |
| +} |
diff --git a/internal/console/economicspass.go b/internal/console/economicspass.go| index f8f1cf37c..08df9644b 100644 |
| --- a/internal/console/economicspass.go |
| +++ b/internal/console/economicspass.go |
| @@ -1393,21 +1393,26 @@ func (s *Server) handleAPIEconomicsBill(w http.ResponseWriter, r *http.Request) |
| // backhaulReport is GET /api/economics/backhaul: what serving through region |
| // edges costs the platform this period (#288). The backhaul meters are the |
| -// hop SUBSET of egress (usage.EdgeRow) — inside the egress totals, never on |
| -// top of them — priced at the rate card's egress $/GB and stated as |
| -// absorbed: not in any account's cost or revenue (#284's ruling). Operator |
| -// only, same bare-404 posture as the bill endpoint. |
| +// hop SUBSET of egress (usage.EdgeRow): bytes the HOME box sent to a region |
| +// edge, which the edge then sent on to the client. The home box's metered |
| +// bytes are already priced as egress; the extra leg is the inter-region |
| +// transfer, priced here at RateCard.InterRegionGBUSD and stated as |
| +// absorbed: a platform cost, in no account's cost or revenue (#284's |
| +// ruling). Accounts with no backhaul are omitted. Operator only, the bill |
| +// door's bare-404 posture for a token without the bit; the route itself is |
| +// discoverable (an unauthenticated GET gets cliUser's 401), which is the |
| +// same as every other /api door. |
| type backhaulReport struct { |
| - Period string `json:"period"` |
| - CoveredDays int `json:"covered_days"` |
| - PeriodDays int `json:"period_days"` |
| - EgressGB float64 `json:"egress_gb"` |
| - BackhaulGB float64 `json:"backhaul_gb"` |
| - BackhaulReq1k float64 `json:"backhaul_requests_1k"` |
| - ShareOfEgress float64 `json:"share_of_egress"` // 0..1; 0 when no egress |
| - EgressGBUSD float64 `json:"egress_gb_usd"` // the rate it is priced at |
| - AbsorbedUSD float64 `json:"absorbed_usd"` |
| - Accounts []backhaulAccount `json:"accounts"` // backhaul desc, account asc on ties |
| + Period string `json:"period"` |
| + CoveredDays int `json:"covered_days"` |
| + PeriodDays int `json:"period_days"` |
| + EgressGB float64 `json:"egress_gb"` |
| + BackhaulGB float64 `json:"backhaul_gb"` |
| + BackhaulReq1k float64 `json:"backhaul_requests_1k"` |
| + ShareOfEgress float64 `json:"share_of_egress"` // 0..1; 0 when no egress |
| + InterRegionGBUSD float64 `json:"inter_region_gb_usd"` // the rate the extra leg is priced at |
| + AbsorbedUSD float64 `json:"absorbed_usd"` |
| + Accounts []backhaulAccount `json:"accounts"` // backhaul desc, account asc on ties |
| } |
| type backhaulAccount struct { |
| @@ -1422,7 +1427,7 @@ type backhaulAccount struct { |
| func buildBackhaulReport(v *EconomicsView) backhaulReport { |
| rep := backhaulReport{Period: v.Period, CoveredDays: v.CoveredDays, PeriodDays: v.PeriodDays, |
| - EgressGBUSD: v.Scenario.RateCard.EgressGBUSD, Accounts: []backhaulAccount{}} |
| + InterRegionGBUSD: v.Scenario.RateCard.InterRegionGBUSD, Accounts: []backhaulAccount{}} |
| for acct, meters := range v.Meters { |
| a := backhaulAccount{Account: acct, Name: v.Names[acct], |
| EgressGB: meters[usage.MeterEgressGB], BackhaulGB: meters[usage.MeterBackhaulGB], |
| @@ -1430,16 +1435,19 @@ func buildBackhaulReport(v *EconomicsView) backhaulReport { |
| if a.EgressGB > 0 { |
| a.ShareOfEgress = a.BackhaulGB / a.EgressGB |
| } |
| - a.AbsorbedUSD = a.BackhaulGB * rep.EgressGBUSD |
| + a.AbsorbedUSD = a.BackhaulGB * rep.InterRegionGBUSD |
| rep.EgressGB += a.EgressGB |
| rep.BackhaulGB += a.BackhaulGB |
| rep.BackhaulReq1k += a.BackhaulReq1k |
| + if a.BackhaulGB == 0 && a.BackhaulReq1k == 0 { |
| + continue // egress counted, row omitted: no backhaul, nothing to say |
| + } |
| rep.Accounts = append(rep.Accounts, a) |
| } |
| if rep.EgressGB > 0 { |
| rep.ShareOfEgress = rep.BackhaulGB / rep.EgressGB |
| } |
| - rep.AbsorbedUSD = rep.BackhaulGB * rep.EgressGBUSD |
| + rep.AbsorbedUSD = rep.BackhaulGB * rep.InterRegionGBUSD |
| sort.Slice(rep.Accounts, func(i, j int) bool { |
| if rep.Accounts[i].BackhaulGB != rep.Accounts[j].BackhaulGB { |
| return rep.Accounts[i].BackhaulGB > rep.Accounts[j].BackhaulGB |
| @@ -1471,9 +1479,8 @@ func (s *Server) handleAPIEconomicsBackhaul(w http.ResponseWriter, r *http.Reque |
| v, err := s.LoadEconomics(r.Context(), r.URL.Query().Get("period")) |
| if err != nil { |
| log.Printf("console: api economics backhaul: %v", err) |
| - http.Error(w, "economics unavailable", http.StatusServiceUnavailable) |
| + jsonError(w, http.StatusServiceUnavailable, "economics unavailable") |
| return |
| } |
| - w.Header().Set("Content-Type", "application/json") |
| - _ = json.NewEncoder(w).Encode(buildBackhaulReport(v)) |
| + writeJSON(w, http.StatusOK, buildBackhaulReport(v)) |
| } |
diff --git a/internal/console/templates/economics.html b/internal/console/templates/economics.html| index 99395623c..3cc92bd3c 100644 |
| --- a/internal/console/templates/economics.html |
| +++ b/internal/console/templates/economics.html |
| @@ -164,8 +164,9 @@ |
| {{$s3storage := mul (sumMeter .Economics.Meters "storage_gb_month") .Economics.Scenario.RateCard.StorageGBMonthUSD}} |
| {{$s3requests := mul (sumMeter .Economics.Meters "requests_1k") .Economics.Scenario.RateCard.RequestsPer1kUSD}} |
| {{$egress := mul (sumMeter .Economics.Meters "egress_gb") .Economics.Scenario.RateCard.EgressGBUSD}} |
| +{{$backhaul := mul (sumMeter .Economics.Meters "backhaul_gb") .Economics.Scenario.RateCard.InterRegionGBUSD}} |
| {{$overheads := overheadUSD .Economics.Scenario .Economics.PeriodDays}} |
| -{{$modelTotal := add $ec2 $ebs $s3storage $s3requests $egress $overheads}} |
| +{{$modelTotal := add $ec2 $ebs $s3storage $s3requests $egress $backhaul $overheads}} |
| <h2>Model vs. bill</h2> |
| <div class="tablewrap"> |
| <table> |
| @@ -176,7 +177,7 @@ |
| <tr><td>S3 requests</td><td id="model-s3-requests">{{usd $s3requests}}</td></tr> |
| <tr><td>Egress</td><td id="model-egress">{{usd $egress}}</td></tr> |
| {{$backhaulGB := sumMeter .Economics.Meters "backhaul_gb"}} |
| - <tr class="muted"><td> of which backhaul (absorbed, inside egress — not added)</td><td><span id="model-backhaul">{{usd (mul $backhaulGB .Economics.Scenario.RateCard.EgressGBUSD)}}</span> · {{printf "%.2f" $backhaulGB}} GB{{with sumMeter .Economics.Meters "egress_gb"}}{{if gt . 0.0}} · {{printf "%.1f" (mul (div $backhaulGB .) 100.0)}}%{{end}}{{end}}</td></tr> |
| + <tr><td>Backhaul (inter-region leg) <span class="small">{{printf "%.2f" $backhaulGB}} GB{{with sumMeter .Economics.Meters "egress_gb"}}{{if gt . 0.0}} · {{printf "%.1f" (mul (div $backhaulGB .) 100.0)}}% of egress{{end}}{{end}} · absorbed, in no account's bill</span></td><td id="model-backhaul">{{usd $backhaul}}</td></tr> |
| <tr><td>Overheads</td><td id="model-overheads">{{usd $overheads}}</td></tr> |
| <tr><th>Model total</th><th id="model-total">{{usd $modelTotal}}</th></tr> |
| </table> |
| @@ -378,13 +379,14 @@ |
| var s3storage = sumMeter(payload.meters, "storage_gb_month") * sc.rate_card.storage_gb_month_usd; |
| var s3requests = sumMeter(payload.meters, "requests_1k") * sc.rate_card.requests_per_1k_usd; |
| var egress = sumMeter(payload.meters, "egress_gb") * sc.rate_card.egress_gb_usd; |
| - var modelTotal = ec2 + ebs + s3storage + s3requests + egress + overheadTotal; |
| + var backhaul = sumMeter(payload.meters, "backhaul_gb") * (sc.rate_card.inter_region_gb_usd || 0); |
| + var modelTotal = ec2 + ebs + s3storage + s3requests + egress + backhaul + overheadTotal; |
| setText("model-ec2", usd(ec2)); |
| setText("model-ebs", usd(ebs)); |
| setText("model-s3-storage", usd(s3storage)); |
| setText("model-s3-requests", usd(s3requests)); |
| setText("model-egress", usd(egress)); |
| - setText("model-backhaul", usd(sumMeter(payload.meters, "backhaul_gb") * sc.rate_card.egress_gb_usd)); |
| + setText("model-backhaul", usd(backhaul)); |
| setText("model-overheads", usd(overheadTotal)); |
| setText("model-total", usd(modelTotal)); |
| @@ -446,6 +448,7 @@ |
| <div class="knob-row"><label for="knob-storage-gb-month-usd">S3 storage $/GB-month</label><input type="number" step="0.001" min="0" id="knob-storage-gb-month-usd" value="{{.Economics.Scenario.RateCard.StorageGBMonthUSD}}"></div> |
| <div class="knob-row"><label for="knob-requests-per-1k-usd">S3 requests $/1k</label><input type="number" step="0.001" min="0" id="knob-requests-per-1k-usd" value="{{.Economics.Scenario.RateCard.RequestsPer1kUSD}}"></div> |
| <div class="knob-row"><label for="knob-egress-gb-usd">Egress $/GB</label><input type="number" step="0.001" min="0" id="knob-egress-gb-usd" value="{{.Economics.Scenario.RateCard.EgressGBUSD}}"></div> |
| +<div class="knob-row"><label for="knob-inter-region-gb-usd">Inter-region $/GB (backhaul leg)</label><input type="number" step="0.001" min="0" id="knob-inter-region-gb-usd" value="{{.Economics.Scenario.RateCard.InterRegionGBUSD}}"></div> |
| <h3>Overheads</h3> |
| <div class="tablewrap"> |
| @@ -813,6 +816,7 @@ |
| sc.rate_card.storage_gb_month_usd = numVal("knob-storage-gb-month-usd"); |
| sc.rate_card.requests_per_1k_usd = numVal("knob-requests-per-1k-usd"); |
| sc.rate_card.egress_gb_usd = numVal("knob-egress-gb-usd"); |
| + sc.rate_card.inter_region_gb_usd = numVal("knob-inter-region-gb-usd"); |
| document.querySelectorAll(".knob-overhead").forEach(function (el) { |
| var i = parseInt(el.getAttribute("data-index"), 10); |
| if (sc.rate_card.overheads && sc.rate_card.overheads[i]) { |
diff --git a/internal/console/templates/economics_accounts.html b/internal/console/templates/economics_accounts.html| index d7888a267..ae1d769ad 100644 |
| --- a/internal/console/templates/economics_accounts.html |
| +++ b/internal/console/templates/economics_accounts.html |
| @@ -83,7 +83,7 @@ |
| <th scope="col">CPU-h</th> |
| <th scope="col">GB-h</th> |
| <th scope="col">Egress GB</th> |
| - <th scope="col" title="the part of egress served through a region edge and backhauled from the home box — inside Egress GB, absorbed by the platform">Backhaul GB</th> |
| + <th scope="col" title="the part of Egress GB that left its home box for a region edge first — the inter-region leg is a platform cost, absorbed, in no account's bill">Backhaul GB</th> |
| <th scope="col">Storage GB-month</th> |
| <th scope="col">Cost</th> |
| <th scope="col">Would-be revenue</th> |
diff --git a/internal/economics/scenario.go b/internal/economics/scenario.go| index 911c0ddd9..ea6dd1743 100644 |
| --- a/internal/economics/scenario.go |
| +++ b/internal/economics/scenario.go |
| @@ -31,7 +31,15 @@ type RateCard struct { |
| StorageGBMonthUSD float64 `json:"storage_gb_month_usd"` |
| RequestsPer1kUSD float64 `json:"requests_per_1k_usd"` |
| EgressGBUSD float64 `json:"egress_gb_usd"` |
| - Overheads []LineItem `json:"overheads"` |
| + // InterRegionGBUSD is what a byte costs to move between the platform's |
| + // own regions — the backhaul leg (#288): a request served through a |
| + // region edge leaves the home box for that edge (this rate) and then |
| + // the edge for the client (EgressGBUSD, which is what the home box's |
| + // metered bytes are priced at). Additive; a scenario saved before it |
| + // existed reads as 0 and the model line shows $0 until the operator |
| + // sets it — never a silent default price for a cost that is real. |
| + InterRegionGBUSD float64 `json:"inter_region_gb_usd,omitempty"` |
| + Overheads []LineItem `json:"overheads"` |
| } |
| // Allocation says how a box's shared cost is split across its tenants. |
| @@ -136,6 +144,7 @@ func DefaultScenario() Scenario { |
| StorageGBMonthUSD: 0.023, |
| RequestsPer1kUSD: 0.005, |
| EgressGBUSD: 0.09, |
| + InterRegionGBUSD: 0.02, |
| Overheads: []LineItem{{Name: "domains + DNS", USDMonth: 5}}, |
| }, |
| Allocation: Allocation{Policy: "blend", CPUWeight: 0.5}, |
diff --git a/internal/usage/usage.go b/internal/usage/usage.go| index 72f6092f0..7a981a110 100644 |
| --- a/internal/usage/usage.go |
| +++ b/internal/usage/usage.go |
| @@ -23,12 +23,13 @@ const ( |
| MeterInstanceHours = "instance_hours" |
| MeterFleetBoxHoursServer = "fleet_box_hours_server" |
| MeterFleetBoxHoursDevice = "fleet_box_hours_device" |
| - // MeterBackhaulGB is RESERVED, not yet billed: the egress a request |
| - // spent crossing a region on a backhaul hop (EdgeRow.HopBytesOut). The |
| - // ruling that added the counters (2026-08-24) deliberately left the |
| - // pricing to Paul and shipped them dormant — no statement, no rate-card |
| - // line, bills byte-identical to before. The name is reserved here |
| - // because meter names are frozen once merged: reserving beats renaming. |
| + // MeterBackhaulGB is the hop SUBSET of MeterEgressGB (usage.EdgeRow's |
| + // HopBytesOut): bytes that left a home box for a region edge before |
| + // reaching the client. It rides in statements and the economics sweep |
| + // as data (#288) and is priced in NO account's cost, revenue or bill — |
| + // #284's ruling is absorb. A billing engine iterating meters generically |
| + // must skip it; PriceAccount names its meters and does. The platform's |
| + // own cost for it is the inter-region leg, RateCard.InterRegionGBUSD. |
| MeterBackhaulGB = "backhaul_gb" |
| // MeterBackhaulRequests1k is the request-count twin of MeterBackhaulGB: |
| // requests that crossed a region before being served. Informational, |
diff --git a/main.go b/main.go| index 7d7cd5dbf..2d7419a65 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -207,7 +207,8 @@ Your deployment — stand up and administer boxes, routes and accounts |
| revoke a capability, hibernate it, change its backing |
| instances with no sub-verb: list THIS box's registry routes |
| release-keygen mint the root signing key, or a scoped signing key |
| - economics record the month's AWS bill for the economics dashboard |
| + economics bill: record the month's AWS bill; backhaul: what region |
| + edges cost us this period |
| status -live: what every box last reported; -dry-run: leak check |
| On the host — what systemd runs; rarely typed by hand |