Skip to content

fix(gateway): Check key spend limits before a video starts - #1267

Merged
kotevcode merged 24 commits into
mainfrom
fix/video-budget-prechecks
Oct 18, 2026
Merged

kotevcode merged 24 commits into
mainfrom
fix/video-budget-prechecks

Conversation

@kotevcode

@kotevcode kotevcode commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

Video requests through the gateway could fail with 402 spend_limit_reached on a key that had spent nothing: every video create and status poll held the per-request reserve ($0.30) for 60 s after it was answered, although only the delivery is charged. Fixing that properly means the gateway has to account for a video job over its whole life, so this PR makes video jobs count against key/member/workspace caps exactly once, from start to charge, and closes the edge cases around it (found by #1201 smoke testing, review rounds, and an adversarial audit).

Gateway (apps/cli/src/gateway)

  • A capped video create is only sent to a buyer that announces video-budget on its spend feed (read fresh per create); an older buyer would ignore the allowance, so such creates answer 503 buyer_upgrade_required. Uncapped keys are unaffected. Capped creates are not held for a restarting buyer (it may come back older).
  • A request that only misses a cap because a create is still being priced gets retryable 429 video_pricing_in_progress instead of 402.
  • A create sends the buyer the room left under the caps (x-antseed-max-video-usdc) and holds that room (≤ payments.maxVideoRequestUsdc, default $5) until priced, so overlapping creates cannot share it.
  • An accepted job holds its price (buyer's x-antseed-video-price-usdc, strictly parsed, clamped to the allowance; whole allowance if missing/invalid) in a persisted video_commitments table (migration 10). A capped create stores a conservative reservation before it is sent (503 if it cannot), swapped for the job's commitment in one transaction; at startup a leftover reservation becomes its job's commitment when the request log names the job, and is dropped otherwise (no key could retrieve that job). The job ID is parsed from the buyer's answer before, and independently of, request-log writes. Polls and downloads of the job are admitted without counting it twice and hold nothing after they answer.
  • The price stops counting only on buyer-authoritative signals: the buyer's charge for the job (lowered by each charge's amount; an opening fee the create already paid is subtracted once, whichever arrives first), the buyer-only x-antseed-video-job-retired marker, 1 h after a download the buyer accepted (x-antseed-video-delivered) with no charge, or 30 days. Revoked keys' jobs stop counting once no admitted retrieve or pending delivery charge can charge them; uncapped keys record the price so later caps count it. Nothing a seller sends can release or inflate it.
  • Only the key that created a job may retrieve it (404 otherwise), so a key is never charged for another key's video; a colliding job ID never overwrites another key's commitment. Unattributed spend (no request ID) never lowers a commitment.
  • Accounting DB writes cannot fail a request (logged); a broken buyer stream now closes the client connection.

Buyer (buyer-core, apps/cli/src/proxy)

  • Refuses a video priced above the caller's cap before the job starts; reports the accepted price.
  • Marks a download delivered only once the job's delivery is recorded and its stored job is cleared; if the clear fails, the seller is still paid but the download is cut off as unrecorded (job kept, not retired) until the clear is saved (the job is then no longer chargeable), using the job's own billing facts rather than the evictable request cache.
  • Strips seller copies (any case) of the buyer-only headers; pays a video exactly its price (no 1.4x token tolerance).
  • Retires a job that reports failed or that the seller no longer has (404/410), durably before answering; retrieves of one job are serialized; a failed save answers retryable unknown.
  • Does not report unknown jobs as retired when its saved routes could not be read at start.
  • Deposits: unchanged, already checked against the video price before the one-off channel opens.

Release Notes

  • CLI gateway: video jobs no longer use up a key's spend limit while they wait, a video costing more than the key has left is refused before it starts, and accepted videos stay reserved until they are charged; only the key that created a video can download it.
  • CLI buyer: failed or expired video jobs are retired so they can never be charged, and videos are paid exactly their price.

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Chore (maintenance tasks, refactoring, or non-functional changes)

Checklist

  • My code follows the code style of this project
  • I have added the necessary documentation (CHANGELOG entry; gateway budgets, troubleshooting and buyer config docs)
  • I have added tests: overlapping creates, two creates against one cap, forged/missing price header, partial charges, delivered-never-charged, failed/gone/unknown jobs, malformed polls, broken-off downloads, ownership, revoked keys, restart persistence, configured video limit, uncapped-then-capped keys, buyer retirement races and save failures, exact video pricing, opening fees in both orders, delivery after request-cache eviction, commitment lost before restart, request-log failure on create, reservation write failure, reservation left by a crash, job-ID collision, untagged spend, older buyer without the capability, capped create during a buyer restart, pricing-in-progress 429, free video polls at the cap, recovery after a charged delivery, unreadable buyer routes, revocation during a download, failed job removal on delivery (seller still paid, job kept)
  • Lint and unit tests pass locally with my changes (cli 1356 pass / 1 skipped, node 1394, buyer-core 45, typecheck clean)

Video creates and status polls no longer keep a hold on the key's caps once answered; only the retrieve that delivers the file is charged. The gateway passes the room left under the key's caps to the buyer, which refuses a video priced above it before any job starts.
@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 01195710d6)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed the P1 in 9f09274: an accepted video's price now stays reserved against the key/member/workspace caps until delivery (moved onto the delivery's hold until its charge settles), job failure, or 24 h. The buyer reports the price on the accepted create (x-antseed-video-price-usdc). A job's own polls and download are admitted without its own commitment. Regression tests: two creates against one cap (second is refused by the buyer with the reduced cap), failed job releases, delivered job hands over to the request hold.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 9f09274358)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Both P1s addressed in the latest commit:

  • Overlapping creates: a video create now holds the whole allowance it is sent (capped at the buyer's $5 per-video default) until the buyer reports the price, then that hold is replaced by the exact commitment. A second create in flight sees the reduced room (test: two overlapping creates against a $1 cap → 200 + 402, only one allowance sent). Member/workspace caps use the same holds map, so they are covered by the same reservation.
  • Interrupted downloads: delivery is recorded only when the upstream MP4 stream ends; an upstream error or client close keeps the job's commitment (test: a download cut mid-file leaves the commitment in place). The gateway also now destroys the client response when the buyer's stream breaks, instead of leaving it open.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 4f26b16dc8)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed the P1: a poll error no longer releases a job's commitment by HTTP status. It is released only for an authoritative terminal answer: a 200 status with status: failed, or a 404 whose error.code is video_route_not_found (the buyer has no record of the job). Any other error (malformed poll, invalid required-parameters header, seller unreachable) keeps the commitment until delivery or the 24 h expiry. Regression test: a malformed poll keeps the $0.60 commitment and a second $0.60 create is still refused; an unknown-job 404 releases it.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed e069f72f3e)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed both:

  • P1 restart: commitments are now stored in a video_commitments table (gateway DB migration 10: job, key/member/workspace, amount, expiry) and reloaded when GatewayAccounting is constructed; commit, delivery hand-over, drop and expiry write through. Test: a $0.60 commitment survives a new GatewayAccounting on the same store and a following create is told only $0.40; dropping it removes the row.
  • P2 hardcoded $5: the create reservation is capped by maxVideoRequestUsdc from the same CLI config the buyer runs with (new payments.maxVideoRequestUsdc field; the CLI buyer now passes it to the node too, default $5). Test covers default, a $10 limit and a limit above the key's room.

@reviewant

Copy link
Copy Markdown
Contributor

@kotevcode the review run failed (RuntimeError); see the agent logs.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed f5e2d71e67)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed the P2: the commitment lifetime now matches the buyer's job lifetime (30 days, same as the resource-route TTL and VIDEO_JOB_TTL_MS), so the gateway never forgets a job the buyer can still deliver and charge. Clock-controlled test with a total cap: a $0.60 job is still counted after 29 days (a new create is told $0.40) and released only after 30.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 182d4cad29)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed the P2 by making a failed status authoritative in the buyer: when a retrieve's normalized answer is failed (a seller FAILED/ERROR status, or a JSON completed with no video → video_not_delivered), the buyer proxy retires the job's resource route and persists that. Every later retrieve is answered 404 video_route_not_found without reaching the seller, so the job can never be delivered or charged — which is what makes the gateway's release of the commitment on failed safe. Regression test (buyer proxy): create → failed (and completed-without-video) poll → next retrieve is 404 and the seller is not called again.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 2f5666eb35)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Addressed both P2s in the buyer proxy:

  • Concurrent retrieves: retrieves of one video job now run one at a time (per-job queue), and each re-checks the job route after taking its turn, so a retrieve queued behind one that retired the job answers 404 video_route_not_found without reaching the seller. Test: two overlapping retrieves, the first reports failed → the second is 404 and the seller is called once.
  • Durability: downloadVideo now calls a retire hook before writing a failed answer; the route is only treated as retired once _persistResourceRoutes succeeds. If the write fails, the route is restored in memory and the poll answers a retryable unknown (so the gateway keeps the commitment), and a later poll retires it once the write works. Test: rejected write → unknown/retryable and the job is still polled; next poll with a working write → failed, then 404.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 1e45916476)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Since round 8 the video lifecycle was redesigned after an internal adversarial audit (see the updated PR description): commitments are released only on buyer-authoritative signals (the buyer's charge, lowered by each charge's amount; the buyer-only x-antseed-video-job-retired marker, which the buyer sets only after durably retiring a failed/gone job; 1 h after a complete download with no charge; 30 days). The buyer strips seller copies of its headers and pays videos exactly their price (no 1.4x tolerance). Only the creating key may retrieve a job. The round-8 finding (a retrieve during a pending retirement save) is fixed: the route stays routable in memory until the save succeeds and is only excluded from the saved snapshot meanwhile.

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Head is now 2df69b7: in addition to the previous comment, a job that still holds its price is always admitted for its own polls and download (hold 0), even when unrelated spend has filled the caps since — its commitment already covers its delivery charge — so a committed job can always finish or be retired.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 8f1013082c)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

Head ae5c47e addresses round 9: (P1) commitment storage now fails closed — a failed save is kept and retried, a failed startup load is retried, and until both succeed requests of capped keys answer 503 spend_tracking_unavailable instead of being admitted against commitments a restart could lose (deletes that fail only over-count); (P2) a free video create reports x-antseed-video-price-usdc: 0 (free-service creates, and zero-priced unit models via the negotiator), so it holds nothing.

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 2df69b7e5a)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed ae5c47e614)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 66204429f2)

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed b820c81fc8)

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed b820c81fc8)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 2c58305ac4)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed 48ad74316b)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

reviewant commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1267 (comment) (previously reviewed e7c7161133)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@reviewant re-review

@reviewant

Copy link
Copy Markdown
Contributor

Antseed Review Summary

The PR adds video-price prechecks against gateway key, member, and workspace spending limits, then persists accepted-job commitments until the corresponding spend arrives or the job is retired. It also adds buyer capability detection, ownership checks, serialized video retrieval, and delivery-state handling so failed persistence or interrupted downloads do not prematurely release reserved budgets. Configuration, documentation, and regression tests accompany these changes.

  • No issues found in the reviewed changes.

Confidence Score: 5/5

No verified merge-blocking issues found in the reviewed paths.

The buyer-core suite passed all 45 tests; targeted node payment-manager and video-reserve-flow tests passed all 97 tests; the full CLI suite passed 1,338 tests with one skipped and no failures. Reviewed paths included budget admission and commitment recovery, caller-provided video caps, buyer-only response headers, serialized retirement, and delivery persistence. Review coverage is incomplete: not every changed test or surrounding core caller was read in full, and live seller interoperability and crash timing were not independently exercised beyond the existing tests; this is not a claim of exhaustive verification.

Files that need attention:

Important Files Changed

Filename Overview
apps/cli/src/gateway/accounting.ts Adds capped video admission, persistent commitments, recovery, and spend reconciliation; no concrete defect was established in the reviewed accounting paths.
apps/cli/src/gateway/server.ts Enforces buyer capability and job ownership, forwards video allowances, and records acceptance and delivery outcomes; reviewed regions revealed no verified issue.
apps/cli/src/gateway/store.ts Adds transactional commitment storage and request-history queries used for recovery and ownership.
apps/cli/src/gateway/store-migrations.ts Appends a video-commitments table migration without changing the existing SQL migration entries.
apps/cli/src/proxy/buyer-proxy.ts Passes local video caps to the buyer and serializes retrieval and durable retirement of each job.
apps/cli/src/proxy/video-download.ts Distinguishes delivered, unrecorded, failed, and unavailable jobs while terminating streams the buyer did not accept.
packages/buyer-core/src/buyer-payment-negotiator.ts Checks estimated video cost against the caller's remaining allowance before initiating the paid request.
packages/buyer-core/src/buyer-request-handler.ts Strips seller-controlled copies of buyer-only headers and reports buyer-calculated prices and recorded delivery outcomes.
packages/buyer-core/src/buyer-payment-manager.ts Tightens video authorization bounds and tracks delivery persistence failures; targeted payment tests passed.
CHANGELOG.md Documents the user-facing budget enforcement, compatibility behavior, persistence, and download changes.

Diagram

sequenceDiagram
    participant Client
    participant Gateway
    participant Store
    participant Buyer
    participant Seller
    Client->>Gateway: Create video
    Gateway->>Store: Persist budget reservation
    Gateway->>Buyer: Create with remaining allowance
    Buyer->>Buyer: Validate advertised video price
    Buyer->>Seller: Submit accepted request
    Seller-->>Buyer: Job identifier
    Buyer-->>Gateway: Job identifier and buyer calculated price
    Gateway->>Store: Replace reservation with commitment
    Gateway-->>Client: Accepted job
    Client->>Gateway: Retrieve owned job
    Gateway->>Buyer: Retrieve video
    Buyer->>Buyer: Serialize retrieval for this job
    Buyer->>Seller: Request status or video
    alt Accepted delivery
        Seller-->>Buyer: Video stream
        Buyer->>Buyer: Record delivery and persistence outcome
        Buyer-->>Gateway: Complete accepted stream
        Buyer-->>Gateway: Attributed spend
        Gateway->>Store: Record spend and reduce commitment
    else Terminal failure
        Seller-->>Buyer: Failed or missing job
        Buyer->>Buyer: Persist route retirement
        Buyer-->>Gateway: Retired job marker
        Gateway->>Store: Remove commitment
    end
Loading

Core files read in full: apps/cli/src/gateway/accounting.ts, apps/cli/src/gateway/video-jobs.ts, apps/cli/src/proxy/resource-routes.ts, apps/cli/src/proxy/video-download.ts

Reviews (18): Last reviewed commit: fix(gateway): Check key spend limits before a video starts (648efe72f3) | requested by @kotevcode | Tag @reviewant with "re-review" to run again

@kotevcode
kotevcode merged commit 4026643 into main Oct 18, 2026
5 checks passed
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.

2 participants