zeekayandHanzo Dev 8a345611a2 a credential that names nobody pays nothing, and a bare name is a member
The signup org is the one org where the NAME decides who pays: its members are
strangers who each hold their own wallet, and the account beside them is the
platform's own balance. Payer read a missing name as "not a person" and answered
with that account, so a credential that had FAILED to resolve a person spent the
platform pool.

It was reachable self-serve and it renewed itself. IAM returns a nameless
identity when a token row's (owner, name) key no longer resolves, and a refresh
token freezes that key at establishment, so re-keying the identity underneath it
— which onboarding a personal org does — makes every later rotation mint one.
Deleting a user did it too, which meant banning someone upgraded their billing.

Refuse instead, and refuse INSIDE the signup-org branch: a blank name is also how
a caller addresses an org account (a member-less grant crediting a tenant's pool,
a bare key naming a ledger), and every other org answers alike for a named member
and a nameless one, so requiring a name estate-wide would break funding a
customer to close a hole that only exists where the name is read.

PayerOf had the same defect from the other side. A slash-less key with an
explicit org had its key discarded and answered with the org account, while the
spend gate re-qualifies that same bare name to <org>/<name> before checking the
balance — so a legacy chat/TTS turn was gated against the person's wallet and
then charged to the pool. One request, two addresses. It is the member.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 18:29:33 -07:00

account

The billing-account rule. One value, one decision, no dependencies.

account.Payer(account.Credential{Owner: org, Name: user, Account: claim}).Subject()

(Org, Subject) is the money's address — Org names the ledger that holds it, Subject the key within that ledger. A deposit credits that address, the spend-gate reads it, a usage debit spends it. One address, one answer, every caller.

Payer is the whole point: it, and only it, decides whether a request pays from an org pool or a person's own account. That decision shipped wrong twice by being made in two places — a gate keyed on the pool while the debit spent a person's wallet, so a funded pool green-lit a request that drained an empty personal one, and an empty pool 402'd a funded person. Any new money path resolves its address here rather than re-deriving it.

Why it is its own module

It has zero dependencies — go.mod is two lines and the code imports only strings. That is what lets every layer import it: cloud's trust boundary (clients/principal), its metering middleware, and commerce's billing resolver all call the same rule without any of them depending on each other.

Folding it into the commerce service would invert that: identity code would have to import a ledger, a SQLite store and an HTTP surface to answer a pure question about who pays.

Layout

  • account.go — Account, Org/Person/Project, Parse, Payer, Credential
  • org.go — org-level helpers

Licensed under MIT OR Apache-2.0, per HIP-0137.

S
Description
pull mirror of github.com/hanzoai/account
Readme
166 KiB
Languages
Go 100%