You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(billing): address accounts by :account_id; harden /wallets/:address/projects auth (#433)
Mirror the gateway api-cleanup-batch-1-accounts contract in SDK/CLI/MCP:
- Account reads are keyed by the canonical billing_account_id (UUID). SDK
getAccount/checkBalance route a UUID to GET /billing/v1/accounts/:account_id
and a wallet/email through the new GET /billing/v1/accounts?wallet=|?email=
lookup; getHistory resolves wallet/email to the account id first, then reads
/accounts/:account_id/history. Add billing.lookupAccount as the resolve
primitive. Response shape drops identifier_type and adds billing_account_id
(BillingBalance kept as a deprecated alias of BillingAccountDetail).
- Exposure hardening: projects.list (GET /wallets/v1/:address/projects) now
sends SIWX — the endpoint went from public to owner-only (admin bypasses).
- MCP tool descriptions, CLI billing help, and the comprehensive docs
(llms-cli.txt, llms-sdk.txt, sdk/README.md, SKILL/openclaw SKILL) updated.
- Tests: rewrote billing/projects URL+auth assertions, added lookupAccount
coverage and a CLI-e2e lookup mock; sync.test endpoint registry updated.
Closes#432
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- `run402 billing history <email-or-wallet> [--limit <n>]` — ledger history (both identifier types supported)
1345
+
- `run402 billing balance <account-id | wallet | email>` — balance + email_credits_remaining + tier + lease + auto_recharge state (response includes `billing_account_id`). A wallet/email is resolved to its account via `GET /billing/v1/accounts?wallet=|?email=`; an account id (UUID) reads `GET /billing/v1/accounts/:account_id` directly.
1346
+
- `run402 billing history <account-id | wallet | email> [--limit <n>]` — ledger history. Keyed by account id: a wallet/email is resolved to its `billing_account_id` first, then `GET /billing/v1/accounts/:account_id/history`.
1347
1347
1348
-
Auth (v2.19.1+): `balance`, `history`, `link-wallet`, and `auto-recharge` require SIWX signed by the relevant wallet (the path wallet for reads, the body wallet for link, a linked wallet for auto-recharge). The CLI signs automatically from the local allowance, so these only work when the agent's allowance wallet is the one being queried/linked/billed. Email-identifier reads require an admin key. Checkout-creation commands (`create-email`, `tier-checkout`, `buy-email-pack`) remain unauthenticated.
1348
+
Auth: account reads (`balance`, `history`) require SIWX from a wallet **linked to** the account (membership), or an admin key; the wallet→account lookup requires SIWX matching the queried wallet (email lookups are admin-only). `link-wallet` requires the body wallet's SIWX (or admin); `auto-recharge` a linked wallet's SIWX. The CLI signs automatically from the local allowance, so these only work when the agent's allowance wallet is on the account being queried/linked/billed. A non-member (or someone guessing an account id) gets 403 with no existence leak. Checkout-creation commands (`create-email`, `tier-checkout`, `buy-email-pack`) remain unauthenticated.
1349
1349
1350
1350
Email packs only activate when the tier daily limit is exhausted AND the project has a verified custom sender domain (spam protection for `mail.run402.com`).
run402 billing auto-recharge <account_id> on --threshold 2000
849
-
run402 billing balance <email-or-wallet>
850
-
run402 billing history<email-or-wallet>
849
+
run402 billing balance <account-id |wallet| email># wallet/email resolved to the account; SIWX must be linked to it (email lookups admin-only)
850
+
run402 billing history<account-id |wallet| email>
851
851
```
852
852
853
853
`run402 tier set` refetches `/tiers/v1/status` after the call and includes the refreshed account-pool snapshot as `status_after` in the JSON output, so the new pooled `api_calls` / `storage_bytes` totals come back in one step.
- `lease_perpetual: boolean | null` — operator escape hatch flag. When `true`, the account never advances past `active` regardless of lease expiry.
633
633
- `tier: "prototype" | "hobby" | "team" | null` — the account's active tier.
634
634
635
-
`r.projects.list(wallet)` returns per-project facts only (`id`, `name`, `api_calls`, `storage_bytes`, `created_at`). The v1.56 `status` / `pinned` / `tier` mirrors and the v1.57 `effective_status` / `account_lifecycle_state` / `lease_perpetual` per-project mirrors were dropped. The v1.56 `projects.pin(id)` SDK method was removed alongside the endpoint — use `r.admin.setLeasePerpetual(ba_id, true)`.
635
+
`r.projects.list(wallet)` returns per-project facts only (`id`, `name`, `api_calls`, `storage_bytes`, `created_at`). It now requires SIWX from the wallet itself (or an admin key) — `GET /wallets/v1/:wallet/projects` was hardened from public to owner-only, since project ids/names/usage are private; the kernel signs the header from the provider, and listing another wallet's projects returns 403. The v1.56 `status` / `pinned` / `tier` mirrors and the v1.57 `effective_status` / `account_lifecycle_state` / `lease_perpetual` per-project mirrors were dropped. The v1.56 `projects.pin(id)` SDK method was removed alongside the endpoint — use `r.admin.setLeasePerpetual(ba_id, true)`.
636
636
637
637
`r.projects.getUsage(id)` still surfaces `effective_status` and `account_lifecycle_state` because that endpoint scopes to a single project and the derivation collapses per-project `archived_at` / `deleted_at` together with the account's lifecycle.
0 commit comments