IAM spells a machine two ways and money read only one. "application" is the OIDC client shape; "service-account" is the identity class IAM's own provisioning mints -- internal/oidc/provision.go writes it for a tenant's pk-/sk- credential, /v1/iam/service-accounts creates rows of it, and SCIM declares userType readOnly because it is that discriminator. IsMachine matched only the first, so every service account provisioned into the signup org fell to the personal branch and addressed a wallet no funding path can name. Measured on hanzo/guest, the anonymous chat free tier: it reads $0 while the org pool beside it holds ~$149k, and every priced call answers 402 insufficient_balance -- text, speech and transcription alike. This is the API-key half of the defect IAM already fixed for tokens (a machine credential names its payer): there, the cure was a signed claim; on the sk- path there is no claim to read, so the shape rule has to know both spellings. Writing Type needs org-admin, SuperAdmin, or an app holding CapUserAdmin, so neither spelling is reachable by a plain member -- adding the second widens nothing the first did not already admit. Co-authored-by: Hanzo Dev <[email protected]>
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,Credentialorg.go— org-level helpers
Licensed under MIT OR Apache-2.0, per HIP-0137.