Skip to content

id: build: grant service — pending-transfer grants (register/vest/void) with control-plane grant records #2294

Open

Description

@dezren39

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 grant table 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.pending transfer per key from issuer.grant_pool to the holder account, dedicated grant code range, user_data_128 = grant id, user_data_64 = key hash; vest via post_pending_transfer + pending_id; refund via void_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 / _expired as 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

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    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

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-29T12:12Z (07:12 CDT)
    • Found via: monitor reconciliation
    • Task: Duty 2 intake enrichment — acceptance criteria appended (original body preserved verbatim above it), labels normalized, meta/enriched applied. 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.
    • Expectation: see the effort label; done = the appended acceptance criteria are met.
    • Links: this issue
  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done — triage verdict: CONFIRMED, ORDERED (buildable design; two sibling dependencies + one Drew decision ahead of it)

    Verified fresh (2026-11-03)

    Blockers / ordering (recorded per the acceptance)

    1. id: build: TigerBeetle cluster provisioning — RTT mesh gate, 6-replica format, dead-replica runbook + rehearsal #2292 — TigerBeetle cluster provisioning (open, sibling): this issue's acceptance requires register → vest and register → void to pass against a dev TigerBeetle cluster. id: build: TigerBeetle cluster provisioning — RTT mesh gate, 6-replica format, dead-replica runbook + rehearsal #2292 is the sibling that provisions that cluster (RTT mesh gate, 6-replica format, rehearsal). Order: id: build: TigerBeetle cluster provisioning — RTT mesh gate, 6-replica format, dead-replica runbook + rehearsal #2292 first, or the build must stand up its own throwaway dev cluster and say so.
    2. id: decide: grant refund window length and whether pending tokens are spendable #2296 — refund-window decision (open, 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.
    3. id: build: grant vesting reconciler and control-plane↔ledger drift alarm #2295 — vesting reconciler (open, sibling): explicitly out of scope here (per the body), listed only to fix the set order: id: build: TigerBeetle cluster provisioning — RTT mesh gate, 6-replica format, dead-replica runbook + rehearsal #2292 → id: build: grant service — pending-transfer grants (register/vest/void) with control-plane grant records #2294 → id: build: grant vesting reconciler and control-plane↔ledger drift alarm #2295, with id: decide: grant refund window length and whether pending tokens are spendable #2296 decided before id: build: grant service — pending-transfer grants (register/vest/void) with control-plane grant records #2294 is called 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/l once the ordering clears.

  5. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    and removed on Nov 3, 2026
  6. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged (task 8609, lane code-k, direct)

    • Agent: queue worker (repo-worker-code), lane code-k
    • Session / ID: task 8609
    • Found via: queue claim (coverage pass re-queue)

    Triage verdict — confirms task 7870's ordering (15:23Z today); both blockers still stand, one has hardened.

    Re-verified fresh (2026-11-03T20:45Z):

    Ordering unchanged: #2292 lands (or a dev-cluster substitute is declared) + #2296 decided → #2294 is startable as a single effort/l build exactly as scoped. No new packet from this triage; the blockers are live, tracked, and labelled.

    Issue stays OPEN. Status: triaged.

  9. added
    status/triagedTriaged and routed (see agent note)
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus/triagedTriaged and routed (see agent note)task/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions