People: human wallets, signup grant, payments between people and agents #7

Open
leora wants to merge 7 commits from leora/human-wallets into main
Contributor

Adds people to the Coffer economy (prepaid only, no cash-out, public ledger).

What changes

  • human:<handle> wallets (units.human_account, account_kind).
  • human_signup(handle): wallet + the same 40-credit signup grant agents get, once per person. No subscription seat for people.
  • pay(payer, payee, credits, reason=..., ref=...): person -> agent, agent -> person and person -> person, once per ref (the platform offer id), max 1000 credits. Granted and funded credit can both be paid (granted first) and the payee receives the same kinds. Agent -> agent stays blocked.
  • No credit purchasing of any kind (removed: packs, test-mode purchase, live switch).
  • Public ledger: shows human:<handle> ids; adds offer to public meta.
  • design/coffer.md: "People" section.

Tests: tests/test_people.py (6) - signup grant once, no purchase API, person pays agent with granted credit + reload + idempotency, agent pays person and person pays person, rules (no agent->agent, no company, limits, insufficient credit), public ledger by handle. Full suite 28 OK.

Kept as a PR for the repo owner (rule change). The platform uses it only on staging, against a sandboxed Coffer DB.

Adds people to the Coffer economy (prepaid only, no cash-out, public ledger). **What changes** - `human:<handle>` wallets (`units.human_account`, `account_kind`). - `human_signup(handle)`: wallet + the same 40-credit signup grant agents get, once per person. No subscription seat for people. - `pay(payer, payee, credits, reason=..., ref=...)`: person -> agent, agent -> person and person -> person, once per `ref` (the platform offer id), max 1000 credits. Granted and funded credit can both be paid (granted first) and the payee receives the same kinds. Agent -> agent stays blocked. - No credit purchasing of any kind (removed: packs, test-mode purchase, live switch). - Public ledger: shows `human:<handle>` ids; adds `offer` to public meta. - `design/coffer.md`: "People" section. **Tests:** `tests/test_people.py` (6) - signup grant once, no purchase API, person pays agent with granted credit + reload + idempotency, agent pays person and person pays person, rules (no agent->agent, no company, limits, insufficient credit), public ledger by handle. Full suite 28 OK. Kept as a PR for the repo owner (rule change). The platform uses it only on staging, against a sandboxed Coffer DB.
leora added 1 commit 2026-10-10 09:45:47 +08:00
leora requested review from code-reviewer 2026-10-10 10:07:04 +08:00
leora changed title from People: human wallets, prepaid test-mode purchases, person<->agent payments to People: human wallets, signup grant, payments between people and agents 2026-10-10 10:11:17 +08:00
leora added 1 commit 2026-10-10 10:11:18 +08:00
First-time contributor

Posted as a comment: the reviewer token lacks the write:repository scope needed for formal PR reviews, so the verdict below is not recorded as a Gitea review state.

Verdict: Request changes. The PR needs another revision anyway: the purchase path is coming out and the human signup grant is going in, as the author noted. Below is what that revision should cover. I'm not blocking on the purchase code simply being present, since its removal is committed. I flag it so the removal is complete.

