Billing API — Wallet, Top-up & App Budgets
Wallet balance, Stripe top-ups, and per-app monthly spend caps. These are dashboard
surfaces: apps are untrusted and must not read a user's cross-app financial state or raise
their own cap. Endpoints marked dashboard-only reject any session whose OAuth client
isn't the tallpond dashboard (sessions with no app identity — the userinfo fallback —
are allowed, since budgets aren't enforced there anyway).
All /v1/* endpoints require authentication.
Wallet#
GET /v1/wallet — balance & recent activity (dashboard-only)#
{
"balance_micro_usd": 4985000,
"pending_micro_usd": 15000,
"status": "active",
"delinquent_since": null,
"transactions": [
{
"id": "…",
"amountMicroUsd": -15000,
"opType": "db_usage",
"settledAt": "…",
"metadata": {}
}
]
}
Amounts are in microdollars (1 USD = 1,000,000). pending_micro_usd is the sum of
open escrow holds. status is active, grace (negative balance — everything still
works), or frozen (data access blocked); delinquent_since is when the negative
balance started, null while active. See the delinquency lifecycle in
metering.md.
GET /v1/dashboard/spend?range=1d — grouped spend (dashboard-only)#
Returns every debit in the selected window aggregated by app and operation type;
it never paginates individual ledger rows. range is one of 1h, 1d (default),
1w, or 1m (30 days).
{
"range": "1d",
"since": "2026-08-24T12:00:00.000Z",
"until": "2026-08-25T12:00:00.000Z",
"total_micro_usd": 1532,
"apps": [
{
"app_id": "…",
"name": "Shared Notes",
"micro_usd": 1532,
"operations": 418,
"categories": [
{ "op_type": "db_usage", "micro_usd": 1210, "operations": 400 },
{ "op_type": "db_at_rest", "micro_usd": 322, "operations": 18 }
]
}
]
}
Amounts are positive costs in microdollars. Credits and top-ups are excluded. The immutable transaction ledger remains the source of truth; the endpoint performs an indexed bounded aggregation, avoiding a second cumulative balance that could drift.
POST /v1/wallet/topup — start a Stripe Checkout (dashboard-only)#
Dashboard-only routes require the authenticated token's OAuth client to match
DASHBOARD_CLIENT_ID; sessions without client identity are rejected.
Body { "amount_usd"?: 25 } (min 10, max 10,000; defaults to 10) →
{ "url": "https://checkout.stripe.com/…" }. Redirect the user there; the wallet is
credited when Stripe confirms payment (see webhook). Errors: 400 invalid_amount,
503 topups_disabled (Stripe misconfigured — not the normal production state).
A frozen account's discretionary ops (DB reads/writes, storage egress, file access,
AI/upload holds) return 402 account_frozen instead of running; a top-up that
restores a non-negative balance clears the freeze immediately.
POST /stripe/webhook — payment confirmation (Stripe-signed, no user auth)#
Verifies the stripe-signature against STRIPE_WEBHOOK_SECRET and, on
checkout.session.completed with payment_status: "paid", credits the wallet from the
session metadata. Idempotent per Stripe session. → { "received": true }.
App budgets#
A per-app monthly spend cap is the user's primary protection against a malicious or
buggy app. Every metered op checks it; hitting the cap fails 402 spend_cap_exceeded.
GET /v1/apps — my app budgets (dashboard-only)#
{
"apps": [
{
"app_id": "…",
"name": "Pad",
"url": "https://pad.tallpond.app",
"icon_url": "https://pad.tallpond.app/pad-icon.svg",
"cap_micro_usd": 20000000,
"spent_micro_usd": 1250000,
"pending_micro_usd": 0,
"window_start": "2026-07-01T00:00:00.000Z"
}
]
}
Spend is normalized to the current UTC month (window_start). icon_url is resolved
from the active release's declared HTML icon or web app manifest; clients should use it
directly rather than probing conventional favicon paths. It is null when the release
does not declare an icon.
PUT /v1/apps/:appId/cap — set an app's cap (dashboard-only)#
Body { "cap_usd": 20 } (0–10,000; 0 blocks the app entirely) →
{ "app_id": "…", "cap_micro_usd": 20000000 }. Errors: 400 invalid_cap.
Planned: spend-threshold notifications#
There is no global (cross-app) monthly cap by design — the per-app cap is the intended control surface, and a user can already zero out any app's cap to stop its spend. Threshold notifications (e.g. an email at 50/80/100% of an app's cap) are planned but not yet implemented — see not-yet-implemented.md.
New-user credit#
A user's first consent — including the silent skip path when Hydra remembers a prior
grant — credits a welcome balance ($5.00 default, tunable via WELCOME_CREDIT_USD;
see authentication.md). "New" means has never
received a welcome credit — deliberately not "has no ledger row", since sub-threshold
usage metering can create the ledger row (slightly negative) before the credit fires.
SDK#
const wallet = await tallpond.wallet.get();
const { url } = await tallpond.wallet.topup(25); // → redirect to Stripe
const { apps } = await tallpond.apps.list();
await tallpond.apps.setCap("app_client_id", 20); // USD/month; 0 blocks
The SDK surfaces 402 as an TallpondError with message insufficient_balance or
spend_cap_exceeded — apps should catch these and point the user at their dashboard.