Skip to content

feat: share the spend ledger and error-body parsing - #9

Merged
agentx402-bot merged 3 commits into
mainfrom
feat/shared-spend-and-error-parsing
Jul 31, 2026
Merged

feat: share the spend ledger and error-body parsing#9
agentx402-bot merged 3 commits into
mainfrom
feat/shared-spend-and-error-parsing

Conversation

@agentx402-bot

Copy link
Copy Markdown
Contributor

Extracts the two things both service SDKs had duplicated — and had already drifted on — into the platform SDK that owns the types they depend on.

Why here

Core defined SpendCapError while the logic that throws it lived in both clients (5 throw sites each). A 2026-07-31 cross-repo audit found two holes of the same shape, each being "one client has the guard, the other doesn't":

  • one checked its cumulative cap against a counter incremented only after the paid round-trip, so concurrent ops all passed the same stale check and all signed — measured, three in-flight reads authorized $0.012 against a $0.005 cap;
  • the other accepted a malformed cap without failing closed.

They stayed divergent partly because the same function had a different name in each repo (assertCapOption vs assertFiniteUsd), so grepping one never finds the other. The error-body parser drifted again during that remediation: hardening landed in one client and not the sibling.

What

  • SpendLedger — per-call and cumulative bounds, with the synchronous reservation that makes the cumulative one hold under concurrency, plus an idempotent release. Caps validated at construction.
  • assertFiniteUsd — a malformed cap fails closed. The dangerous case is NaN: usd > NaN is false, so an unchecked NaN cap doesn't merely fail to bind, it silently disables the comparison while still reading as configured.
  • parseErrorBody — type-checks every field of an untrusted response body. JSON.parse returns any and a cast asserts nothing at runtime; a non-string code silently breaks every e.code === "…" comparison callers dispatch on. Only the parsing is shared — each SDK still constructs its own error class, since that identity is what callers instanceof.

All additive. No existing export changes.

A bug found while extracting

Writing the tests surfaced a latent defect that exists in both clients today: cumulative spend is accumulated USD floats, so $0.005 settled plus a $0.004 op is 0.009000000000000001, and a bare <= refuses a spend landing exactly on a $0.009 cap. SpendLedger absorbs that with a sub-atomic slack — one atomic USDC, the same magnitude and reasoning the SDKs already apply to a quoted price — so it cannot admit a spend anyone could actually be charged. Three regression tests pin it, including that ten times the slack is still refused.

Adoption

Nothing consumes this yet. The clients can only adopt after a core release, so this ships first and the two client PRs follow — deliberately, so the extraction can be reviewed on its own rather than tangled with two migrations.

Gate: biome ci clean, tsc --noEmit clean, 144 tests passing.

Both service SDKs carried their own copy of each, under different names for the same function, and both drifted — the cross-repo audit that motivated this found a concurrency hole in one client's cumulative cap and a missing fail-closed check in the other, and the error parser drifted again during that very remediation. Core already owned SpendCapError while the logic that throws it lived downstream.

SpendLedger carries the per-call and cumulative bounds plus the synchronous reservation that makes the cumulative one hold under concurrency; assertFiniteUsd makes a malformed cap fail closed; parseErrorBody type-checks an untrusted response body, sharing the parsing while each SDK keeps its own error class.

Writing the tests surfaced a latent defect present in both clients: cumulative spend accumulates float error, so a spend landing exactly on a cap was refused. Absorbed with a sub-atomic slack, matching the per-op price slack the SDKs already apply, and pinned by regressions that also prove ten times the slack is still refused.

Additive only; nothing consumes it until a release lets the clients adopt.
Minor bump: the new exports are additive and no existing export changes, so a consumer floating ^0.2.x is unaffected until it opts in. Documents the float-accumulation fix, which reaches the service SDKs as they adopt.
maxSpendUsd bounds what a single operation may cost; pre-buying $20 of credit is not a $20 operation, so a deposit or top-off has to be able to opt out of that one bound. The cumulative cap still applies — real money still leaves the wallet — so this narrows one bound, never both. Without it the ledger could not cover a service SDK that sells prepaid credit, which is the shape the agentkv client already needs.
@agentx402-bot
agentx402-bot merged commit 1b3240c into main Jul 31, 2026
3 checks passed
@agentx402-bot
agentx402-bot deleted the feat/shared-spend-and-error-parsing branch July 31, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant