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 osgarden 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,
  "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.

POST /v1/wallet/topup — start a Stripe Checkout (dashboard-only)#

Body { "amount_usd"?: 25 } (min 5, max 10,000; defaults to 5) → { "url": "https://checkout.stripe.com/…" }. Redirect the user there; the wallet is credited when Stripe confirms payment (see webhook). Errors: 400 invalid_amount.

Preview mode (current production state): Stripe is deliberately not configured. The endpoint responds 503 topups_disabled with a thank-you detail (which the dashboard shows as an info notice), and records the attempt — user and amount — in the topup_interest table. That table is the demand signal that decides whether payments are worth enabling; usage meanwhile runs on the welcome credit.

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": "",
      "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).

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 osgarden.wallet.get();
const { url } = await osgarden.wallet.topup(25); // → redirect to Stripe

const { apps } = await osgarden.apps.list();
await osgarden.apps.setCap("app_client_id", 20); // USD/month; 0 blocks

The SDK surfaces 402 as an OsgardenError with message insufficient_balance or spend_cap_exceeded — apps should catch these and point the user at their dashboard.