Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ The MCP server manages a local agent allowance — a wallet key dedicated to pay
- **`init`** — composes `allowance_create` + `request_faucet` + `tier_status` + `list_projects`. Use this on a fresh install.
- **`allowance_create`** / **`allowance_status`** / **`allowance_export`** — granular allowance ops.
- **`request_faucet`** — Base Sepolia testnet USDC.
- **`check_balance`** — mainnet + testnet + billing balance for an address.
- **`check_balance`** — run402 billing account balance (available + held) for the agent's wallet; resolves the wallet to its account over SIWX.

Other allowance options:
- **Coinbase AgentKit** — MPC wallet on Base with built-in x402.
Expand Down
16 changes: 15 additions & 1 deletion cli-e2e.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,22 @@ async function mockFetch(input, init) {
}

// Billing
// Account lookup: GET /billing/v1/accounts?wallet=|?email= → resolve to a
// billing_account_id (and return the same detail shape as the by-id read).
if (pathNoQuery === "/billing/v1/accounts" && method === "GET") {
return Promise.resolve(json({
billing_account_id: "00000000-0000-4000-8000-0000000000e2",
available_usd_micros: 150000,
held_usd_micros: 0,
email_credits_remaining: 0,
tier: null,
lease_expires_at: null,
auto_recharge_enabled: false,
auto_recharge_threshold: 0,
}));
}
if (path.match(/^\/billing\/v1\/accounts\/[^/]+$/) && method === "GET") {
return Promise.resolve(json({ available_usd_micros: 150000, held_usd_micros: 0 }));
return Promise.resolve(json({ billing_account_id: "00000000-0000-4000-8000-0000000000e2", available_usd_micros: 150000, held_usd_micros: 0 }));
}
if (path.match(/\/history/) && method === "GET") {
return Promise.resolve(json({ transactions: [{ id: "tx1", amount: -100000, description: "Tier subscription" }] }));
Expand Down
32 changes: 22 additions & 10 deletions cli/lib/billing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Subcommands:
tier-checkout <tier> [--email <e> | --wallet <w>] Stripe tier checkout
buy-email-pack [--email <e> | --wallet <w>] Buy \$5 email pack (10,000 emails)
auto-recharge <account_id> <on|off> [--threshold <n>]
balance <identifier> Balance by email or wallet (0x...)
history <identifier> [--limit <n>] Ledger history by email or wallet
balance <identifier> Balance by account id (UUID), wallet (0x...), or email
history <identifier> [--limit <n>] Ledger history by account id (UUID), wallet, or email

Examples:
run402 billing create-email user@example.com
Expand Down Expand Up @@ -70,32 +70,44 @@ Examples:
run402 billing auto-recharge acct_abc on --threshold 2000
run402 billing auto-recharge acct_abc off
`,
history: `run402 billing history — Show ledger history for an email or wallet
history: `run402 billing history — Show ledger history for a billing account

Usage:
run402 billing history <identifier> [--limit <n>]

Arguments:
<identifier> Email address or wallet (0x...)
<identifier> Billing account id (UUID), wallet (0x...), or email.
Wallet/email are resolved to the account id first.

Options:
--limit <n> Max entries to return (default: 50)

Auth:
Requires SIWX from a wallet linked to the account (signed automatically from
the local allowance), or an admin key. Email lookups require an admin key.

Examples:
run402 billing history user@example.com
run402 billing history 0x1234... --limit 100
run402 billing history 00000000-0000-4000-8000-000000000001
`,
balance: `run402 billing balance — Show balance for an email or wallet
balance: `run402 billing balance — Show balance for a billing account

Usage:
run402 billing balance <identifier>

Arguments:
<identifier> Email address or wallet (0x...)
<identifier> Billing account id (UUID), wallet (0x...), or email.
Wallet/email are resolved via the accounts lookup.

Auth:
Requires SIWX from a wallet linked to the account (signed automatically from
the local allowance), or an admin key. Email lookups require an admin key.

Examples:
run402 billing balance user@example.com
run402 billing balance 0x1234abcd...
run402 billing balance 00000000-0000-4000-8000-000000000001
`,
"create-email": `run402 billing create-email — Create an email billing account

Expand Down Expand Up @@ -292,8 +304,8 @@ async function balance(args) {
if (!id) {
fail({
code: "BAD_USAGE",
message: "Missing <email-or-wallet>.",
hint: "run402 billing balance <email-or-wallet>",
message: "Missing <identifier>.",
hint: "run402 billing balance <account-id | wallet | email>",
});
}
try {
Expand All @@ -316,8 +328,8 @@ async function history(args) {
if (!id) {
fail({
code: "BAD_USAGE",
message: "Missing <email-or-wallet>.",
hint: "run402 billing history <email-or-wallet> [--limit <n>]",
message: "Missing <identifier>.",
hint: "run402 billing history <account-id | wallet | email> [--limit <n>]",
});
}
const limit = parsedArgs.includes("--limit")
Expand Down
6 changes: 3 additions & 3 deletions cli/llms-cli.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1342,10 +1342,10 @@ Email-based billing accounts, Stripe tier checkout, and email packs. Pay run402
- `run402 billing tier-checkout <tier> [--email <e> | --wallet <w>]` — subscribe/renew/upgrade a tier (prototype $0.10 / hobby $5 / team $20) via Stripe. Returns a checkout URL.
- `run402 billing buy-email-pack [--email <e> | --wallet <w>]` — buy a $5 email pack (10,000 emails, never expire). Returns a Stripe checkout URL.
- `run402 billing auto-recharge <account_id> <on|off> [--threshold <n>]` — auto-repurchase $5 packs when credits drop below threshold. Requires saved Stripe payment method.
- `run402 billing balance <email-or-wallet>` — balance + email_credits_remaining + tier + lease + auto_recharge state
- `run402 billing history <email-or-wallet> [--limit <n>]` — ledger history (both identifier types supported)
- `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.
- `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`.

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.
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.

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`).

Expand Down
4 changes: 2 additions & 2 deletions openclaw/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,8 +846,8 @@ run402 billing link-wallet <account_id> <wallet> # response includes pool_impl
run402 billing tier-checkout hobby --email user@example.com
run402 billing buy-email-pack --email user@example.com # $5 / 10k emails (never expire)
run402 billing auto-recharge <account_id> on --threshold 2000
run402 billing balance <email-or-wallet>
run402 billing history <email-or-wallet>
run402 billing balance <account-id | wallet | email> # wallet/email resolved to the account; SIWX must be linked to it (email lookups admin-only)
run402 billing history <account-id | wallet | email>
```

`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.
Expand Down
2 changes: 1 addition & 1 deletion sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ The `CredentialsProvider` interface has two required methods (`getAuth`, `getPro
| `auth` | `requestMagicLink`, `verifyMagicLink`, `createUser`, `inviteUser`, `setUserPassword`, `settings`, passkey registration/login/list/delete helpers, `providers`, `promote`, `demote` |
| `apps` | `browse`, `getApp`, `fork`, `publish`, `listVersions`, `updateVersion`, `deleteVersion` |
| `tier` | `set`, `status` (tier pricing lives on `r.projects.getQuote()`) |
| `billing` | `createEmailAccount`, `linkWallet`, `tierCheckout`, `buyEmailPack`, `setAutoRecharge`, `checkBalance`, `getAccount`, `getHistory`, `balance`, `history`, `createCheckout` |
| `billing` | `createEmailAccount`, `linkWallet`, `tierCheckout`, `buyEmailPack`, `setAutoRecharge`, `checkBalance`, `getAccount`, `lookupAccount`, `getHistory`, `balance`, `history`, `createCheckout` |
| `contracts` | `provisionWallet`, `getWallet`, `listWallets`, `setRecovery`, `setLowBalanceAlert`, `call`, `read`, `callStatus`, `drain`, `deleteWallet` |
| `ai` | `translate`, `moderate`, `usage`, `generateImage` |
| `allowance` | `status`, `create`, `export`, `faucet` |
Expand Down
22 changes: 17 additions & 5 deletions sdk/llms-sdk.txt
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ demoteUser(id, email): Promise<void>
- `lease_perpetual: boolean | null` — operator escape hatch flag. When `true`, the account never advances past `active` regardless of lease expiry.
- `tier: "prototype" | "hobby" | "team" | null` — the account's active tier.

`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)`.
`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)`.

`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.

Expand Down Expand Up @@ -1159,13 +1159,14 @@ gateways.

```
createEmailAccount(email): Promise<EmailBillingAccount>
linkWallet(billingAccountId, wallet): Promise<LinkWalletResult>
linkWallet(billingAccountId, wallet): Promise<LinkWalletResult> // billingAccountId = UUID; POST /billing/v1/accounts/:account_id/link-wallet
tierCheckout(tier, identifier: { email?: string, wallet?: string }): Promise<CreateCheckoutResult>
buyEmailPack(identifier: { email?: string, wallet?: string }): Promise<CreateCheckoutResult>
setAutoRecharge(opts: { billingAccountId: string, enabled: boolean, threshold? }): Promise<void>
checkBalance(identifier): Promise<BillingBalance>
getAccount(identifier): Promise<BillingBalance>
balance(identifier): Promise<BillingBalance> // alias of checkBalance
checkBalance(identifier): Promise<BillingAccountDetail> // identifier = account id (UUID) | wallet | email
getAccount(identifier): Promise<BillingAccountDetail>
lookupAccount(identifier): Promise<BillingAccountDetail> // resolve wallet/email → account (incl. billing_account_id)
balance(identifier): Promise<BillingAccountDetail> // alias of checkBalance
history(identifier, limit?: number): Promise<BillingHistoryResult>
getHistory(identifier, limit?: number): Promise<BillingHistoryResult>
createCheckout(wallet, amountUsdMicros: number): Promise<CreateCheckoutResult>
Expand All @@ -1175,6 +1176,17 @@ createEmail(email): Promise<EmailBillingAccount>
autoRecharge(opts): Promise<void>
```

Accounts are addressed by their canonical `billing_account_id` (UUID).
`getAccount` / `checkBalance` / `history` accept an account id, wallet, or email:
an account id reads `GET /billing/v1/accounts/:account_id` directly, while a
wallet/email is resolved through the `GET /billing/v1/accounts?wallet=|?email=`
lookup (also exposed as `lookupAccount`). The detail shape dropped
`identifier_type` and added `billing_account_id`; `BillingBalance` is a
deprecated alias of `BillingAccountDetail`. Account reads require SIWX from a
wallet **linked to** the account (or matching the looked-up `?wallet`), or an
admin key — email lookups are admin-only; `history` resolves to the account id
first, then reads `GET /billing/v1/accounts/:account_id/history`.

`linkWallet` merges a wallet into an existing billing account's pool. On v1.46+
gateways the response includes a `pool_implications` block (`tier`,
`projects_in_pool_count`, `account_api_calls_current`, `account_storage_bytes_current`,
Expand Down
Loading
Loading