Repository navigation
fix(credits): one baseline for every stream credit, and totals only on funded entries - #28
Conversation
…revent double counting
…to ensure accurate accounting
… and return write status
…estamp instead of creation timestamp
…improve message clarity in API response
…edit calculation logic
There was a problem hiding this comment.
🟡 Changes recommended
Closed intervals, missed rate changes, root aggregation, and concurrent total updates remain incorrect.
5 open findings
Serialize GoodID root updates to prevent lost accumulations · New Credit closed stream intervals before clearing zero rates · New Authorize based on the actual funded amount · New Prevent child flow rates from clobbering GoodID root aggregates · New Update guide to document gdAmountWei accounting · New
What changed in this PR
Fixes Worker stream-credit accounting and funded-total tracking without changing the payment boundary.
Changes:
- Unifies stream-credit baselines and synchronizes closed-stream rates.
- Moves lifetime totals to successful funding completion.
- Adds regression tests and architecture documentation.
| File | Description |
|---|---|
backend/src/worker.ts |
Revises stream fetching, timing, event, and cron logic. |
backend/src/kv-credit-store.ts |
Updates flow-rate and funded-total persistence. |
backend/test/worker.test.ts |
Adds stream-accounting regressions. |
backend/test/kv-credit-store.test.ts |
Tests terminal funding and rate persistence. |
docs/ARCHITECTURE.md |
Documents revised accounting behavior. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| totalGdDepositedWei: credited ? addDecimalStrings(current.totalGdDepositedWei, entry.gdAmountWei) : current.totalGdDepositedWei, | ||
| totalGDStreamedWei: | ||
| credited && entry.source.startsWith("stream") ? addDecimalStrings(current.totalGDStreamedWei, entry.gdAmountWei) : current.totalGDStreamedWei, |
There was a problem hiding this comment.
@sirpy
Valid. This race already exists on main for the USD totals;
The proper fix is to split per-wallet profiles from the identity aggregate.
I do not fix this in the pr
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@copilot |
Reviewing the available tree, I found three remaining accounting issues:
The checkout is shallow (no base ancestry), so this review is against the available current tree rather than a complete ancestry-based PR diff. |
… and preserving rates for first-time streamers
…ndow over-crediting



Description
Account$730.54 stuck as outstanding, and an 8,900 G$ /mo stream rate for streams that closed on 2026-09-17. Superfluid says 7,134.63 G$ ever streamed to the vault from that account; the backend funded 19,283.45 G$ against it
0x2CeADe86…0627reports 6,229,114 G$ deposited against a $4.25 credit balance,1. The credit window was unbounded
elapsedSecondscame fromlastStreamCreditAt || stream.lastUpdateAtwith no validation. With the profile defaulting that timestamp to1970-01-01, "time since the last credit" became "time since 1970" — 56.5 years in one entry:99.4% of the account's reported total, from a single entry whose funding reverted.
Now: the window starts at the later of
lastStreamCreditAtand the stream'supdatedAtTimestamp. That moves forward on every create, rate change and close, so a stale clock is unreachable — a dormant gap between a closed stream and its replacement is never billed, and a brief stream cannot be charged across weeks. The window itself is not capped: a long gap means the stream really did flow, and a stalled cron should still pay out.Because
updatedAtTimestampis the last on-chain flow change, the rate is provably constant across the window — sorate × windowis exact, not an estimate.2. Totals were credited before funding landed
G$ totals were added in
recordGdCreditwhile USD totals moved only inmarkFundingResulton success, so a reverted funding inflated the G$ counters permanently and never releasedtotalOutstandingFundingUsd. Now: all lifetime totals move together, only on funded entries; outstanding is released on either terminal state. This is what produced the 6.2M-vs-$4.25 divergence.3.
StreamUpdateddouble-countedIt credited
event.totalFlowWei, measured from the last on-chain flow change, while the scheduled run measures fromlastStreamCreditAt— overlapping windows:It also advanced the clock without paying for the window: on 2026-08-28 it credited 0.18 G$ while moving the clock past 3.39 days, dropping 1,005 G$. Now: the event credits the window actually owed, from the same baseline as the scheduled run.
4. Closed streams were invisible, and their final interval unpaid
The scheduled run filtered
currentFlowRate_gt: "0", so a closed stream vanished and was never revisited — its rate was never cleared (the only writer of0was a push-only event, andinput.flowRate ? …discarded0nas falsy), and the interval between the last credit and the close was never paid.Now: closed streams are fetched, rates sync every run (not only when a credit clears the cooldown and the 4,000 G$ minimum — a fortnight apart at 8,900 G$/mo), summed per account so a closed revision can't clobber its live replacement, and the final interval is credited in both the scheduled run and
POST /stream-credits.5. Root profiles held the wrong flow rate
Totals reach a GoodID root by mirroring in
updateUser, correct because they accumulate. A flow rate is absolute, so mirroring left the root holding whichever sub-account wrote last. Now: excluded from the mirror and summed onto the root.This PR still reconstructs amounts rather than reading them
Every remaining limitation below has one cause: the credit amount is computed as
flowRate × windowinstead of being read from Superfluid.The subgraph already holds the exact figures, and this PR does not use them.
streamPeriodsreturns one row per span of constant flow rate, with exact start, stop, rate and amount:Summing
flowRate × overlapacross the periods after the last credit is exact — the rate is constant within a period by definition — and removes the floor, the remembered rate, any need for a ceiling, and the live/closed branching entirely.analytics.ts:546already queries this entity; the credit path never adopted it. Deliberately left as a follow-up to keep this PR to the defects above.Consequences carried in the meantime:
The final interval has one chance to be paid. The scheduled run captures the pre-close rate before zeroing it, so only the first run after a close can price that window. If that run is blocked by the cooldown or the 4,000 G$ minimum, the next sees rate
0and the interval is lost.POST /stream-creditscannot recover it, since it reads the already-zeroed rate — correct when the cron credited, a dead end when it didn't. Final intervals are often short, so this will frequently not pay out. Under-credit, not loss of funds.The closed-stream branch can over-credit. It prices
lastCredit → terminationat one remembered rate, with no floor (a closed stream's last update is its termination) and no ceiling. If the rate dropped and the change wasn't ingested, the remembered rate is the older, higher one. AstreamedUntilUpdatedAtcap was tried and removed: it bounds the stream's lifetime total rather than what's owed, so it guards the wrong quantity and usually doesn't bind — false assurance was worse than none.Rate changes under-credit when the event is missed.
updatedAtTimestamprecords only the most recent change, so the window before it cannot be priced and is skipped./v1/celo/events/recordis push-only with no retry. Deliberate — errs towards under-crediting.Not included
Existing KV profiles are already corrupt and are not repaired here; the account above keeps reporting 6.2M G$ until its stored totals are rewritten.
About #27
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
Checklist: