zeekayandHanzo Dev c2639c71f9 being a program is not a grant, so it stops deciding who pays
Credential carried a Machine flag, derived by each caller from User.Type, and
Payer read it one step above the rule that governs the signup org. So a
machine-typed row took Org(SignupOrg) — the platform's own balance — without
ever reaching that rule.

Outside the signup org the flag decided nothing: machine and person both pool.
Its only effect was to turn "is a program", a profile column, into "may spend
the platform's money", an authority. The fallback exists to answer for
credentials that carry no claim, and a credential carrying no claim carries no
evidence of authority either — it must resolve, not grant.

So the field is gone rather than merely unread: the defect is now unexpressible
at the type level, and every caller that asserted a class is a compile error
rather than a silent wrong answer. Machine-ness still answers the questions a
column can answer (IsMachine — which column a usage record fills, whether there
is a person behind the credential); who pays is stated by IAM at the identity
boundary and carried in the signed billing_account claim.

First-party service credentials are untouched: both doors state that claim, and
the API-key door does not transmit User.Type at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 10:15:11 -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%