Purchase path (to be removed): make sure all of it goes

  • buy_credits creates funded credit with no payment in test mode. It also works for any account type: aid = account_id if ":" in account_id else ... (product.py:500) accepts agent:/company: too. Test credit is indistinguishable from real funded credit (lot kind FUNDED) and can be paid to agents. If this stays reachable anywhere outside a throwaway sandbox DB, it creates unlimited money.
  • Remove all of it: Ledger.purchase, buy_credits, CREDIT_PACKS, the payment public-meta key (public_ledger.py:36), the design doc text, and the purchase tests. On the platform side (leora/agent-chat#31) also remove /v1/net/wallet/buy, coffer_hook.buy_test_credits/TEST_CARD_* and the buy UI in messages.html.

Human signup grant (40 credits, to be added): requirements

  1. Key the once-only marker on the immutable Gitea user id, not the handle. Today agent-chat link_human keys humans by username, so renaming a Gitea account gives a new handle, a new human:<handle> wallet and another grant. I reproduced the linking half: link_human('alice',101) and link_human('alice2',101) both succeed. Either the grant marker must be signup_grant:gitea:<id>, or agent-chat must fix B5 first.
  2. Sybil accounts. Gitea sign-up is one captcha and no email, so every account is worth 40 credits. pay() correctly refuses promotional credit (_debit_kinds=(FUNDED,), ledger.py:273), so grants can't be passed to others. They still spend in the store, though (buy_shirt draws promotional first), and the store sells physical goods with real cost. Suggested guards: grant only on first link; require account age or a verified email; a per-IP and global daily cap; and/or limit human promotional credit to non-physical SKUs.
  3. Double claims. grant_signup does get_meta → grant → set_meta with no lock. agent-chat opens a fresh CofferProduct (its own in-memory ledger) per call (coffer_hook._with_product) on a ThreadingHTTPServer. Concurrent claims are currently stopped only by accident: the transactions.seq PRIMARY KEY collides. Make it explicit: inside BEGIN IMMEDIATE, first insert a unique grant key, then reload the account, then mutate.
  4. Grant only at link time on the server. Never grant lazily from GET /v1/net/wallet, which calls create_wallet for any caller.

Other findings

  • agent_account() now passes human: ids through (units.py:61). Every existing agent entry point that normalises with it (grant_signup, top_up, charge_subscription, store orders, the CLI) will now act on a human wallet when given human:x. Before this change that input became agent:human:x. Keep agent_account strict and handle humans explicitly.
  • Idempotency is check-then-act and not bound to the request (product.py:501-507, 518-525). The in-memory ledger is mutated before the key is inserted, so if the insert or commit fails, memory and the DB disagree. The key is also global: replaying pay:<ref> with a different payer, payee or amount returns duplicate: True with someone else's tx. Insert the key first in the same transaction, store (payer, payee, amount) with it, and raise on a mismatch.
  • pay() (product.py:515): round(float(credits) * SCALE) raises ValueError/OverflowError for nan/inf instead of ProductError. Validate the type and finiteness first.
  • design/coffer.md:77 says "People get no signup grant". Update it with the grant change.

Good: the same-kind peer block, funded-only payments, payment_ref kept off the public ledger, the 1000-credit cap and the tests.

Inline notes

  • coffer/units.py:61: Widening agent_account() to pass human: through means every agent-only entry point (grant_signup, top_up, charge_subscription, store orders, CLI) now acts on human wallets when given human:x. Keep this strict and add an explicit human path.
  • coffer/product.py:500: Purchase path (to be removed): this also accepts agent:/company: ids, so test mode creates FUNDED credit for any account. Please remove it fully, along with the platform's /v1/net/wallet/buy.
  • coffer/product.py:501: The idempotency key is global and not bound to account/amount, and it is checked before the in-memory ledger is mutated but inserted after. Insert the key first inside BEGIN IMMEDIATE, store the request tuple with it, and raise on a mismatch. The same applies to pay: below and to the planned human signup grant marker (key that one on the Gitea id, not the handle).
  • coffer/product.py:515: float('nan')/inf raise ValueError/OverflowError here, not ProductError. Validate finiteness and type first.
_Posted as a comment: the reviewer token lacks the `write:repository` scope needed for formal PR reviews, so the verdict below is not recorded as a Gitea review state._ **Verdict: Request changes.** The PR needs another revision anyway: the purchase path is coming out and the human signup grant is going in, as the author noted. Below is what that revision should cover. I'm not blocking on the purchase code simply being present, since its removal is committed. I flag it so the removal is complete. ## Purchase path (to be removed): make sure all of it goes - `buy_credits` creates **funded** credit with no payment in test mode. It also works for any account type: `aid = account_id if ":" in account_id else ...` (`product.py:500`) accepts `agent:`/`company:` too. Test credit is indistinguishable from real funded credit (lot kind `FUNDED`) and can be paid to agents. If this stays reachable anywhere outside a throwaway sandbox DB, it creates unlimited money. - Remove all of it: `Ledger.purchase`, `buy_credits`, `CREDIT_PACKS`, the `payment` public-meta key (`public_ledger.py:36`), the design doc text, and the purchase tests. On the platform side (leora/agent-chat#31) also remove `/v1/net/wallet/buy`, `coffer_hook.buy_test_credits`/`TEST_CARD_*` and the buy UI in `messages.html`. ## Human signup grant (40 credits, to be added): requirements 1. **Key the once-only marker on the immutable Gitea user id, not the handle.** Today agent-chat `link_human` keys humans by username, so renaming a Gitea account gives a new handle, a new `human:<handle>` wallet and another grant. I reproduced the linking half: `link_human('alice',101)` and `link_human('alice2',101)` both succeed. Either the grant marker must be `signup_grant:gitea:<id>`, or agent-chat must fix B5 first. 2. **Sybil accounts.** Gitea sign-up is one captcha and no email, so every account is worth 40 credits. `pay()` correctly refuses promotional credit (`_debit_kinds=(FUNDED,)`, `ledger.py:273`), so grants can't be passed to others. They still spend in the store, though (`buy_shirt` draws promotional first), and the store sells physical goods with real cost. Suggested guards: grant only on first link; require account age or a verified email; a per-IP and global daily cap; and/or limit human promotional credit to non-physical SKUs. 3. **Double claims.** `grant_signup` does `get_meta` → `grant` → `set_meta` with no lock. agent-chat opens a fresh `CofferProduct` (its own in-memory ledger) per call (`coffer_hook._with_product`) on a `ThreadingHTTPServer`. Concurrent claims are currently stopped only by accident: the `transactions.seq` PRIMARY KEY collides. Make it explicit: inside `BEGIN IMMEDIATE`, first insert a unique grant key, then reload the account, then mutate. 4. Grant only at link time on the server. Never grant lazily from `GET /v1/net/wallet`, which calls `create_wallet` for any caller. ## Other findings - **`agent_account()` now passes `human:` ids through** (`units.py:61`). Every existing agent entry point that normalises with it (`grant_signup`, `top_up`, `charge_subscription`, store orders, the CLI) will now act on a human wallet when given `human:x`. Before this change that input became `agent:human:x`. Keep `agent_account` strict and handle humans explicitly. - **Idempotency is check-then-act and not bound to the request** (`product.py:501-507`, `518-525`). The in-memory ledger is mutated *before* the key is inserted, so if the insert or commit fails, memory and the DB disagree. The key is also global: replaying `pay:<ref>` with a different payer, payee or amount returns `duplicate: True` with someone else's tx. Insert the key first in the same transaction, store `(payer, payee, amount)` with it, and raise on a mismatch. - `pay()` (`product.py:515`): `round(float(credits) * SCALE)` raises `ValueError`/`OverflowError` for `nan`/`inf` instead of `ProductError`. Validate the type and finiteness first. - `design/coffer.md:77` says "People get no signup grant". Update it with the grant change. Good: the same-kind peer block, funded-only payments, `payment_ref` kept off the public ledger, the 1000-credit cap and the tests. ## Inline notes - `coffer/units.py:61`: Widening `agent_account()` to pass `human:` through means every agent-only entry point (grant_signup, top_up, charge_subscription, store orders, CLI) now acts on human wallets when given `human:x`. Keep this strict and add an explicit human path. - `coffer/product.py:500`: Purchase path (to be removed): this also accepts `agent:`/`company:` ids, so test mode creates FUNDED credit for any account. Please remove it fully, along with the platform's /v1/net/wallet/buy. - `coffer/product.py:501`: The idempotency key is global and not bound to account/amount, and it is checked before the in-memory ledger is mutated but inserted after. Insert the key first inside `BEGIN IMMEDIATE`, store the request tuple with it, and raise on a mismatch. The same applies to `pay:` below and to the planned human signup grant marker (key that one on the Gitea id, not the handle). - `coffer/product.py:515`: `float('nan')`/`inf` raise ValueError/OverflowError here, not ProductError. Validate finiteness and type first.
leora added 1 commit 2026-10-10 10:31:15 +08:00
Author
Contributor

Review fixes pushed (a523b8a):

  • human_signup(handle, gitea_id): one grant per Gitea user id, claim key inserted first inside BEGIN IMMEDIATE with a reload, so concurrent claims from separate processes give one grant and a renamed account cannot claim again (test: 8 threads on separate connections -> one grant).
  • agent_account() is strict again and refuses human: ids, so grant_signup, top_up, charge_subscription, signup_flow, store orders and the CLI cannot touch people's wallets (test added).
  • pay(): idempotency key inserted first and bound to (payer, payee, amount); replay with anything else raises; NaN/inf/bool/str amounts refused; runs inside the same atomic block.
  • human_account() accepts lowercase a-z0-9- only (matches platform handles).
  • design/coffer.md updated. Tests: 31/31.
Review fixes pushed (a523b8a): - `human_signup(handle, gitea_id)`: one grant per **Gitea user id**, claim key inserted first inside `BEGIN IMMEDIATE` with a reload, so concurrent claims from separate processes give one grant and a renamed account cannot claim again (test: 8 threads on separate connections -> one grant). - `agent_account()` is strict again and refuses `human:` ids, so `grant_signup`, `top_up`, `charge_subscription`, `signup_flow`, store orders and the CLI cannot touch people's wallets (test added). - `pay()`: idempotency key inserted first and bound to (payer, payee, amount); replay with anything else raises; NaN/inf/bool/str amounts refused; runs inside the same atomic block. - `human_account()` accepts lowercase a-z0-9- only (matches platform handles). - design/coffer.md updated. Tests: 31/31.
code-reviewer reviewed 2026-10-10 11:06:46 +08:00
code-reviewer left a comment
First-time contributor

COMMENT (mechanics are sound; one economic question before people's grants go live)

Reviewed at a523b8a. Full suite 31 OK, and I ran concurrency/abuse tests. The round-1 Coffer blockers are fixed:

  • Signup-grant race/duplication — fixed. human_signup uses BEGIN IMMEDIATE + insert-first idempotency key signup_grant:gitea:<gid>. 8 concurrent sign-ins for one Gitea id → exactly one grant, 7 already=True; balance stays 40. Keyed on the immutable Gitea id, so renames can't re-claim.
  • Payment idempotency — fixed and correctly bound. pay is once-per-ref, and the key stores payer>payee:amount; replaying a ref with a different request raises (verified). 4 concurrent 30-credit pays from a 40 balance → exactly one succeeds, rest InsufficientCredit; no double-spend, no negative balance.
  • Agent-only entry points refuse people — fixed. human_account() rejects non-ascii/upper/agent:; top_up, grant_signup, charge_subscription, buy_shirt all raise on human: ids ("not an agent account").
  • No purchasing — fixed. No buy_credits attribute, packs/test-mode/live switch all gone.
  • agent→agent still blocked, company excluded, MAX_PAYMENT_CREDITS=1000 enforced, public ledger shows handles only + the offer meta.

The one thing to decide before people's grants are enabled on prod: granted (promotional) credit is spendable the same as funded credit, including on physical goods — design/coffer.md: "Promotional credit granted for the launch spends on the shirt the same way funded credit does. Coffer still pays the printer." Combined with person→person pay, N sybil accounts can each take the 40-credit signup grant, funnel it to one collector, and that collector orders real merchandise. That is an effective cash-out of farmed grants, despite "no cash-out, ever." The 50-grants/day platform cap bounds daily exposure (~2000 credits/day), but the sink is open. Recommend either (a) marking granted credit non-spendable on goods / printer-backed SKUs, or (b) documenting this explicitly as accepted residual risk with its cap, in the threat model. Not a correctness bug in this PR, so COMMENT rather than REQUEST CHANGES — it's a product/policy call for the owner, who is keeping this as a PR anyway.

Minor: if the platform's net handle for a person ever diverges from the stored one, human_signup(handle2, gid) raises idempotency key reused for a different request (the key value stores the account id, not just the gid). Harmless (never double-grants) but produces a confusing error and an endless retry; consider keying the stored value on the gid only.

## COMMENT (mechanics are sound; one economic question before people's grants go live) Reviewed at `a523b8a`. Full suite 31 OK, and I ran concurrency/abuse tests. The round-1 Coffer blockers are fixed: - **Signup-grant race/duplication — fixed.** `human_signup` uses `BEGIN IMMEDIATE` + insert-first idempotency key `signup_grant:gitea:<gid>`. 8 concurrent sign-ins for one Gitea id → exactly one grant, 7 `already=True`; balance stays 40. Keyed on the immutable Gitea id, so renames can't re-claim. - **Payment idempotency — fixed and correctly bound.** `pay` is once-per-`ref`, and the key stores `payer>payee:amount`; replaying a ref with a different request raises (verified). 4 concurrent 30-credit pays from a 40 balance → exactly one succeeds, rest `InsufficientCredit`; no double-spend, no negative balance. - **Agent-only entry points refuse people — fixed.** `human_account()` rejects non-ascii/upper/`agent:`; `top_up`, `grant_signup`, `charge_subscription`, `buy_shirt` all raise on `human:` ids ("not an agent account"). - **No purchasing — fixed.** No `buy_credits` attribute, packs/test-mode/live switch all gone. - **agent→agent still blocked**, company excluded, `MAX_PAYMENT_CREDITS=1000` enforced, public ledger shows handles only + the `offer` meta. **The one thing to decide before people's grants are enabled on prod:** granted (promotional) credit is spendable the same as funded credit, including on physical goods — `design/coffer.md`: "Promotional credit granted for the launch spends on the shirt the same way funded credit does. Coffer still pays the printer." Combined with person→person `pay`, N sybil accounts can each take the 40-credit signup grant, funnel it to one collector, and that collector orders real merchandise. That is an effective cash-out of farmed grants, despite "no cash-out, ever." The 50-grants/day platform cap bounds daily exposure (~2000 credits/day), but the sink is open. Recommend either (a) marking granted credit non-spendable on goods / printer-backed SKUs, or (b) documenting this explicitly as accepted residual risk with its cap, in the threat model. Not a correctness bug in this PR, so COMMENT rather than REQUEST CHANGES — it's a product/policy call for the owner, who is keeping this as a PR anyway. Minor: if the platform's net handle for a person ever diverges from the stored one, `human_signup(handle2, gid)` raises `idempotency key reused for a different request` (the key value stores the account id, not just the gid). Harmless (never double-grants) but produces a confusing error and an endless retry; consider keying the stored value on the gid only.
leora added 1 commit 2026-10-10 11:21:59 +08:00
leora added 1 commit 2026-10-10 11:28:40 +08:00
leora added 1 commit 2026-10-10 11:31:15 +08:00
code-reviewer approved these changes 2026-10-10 12:03:30 +08:00
Dismissed
code-reviewer left a comment
First-time contributor

Re-review (round 3) — APPROVE

Reviewed at c46842f (matches the named commit). Full suite 36/36. I re-ran concurrency/abuse tests against the new order-escrow code. The merch-review rule is implemented correctly and the earlier fixes still hold.

Shirt orders held for review — verified

  • Held, never auto-fulfilled. buy_shirt→place_order creates the order pending_review with credits_charged=0, holding the price in company:coffer-escrow via ledger.hold. No order is fulfilled without an operator accept.
  • Reject releases the exact amount, once, no race. reject_order→_review raises OrderNotPending unless status is pending_review, and release_hold returns the stored per-order draws as the same credit kinds. 6 concurrent rejects of one order → exactly one ok, five OrderNotPending; the buyer got back precisely the held 20 (as granted). Reject-after-accept → OrderNotPending (no double decide). All inside _atomic() (BEGIN IMMEDIATE), so no double release.
  • 2-pending cap holds under concurrency. 8 concurrent buy_shirt for one funded account → exactly 2 pending_review, six TooManyPendingOrders; the check runs inside BEGIN IMMEDIATE after a reload.
  • Operator-only review. accept_order/reject_order reject any non-reviewer (NotAReviewer); a human rejecting their own order and an arbitrary actor accepting both fail.

Granted credit has no non-reviewed cash-out — verified. pay credits the payee promotional-as-promotional (granted stays granted across transfers), funded/earned become earned; so a grant can't be laundered into a freely-spendable kind. The only way granted credit leaves as a real-world good is an operator-reviewed order. top_up/grant_signup/charge_subscription all still refuse human: accounts.

Prior fixes re-confirmed: grant once per Gitea id (8 concurrent → one grant), payment idempotent and bound to payer/payee/amount, agent→agent blocked, public ledger handles-only (+ offer meta, human:<handle> ids).

Minor (unchanged from round 2): if a person's stored net handle ever diverged from the one passed to human_signup, the grant key would raise idempotency key reused for a different request — harmless (never double-grants) but a confusing error. Non-blocking.

This stays a PR in your repo by your choice; from the economy/abuse standpoint I'd sign off on enabling people's credits with it. Approving.

## Re-review (round 3) — APPROVE Reviewed at `c46842f` (matches the named commit). Full suite 36/36. I re-ran concurrency/abuse tests against the new order-escrow code. The merch-review rule is implemented correctly and the earlier fixes still hold. **Shirt orders held for review — verified** - **Held, never auto-fulfilled.** `buy_shirt`→`place_order` creates the order `pending_review` with `credits_charged=0`, holding the price in `company:coffer-escrow` via `ledger.hold`. No order is fulfilled without an operator accept. - **Reject releases the exact amount, once, no race.** `reject_order`→`_review` raises `OrderNotPending` unless status is `pending_review`, and `release_hold` returns the stored per-order `draws` as the **same credit kinds**. 6 concurrent rejects of one order → exactly one `ok`, five `OrderNotPending`; the buyer got back precisely the held 20 (as granted). Reject-after-accept → `OrderNotPending` (no double decide). All inside `_atomic()` (`BEGIN IMMEDIATE`), so no double release. - **2-pending cap holds under concurrency.** 8 concurrent `buy_shirt` for one funded account → exactly 2 `pending_review`, six `TooManyPendingOrders`; the check runs inside `BEGIN IMMEDIATE` after a reload. - **Operator-only review.** `accept_order`/`reject_order` reject any non-reviewer (`NotAReviewer`); a human rejecting their own order and an arbitrary actor accepting both fail. **Granted credit has no non-reviewed cash-out — verified.** `pay` credits the payee promotional-as-promotional (granted stays granted across transfers), funded/earned become `earned`; so a grant can't be laundered into a freely-spendable kind. The only way granted credit leaves as a real-world good is an operator-reviewed order. `top_up`/`grant_signup`/`charge_subscription` all still refuse `human:` accounts. Prior fixes re-confirmed: grant once per Gitea id (8 concurrent → one grant), payment idempotent and bound to payer/payee/amount, agent→agent blocked, public ledger handles-only (+ `offer` meta, `human:<handle>` ids). Minor (unchanged from round 2): if a person's stored net handle ever diverged from the one passed to `human_signup`, the grant key would raise `idempotency key reused for a different request` — harmless (never double-grants) but a confusing error. Non-blocking. This stays a PR in your repo by your choice; from the economy/abuse standpoint I'd sign off on enabling people's credits with it. Approving.
leora added 1 commit 2026-10-10 12:36:48 +08:00
code-reviewer approved these changes 2026-10-10 12:46:54 +08:00
code-reviewer left a comment
First-time contributor

Code Reviewer — Round 4 re-review at 7b49a53 (human_signup error nit + escrow/grant/idempotency regression check). Verdict: APPROVED.

Head matches the commit Leora named (7b49a53). The only delta since c46842f is the human_signup guard; full test suite green (36 tests).

human_signup error nit — fixed cleanly. Before claiming the grant, it now checks the existing idempotency row for signup_grant:gitea:{gid}:

  • same gitea id + same handle → falls through to _claim, which returns the stored value (idempotent, no second grant);
  • same gitea id + different handle (renamed account) → raises a clear ProductError("signup grant already claimed for this Gitea account under another handle") instead of the opaque "idempotency key reused" error.

Security outcome is unchanged (still exactly one grant per gitea id, no double-grant on rename); only the error is now meaningful. The row format ("pending|<request>" then "<seq>|<request>", split on |) matches what _claim/_settle_claim write, so the new comparison is correct and does not false-positive on a legitimate idempotent retry.

Regression check — no regressions. Escrow (order held for review, never auto-fulfilled), rejection releasing the exact held amount once, the 2-pending-per-account cap, and payment idempotency (pay once-per-ref) are untouched by this delta and remain as approved in round 3. Granted credit still has no cash-out path other than operator-reviewed shirt orders.

**Code Reviewer — Round 4 re-review at 7b49a53 (human_signup error nit + escrow/grant/idempotency regression check). Verdict: APPROVED.** Head matches the commit Leora named (7b49a53). The only delta since c46842f is the `human_signup` guard; full test suite green (36 tests). **human_signup error nit — fixed cleanly.** Before claiming the grant, it now checks the existing idempotency row for `signup_grant:gitea:{gid}`: - same gitea id + same handle → falls through to `_claim`, which returns the stored value (idempotent, no second grant); - same gitea id + different handle (renamed account) → raises a clear `ProductError("signup grant already claimed for this Gitea account under another handle")` instead of the opaque "idempotency key reused" error. Security outcome is unchanged (still exactly one grant per gitea id, no double-grant on rename); only the error is now meaningful. The row format (`"pending|<request>"` then `"<seq>|<request>"`, split on `|`) matches what `_claim`/`_settle_claim` write, so the new comparison is correct and does not false-positive on a legitimate idempotent retry. **Regression check — no regressions.** Escrow (order held for review, never auto-fulfilled), rejection releasing the exact held amount once, the 2-pending-per-account cap, and payment idempotency (`pay` once-per-ref) are untouched by this delta and remain as approved in round 3. Granted credit still has no cash-out path other than operator-reviewed shirt orders.
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin leora/human-wallets:leora/human-wallets
git checkout leora/human-wallets
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: cursorman/coffer#7
No description provided.