Repository navigation
Open
Labels
app/idPackage/app: id (pkgs/ or apps/ in this repo)Package/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insLarge: multi-day or open-ended; expect staged check-inskind/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)Ordinary priority (the default)source/creatorFiled by the new-issue-creator jobFiled by the new-issue-creator jobstatus/triagedTriaged and routed (see agent note)Triaged and routed (see agent note)task/triageQueue packet kind: verify, reproduce, and route this itemQueue packet kind: verify, reproduce, and route this item
Description
Follow-up from the #2065 design (doc/2026-10-29T12-10-00Z_design_refundable_grants_pending_transfers/, PR #2293). Buildable once the control-plane/gateway shape in #2062 is settled.
Scope
- Control-plane
granttable exactly as §2.3 of the design: id, steam_key_hash, user_id, ledger, amount, state, pending_transfer_id, resolution_transfer_id, window_started_at/ends_at, attempt counters. - Endpoints (§2.4):
POST /grants(idempotent on steam_key_hash),POST /grants/{id}/vest,POST /grants/{id}/void,GET /grants/{id},GET /users/{id}/grants. - TigerBeetle side: one
flags.pendingtransfer per key fromissuer.grant_poolto the holder account, dedicated grantcoderange,user_data_128= grant id,user_data_64= key hash; vest viapost_pending_transfer+pending_id; refund viavoid_pending_transfer. - Partial refunds: void-and-reissue (§4) — TigerBeetle's void is all-or-nothing, so a partial refund voids the hold and issues a new pending transfer for the remainder, linked by
correction_of. - Treat
pending_transfer_already_posted/_voided/_expiredas idempotent outcomes (409 at the API, success internally).
Not in scope
Vesting automation and drift detection — that is the reconciler issue. The refund-window value comes from the decision issue; use it as a config, never hard-coded.
Done when
Register → vest and register → void both work against a dev TigerBeetle cluster with targeted tests, double-resolution returns 409, and a partial refund leaves no posted credit behind.
Acceptance criteria
- Grant table matches design §2.3 exactly (fields incl. pending/resolution transfer ids, window bounds, attempt counters)
- Endpoints live: POST /grants (idempotent on steam_key_hash), vest, void, GET one, GET per-user
- Register → vest and register → void both pass against a dev TigerBeetle cluster with targeted tests
- Double-resolution returns 409; already-posted/voided/expired treated as idempotent outcomes internally
- Partial refund via void-and-reissue leaves no posted credit behind; refund-window value comes from config, never hard-coded
Metadata
Metadata
Assignees
Labels
app/idPackage/app: id (pkgs/ or apps/ in this repo)Package/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insLarge: multi-day or open-ended; expect staged check-inskind/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)Ordinary priority (the default)source/creatorFiled by the new-issue-creator jobFiled by the new-issue-creator jobstatus/triagedTriaged and routed (see agent note)Triaged and routed (see agent note)task/triageQueue packet kind: verify, reproduce, and route this itemQueue packet kind: verify, reproduce, and route this item
Activity
developing-today commentedon Oct 29, 2026
CODE-1150
dezren39 commentedon Oct 29, 2026
Intake review
Related: #2065 design (PR #2293, §2.3/§2.4/§4), #2062 (control-plane shape — buildable once settled), the refund-window decision issue #2296, epic #2056
Notes: Foundational build for the refundable-grants family; #2295 (reconciler) depends on this table.
Risks: TigerBeetle void is all-or-nothing — partial-refund correctness lives entirely in the void-and-reissue path; test it hardest.
Suggested approach: Settle #2062 shape first, then table + endpoints, then the partial-refund tests.
🤖 Agent note · filed
meta/enrichedapplied. Still-live check this pass: no merged PR or fix commit explicitly resolving this issue was found; where a queue triage task owns verification, it is named in the notes.dezren39 commentedon Nov 3, 2026
🤖 Agent note · done — triage verdict: CONFIRMED, ORDERED (buildable design; two sibling dependencies + one Drew decision ahead of it)
Verified fresh (2026-11-03)
mainatdoc/2026-10-29T12-10-00Z_design_refundable_grants_pending_transfers/, with the later grant-lifecycle spec atdoc/2026-10-30T11-55-00Z_design_grant_lifecycle.md. The "buildable once id: design: currency control plane — registry, account factory, gateway #2062 is settled" precondition in the body is met.pkgs/idcontains no control-plane service, grant table, or gateway implementation — only the PoC atpkgs/id/src/currency_poc/ledger.rs. This is a greenfield build on the id: design: currency control plane — registry, account factory, gateway #2062/id: design: refundable grants via pending transfers — Steam premium tokens voidable through the refund window #2065 designs; theeffort/llabel stands.Blockers / ordering (recorded per the acceptance)
user/review,priority/high, Drew's): the window length (design recommends 14 days, Steam's window) and whether pending tokens are spendable are deliberately NOT to be defaulted in code — id: decide: grant refund window length and whether pending tokens are spendable #2296 was filed "so it is decided deliberately rather than defaulted in code". The build can plumb the value as config without the decision, but it must not ship a hard-coded or silently-chosen default; the decision gates calling this done.Verdict
Still live, correctly scoped, not yet startable as a clean single build until #2292 lands (or a dev-cluster substitute is declared) and #2296 is decided. No packet defect; routing stays a build task at
effort/lonce the ordering clears.developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
🤖 Agent note · triaged (task 8609, lane code-k, direct)
Triage verdict — confirms task 7870's ordering (15:23Z today); both blockers still stand, one has hardened.
Re-verified fresh (2026-11-03T20:45Z):
pkgs/idat current main — this remains a greenfield build on the settled id: design: currency control plane — registry, account factory, gateway #2062/id: design: refundable grants via pending transfers — Steam premium tokens voidable through the refund window #2065 designs,effort/lstands.status/blocked+queue/blocked+user/escalated. id: build: grant service — pending-transfer grants (register/vest/void) with control-plane grant records #2294's acceptance requires register → vest / register → void against a dev TigerBeetle cluster, so this still gates a clean build (or the build must declare its own throwaway dev cluster, per task 7870's note).user/review,priority/high: the window length (design recommends 14 days, Steam's window) and whether pending tokens are spendable. The build may plumb the value as config, but id: build: grant service — pending-transfer grants (register/vest/void) with control-plane grant records #2294 cannot be called done until this is decided — it must not ship a hard-coded or silently-chosen default. The decision lives on id: decide: grant refund window length and whether pending tokens are spendable #2296; it is not re-escalated from here.Ordering unchanged: #2292 lands (or a dev-cluster substitute is declared) + #2296 decided → #2294 is startable as a single
effort/lbuild exactly as scoped. No new packet from this triage; the blockers are live, tracked, and labelled.Issue stays OPEN. Status: triaged.