Skip to content

fix(spend): book $0 for any paid call the gateway did not settle - #89

Merged
VickyXAI merged 2 commits into
mainfrom
fix/settlement-skipped-books-zero
Oct 27, 2026
Merged

VickyXAI merged 2 commits into
mainfrom
fix/settlement-skipped-books-zero

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Problem

The gateways now serve some paid requests without settling them, and say so in headers:

Route Headers
sol /api/v1/pm/markets/search, empty result X-Settlement-Skipped: empty-result, X-Payment-Waived: empty-result, X-Payment-Settled: false, no PAYMENT-RESPONSE
sol /api/v1/rpc/*, JSON-RPC never executed X-Settlement-Skipped: unexecuted, X-Payment-Settled: false
chat, free fallback served it X-Settlement-Skipped: free-fallback
blockrun.ai /v1/pm/*, empty result X-Payment-Waived: empty-result only

settlementSkipped() (src/receipt.ts) only matched free-fallback, and only the chat paths asked it. Every other booking site (LLMClient raw GET/POST, both chat streams, all of SolanaLLMClient's raw, stream, batch and media paths, BlockrunClient, and the Video/Voice/Speech/Music/Rpc/Portrait/Phone clients) added the full signed amount on any 2xx. So getSpending() and cost_log.jsonl overstated spend for calls that moved no money.

Change

  • settlementSkipReason(response) (new) returns the trimmed X-Settlement-Skipped value, else the X-Payment-Waived value, else undefined. settlementSkipped() is true when either one names a reason. Empty or blank values don't count, so a call is booked unless the gateway names why it wasn't charged. X-Payment-Settled: false on its own is not a skip: settlement just hadn't landed yet, and the payment is still owed.
  • Each booking site now checks it before booking:
    • LLMClient: a new bookPaidRaw() handles all 4 raw GET/POST sites (first send and the 502/503 resend). The 2 stream sites (pre-auth hit and 402→sign) are guarded, and bookPaid (chat) is unchanged.
    • SolanaLLMClient: recordSettlement(costUsd, response) now takes the response, so none of its 7 callers can skip the check (exact chat, batch, stream, raw 200, raw 202 image/music, GET raw, video delivery).
    • BlockrunClient.recordSpending(response) covers get/post, stream and both poll exits. A skip drops the pending cost.
    • VideoClient, VoiceClient, SpeechClient, MusicClient, RpcClient, PortraitClient and PhoneClient each have their one booking site guarded.
  • A skipped call adds nothing to calls or totalUsd and writes no cost-log entry. This matches the existing free-fallback behaviour.
  • Settlement.skipped now carries the gateway's reason ("free-fallback" | (string & {})). For free-fallback the output is identical to before.
  • No assertPaid change was needed. Neither client throws or warns when PAYMENT-RESPONSE is missing on a 2xx. settlementSkipReason returns undefined for a response-like object with no headers, so a custom fetch can't turn a paid call into a throw after payment.
  • Out of scope: ImageClient never books spend at all (it was already like this), so there is nothing to gate there.

Tests

  • test/unit/settlement-skipped-booking.test.ts covers 20 EVM booking sites, each driven through its public method against a faked fetch with the real EVM signer. Each site runs 5 skipped header shapes (sol pm, sol rpc, free-fallback, X-Payment-Waived alone, and an unknown future reason), which must book $0, write no cost log and not throw. It also runs 3 booked shapes (settled, X-Payment-Settled: false alone, empty/blank skip headers), which must book the full quote. There are predicate unit tests and chat settlement.skipped reason tests too.
  • test/unit/settlement-skipped-solana.test.ts covers the 7 SolanaLLMClient sites with 4 skipped and 2 booked shapes each.

The tests catch the bug:

  • Reverting every booking site (with the new predicate kept): 114 failed. Every unguarded site fails on every skipped shape. Zero booked cases fail.
  • Reverting only the predicate to === "free-fallback": 107 failed. Every site fails on every non-free-fallback skipped shape. No free-fallback or booked case fails.
  • After restoring the source, git diff shows only the intended files.

Results

  • pnpm run test --run: 45 files passed, 1 skipped; 946 tests passed, 12 skipped (958), no type errors. The baseline on main was 733 passed and 12 skipped, so 213 tests are new.
  • pnpm run typecheck: clean (exit 0).
  • pnpm run lint: clean (exit 0). The new test files also pass eslint.
  • pnpm run test:package: PASS.
  • sync-brand-numbers --check: up to date.

No version bump. The fix is added to CHANGELOG under [Unreleased], following the repo's fix-PR convention.

🤖 Generated with Claude Code

1bcMax and others added 2 commits October 27, 2026 00:01
The gateways now serve some paid requests without settling and say so:
X-Settlement-Skipped: empty-result (sol pm empty search), unexecuted (sol
rpc), free-fallback (chat), and X-Payment-Waived alone (blockrun.ai pm).
settlementSkipped() only knew free-fallback and only the chat path asked it,
so getSpending() and the cost log booked the full signed amount for calls
that moved no money.

settlementSkipped() is now true for any non-empty X-Settlement-Skipped or
X-Payment-Waived, and every booking site goes through it: LLMClient raw
GET/POST (bookPaidRaw), chat, both stream paths; SolanaLLMClient's
recordSettlement now takes the response (exact, batch, stream, raw, 202
media, video delivery); BlockrunClient.recordSpending; Video, Voice,
Speech, Music, Rpc, Portrait and Phone clients. settlement.skipped carries
the gateway's reason. X-Payment-Settled: false alone is still booked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@VickyXAI
VickyXAI merged commit fed16d7 into main Oct 27, 2026
3 checks passed
VickyXAI pushed a commit that referenced this pull request Oct 27, 2026
…Client costs per call

Follow-ups from the review of #89:

- settlementSkipReason(): a response that names a skipped settlement but also
  proves it settled (successful PAYMENT-RESPONSE with a transaction or a
  positive charge, or an X-Payment-Receipt hash) is not skipped; the conflict
  goes to stderr. blockrun.ai's free-fallback receipt has an empty transaction
  and no amount (chat/completions/route.ts), so today's skips still book $0.
- Header values false / 0 / no / none are not a skip reason.
- BlockrunClient: signFrom402() returns the signed cost with the payload and
  recordSpending() books that, replacing the per-client pendingCostUsd that a
  skipped call could zero under a concurrent call.

Tests: four booked shapes and one skipped shape (free-fallback as sent) across
every EVM site, a receipt-wins shape on every Solana site, predicate and
stderr tests, and a concurrent BlockrunClient test. 87 fail on main.
VickyXAI added a commit that referenced this pull request Oct 27, 2026
…Client costs per call (#92)

Follow-ups from the review of #89:

- settlementSkipReason(): a response that names a skipped settlement but also
  proves it settled (successful PAYMENT-RESPONSE with a transaction or a
  positive charge, or an X-Payment-Receipt hash) is not skipped; the conflict
  goes to stderr. blockrun.ai's free-fallback receipt has an empty transaction
  and no amount (chat/completions/route.ts), so today's skips still book $0.
- Header values false / 0 / no / none are not a skip reason.
- BlockrunClient: signFrom402() returns the signed cost with the payload and
  recordSpending() books that, replacing the per-client pendingCostUsd that a
  skipped call could zero under a concurrent call.

Tests: four booked shapes and one skipped shape (free-fallback as sent) across
every EVM site, a receipt-wins shape on every Solana site, predicate and
stderr tests, and a concurrent BlockrunClient test. 87 fail on main.

Co-authored-by: 1bcMax <viewitter@gmail.com>
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