---
name: run402
description: Build, deploy and operate full-stack applications with the Run402 CLI. Postgres, auth, storage, functions and hosting with a free prototype tier and an explicit spending allowance. Use for web apps, database-backed sites, APIs and deploy.
metadata:
  openclaw:
    emoji: "🐘"
    homepage: https://run402.com
    requires:
      bins:
        - npx
    install:
      - kind: node
        package: "run402"
        bins: [run402]
    primaryEnv: RUN402_API_BASE
---

# Run402 — Postgres, storage & deploys for AI agents

Run402 gives an agent a real Postgres database with REST API and user auth, content-addressed CDN storage, static site hosting, Node 22 serverless functions, email, image generation, and KMS-backed on-chain signing. One command provisions; payment happens automatically with x402 USDC on Base. Prototype tier is free on testnet — no real money or human signup for the routine cold-start path.

Run402 is agent-first because agents are first-class participants, not because people disappear. Act through your own Run402 principal and authenticator; do not borrow a human login, shared cloud account, or another agent's credential. Identity records who acted. Memberships, roles, grants, grant keys, freshness, and spend policy determine what you may do. A founder agent may legitimately own its org-of-one; an agent entering somebody else's organization uses bounded authority.

**Use the CLI by default.** General operating examples below use CLI commands; application code and manifests keep their native syntax. The typed, opinionated SDK owns shared orchestration, authentication, payments, validation and safe retries; CLI and MCP are thin adapters over it and the underlying HTTP API. Use the SDK for programmatic TypeScript/JavaScript workflows; shell scripts and CI can keep using CLI. The CLI prints JSON to stdout, JSON errors to stderr, and exits 0 on success and nonzero on failure or non-coherent verification (inspect the command’s documented status) — designed for shells, scripts, and agent loops.

## 30-second start

Install `npm install -g run402@latest`, then create the complete manifest and app files from https://run402.com/llms.txt in the intended application directory. `--name` explicitly requests a new project; `-y` approves required setup within that deploy intent.

```bash
run402 up --name my-app -y                 # bootstrap/link/deploy run402.json, run402.deploy.json, or app.json
run402 up verify                           # rerun app HTTP verification without deploying
run402 up --verify                         # deploy, then wait for gateway/edge release coherence
```

That's a real Postgres database + a deployed static site, paid for autonomously with testnet USDC.

`run402 up` is the CLI path for local repos with a deploy manifest. It classifies app-shaped `run402.json` through the app-install graph and release-shaped `run402.json` through the same ReleaseSpec normalizer as `run402 deploy`; malformed app manifests return `APP_SPEC_INVALID` instead of an internal JavaScript exception. It validates the manifest first, then recursively performs only missing prerequisites through the SDK action runner. Project resolution is `--project`, `.run402/project.json`, manifest `project_id`, approved creation from `--name`; global active state never selects a deploy target. `--check` returns nullable target intent, local evidence and explicit deferred gateway checks in `result.preflight`; it never claims gateway validation. `--print-manifest` exports reloadable snake_case JSON relative to the original manifest directory; `--print-spec` is advanced SDK inspection. Unsupported dynamic/secret/build constructs fail with field paths. `--plan` obtains a deploy plan; review it before approving deploy. If an app manifest defines `verify.http[]`, `up` reports fresh edge sentinel misses as `propagation_pending` while the host binding converges; tune with `--propagation-budget-s`, use `--no-propagation-wait` to return immediately, and run `run402 up verify` to rerun checks without upload, deploy, project creation, or resource mutation. Add `--verify` to a real deploy when you need `edge_coherence` evidence in the final JSON; a valid non-coherent report exits 2.

## MCP-only hosts and SDK scripting

If your host has no shell, use https://docs.run402.com/llms-mcp.txt for MCP setup and the `up` tool. The MCP server has eight tools; every other operation is a `run` snippet against the SDK client `r` (the `docs` tool is its reference), and an operation that returns or consumes a one-time secret refuses there with `SECRET_REQUIRES_CLI`, naming the CLI command to hand the person. Do not assume tools are installed. For typed scripting use https://docs.run402.com/sdk/scripting/ and the native SDK reference. Direct HTTP is supported for intentional protocol-level integrations at https://run402.com/llms-full.txt.

To update this skill, rerun the installation flow at https://run402.com/install.txt for the same host; a new discovery index does not update an already installed copy.

## Self-hosted Core target

Use the same commands against a self-hosted Run402 Core Gateway by configuring the API base once:

```bash
run402 init --api-base=http://my-core:4020
run402 projects provision --name my-app    # → anon_key, service_key, project_id
run402 deploy --manifest app.json --project <project_id>
```

For Core, `init --api-base` stores the target in the active profile and does not create a Cloud wallet, request faucet funds, or require a Cloud tier. The CLI, Node SDK, and MCP all read the same configured target by default. Unsupported Cloud-only manifest slices fail as Core capability errors; they are not silently deployed to Run402 Cloud.

## How to think about it

| You want to… | Reach for… |
|---|---|
| Set up a wallet from scratch | `run402 init` |
| Buy from an x402-priced URL | `run402 pay <url> --max-usd <amount>` |
| Redeem a promo code you were given | `run402 redeem <code>` (or `run402 init --voucher <code>`) |
| Bootstrap/link/deploy a manifest repo | `run402 up --name <name> -y` |
| Bootstrap/link/deploy and wait for edge coherence | `run402 up --name <name> -y --verify` |
| Rerun app HTTP verification | `run402 up verify --project <project_id>` |
| Point at self-hosted Core | `run402 init --api-base=<url>` |
| Make a database | `run402 projects provision` |
| Run SQL on it | `run402 projects sql` |
| Check an auth manifest before applying | `run402 projects validate-expose` |
| Make a table reachable from the browser | `run402 projects apply-expose` |
| Deploy a frontend from a directory | `run402 sites deploy-dir <path>` |
| Link GitHub Actions deploys | `run402 ci link github` |
| Stash a file with a paste-able CDN URL | `run402 assets put <file>` |
| Run code on the server | `run402 functions deploy` |
| Send email | `run402 email send` |
| Sign on-chain | `run402 contracts call` |
| One-call full-stack deploy | `run402 deploy --manifest app.json` |

The active project is sticky — `run402 projects use <project_id>` server-validates the project and makes it the default for every subsequent `<project_id>`-taking command. Most commands work without an explicit `<project_id>` once a project is active.

## Multiple wallets (profiles)

Hold several wallets on one machine - per-client isolation, personal vs work, blast-radius containment. Keys never leave the machine (non-custodial). The `default` wallet stays at `~/.config/run402/`; named wallets live under `~/.config/run402/profiles/<name>/`.

```bash
run402 wallets new kychon            # create a named wallet
run402 wallets list                  # name, label, address, rail, active
run402 wallets use kychon            # set the global default wallet
run402 wallets bind kychon           # write ./.run402.json - this repo uses kychon (safe to commit)
run402 wallets current               # which wallet is active + how it was selected
run402 --wallet kychon projects list # one-off override for a single command
```

Selection precedence for every command: `--wallet <name>` flag > `RUN402_WALLET` env > nearest `./.run402.json` directory binding > `wallets use` default > `default`. A `RUN402_WALLET` that disagrees with a `.run402.json` binding is a hard error - pass `--wallet`, `unset RUN402_WALLET`, or `run402 wallets unbind`. Non-default selections echo the wallet name on stderr so you always know which wallet you are on.

## Public Buzz/Nostr attribution

`run402 identity link nostr begin|complete` is the agent EOA-plus-kind-1 ceremony. Human accounts instead use <https://console.run402.com/identity-links/connect> for a normal browser, fresh-passkey, and Buzz approval flow; never ask a human to paste an event or handle a passkey/session/private key/id. Both yield the same public `idlnk_…` shape with a discriminated proof protocol. One principal may hold multiple subjects; a subject has only one active principal. Links are attribution only, never authentication or organization authority, and link/membership revocation are independent. Existing reads preserve all returned link ids, protocols, and lifecycle states.

`run402 buzz status` reports independent skill, adoption-offer/adoption, community-installation, and enrollment states. Use `run402 buzz adopt offer` for the durable HTTPS ownership transfer, and poll `offer show`; opening a link is never completion. A completed poll must show the terminal receipt, public human link, and ordinary owner membership separately. Only membership grants org authority; link/membership revocation are independent and the receipt remains completed. `buzz adopt direct` is advanced compatibility. MCP remains read-only for signing/passkey-sensitive work and returns exact HTTPS/CLI next steps.

## Project credentials

After `provision`, two keys are cached in the selected profile's `credentials/project-keys.v1.json`:

- `anon_key` — for the browser. Read-only by default; safe to embed in HTML. RLS still applies.
- `service_key` — server-side admin. Never embed in browser code. CORS is intentionally open for x402 clients, so a leaked service_key is exploitable from any origin. Use only inside functions or when running CLI as the agent.

Neither expires. Lease enforcement happens server-side. This is a local credential cache, not project inventory; server-visible projects come from `run402 projects list/get/use`.

```bash
run402 credentials project-keys list
run402 credentials project-keys status --project <project_id>      # redacted cache status
run402 credentials project-keys export --project <project_id> --reveal
run402 projects get <project_id>                                   # server project detail, no keys
```

## Error Envelopes and Safe Retry

Run402 JSON errors may include canonical fields. Branch on stable `code`, not English `message` or legacy `error` text.

Fields to use:
- `code`: machine-readable reason, e.g. `PROJECT_FROZEN`, `PAYMENT_REQUIRED`, `MIGRATION_FAILED`, `MIGRATE_GATE_ACTIVE`
- `retryable`: the same request may succeed later
- `safe_to_retry`: repeating the same request should not duplicate or corrupt a mutation
- `mutation_state`: one of `none`, `not_started`, `committed`, `rolled_back`, `partial`, `unknown`
- `trace_id`: include when reporting an issue
- `request_id`: routed/function failure handle; use `run402 functions logs <project_id> <name> --request-id <req_...>` for diagnostics. Distinct from gateway `trace_id`.
- `details`: structured route-specific context
- `next_actions`: advisory actions e.g. `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, `edit_migration`, `create_project`, `initialize_wallet`, `deploy`; never treat them as blindly executable. CLI-resolvable entries carry a literal `command`. Cold start: from `run402 deploy`, follow the chain it hands back — no local wallet -> `run402 init`, no tier -> `run402 tier set prototype`, no project -> `run402 projects provision` — then retry. `tier set` and `projects provision` accept `--idempotency-key` so retries never double-charge

Retry policy:
- Retry directly only when `retryable: true` and `safe_to_retry: true`; reuse the same idempotency key for mutating operations.
- `safe_to_retry: true` alone is not a retry signal; it means duplicate-safe, not likely-to-succeed. Lifecycle-gated writes, auth token exchanges, and passkey verifies need the indicated action before retrying.
- `run402 deploy` already handles safe `BASE_RELEASE_CONFLICT` release races for omitted/current-base specs by re-planning through the SDK. A handled retry appears as a `deploy.retry` stderr event; exhausted retries include `attempts`, `max_retries`, and `last_retry_code`. Static activation/config failures reported from `activation_pending` throw promptly with gateway metadata. Do not hand-roll this specific deploy race loop.
- For mutating 5xx errors with `safe_to_retry: false`, or `mutation_state: "committed"`, `"partial"`, or `"unknown"`, inspect/poll/reconcile state before retrying. For deploys, inspect events or resume the existing operation instead of starting a duplicate deploy.
- Lifecycle/payment codes usually require an action: `PROJECT_FROZEN`/`PROJECT_DORMANT`/`PROJECT_PAST_DUE` -> check usage or renew tier; `PAYMENT_REQUIRED`/`INSUFFICIENT_FUNDS` -> submit/fund payment.

Examples:
```json
{ "message": "Project is frozen.", "code": "PROJECT_FROZEN", "category": "lifecycle", "retryable": false, "safe_to_retry": true, "mutation_state": "none", "next_actions": [{ "type": "renew_tier" }, { "type": "check_usage" }] }
```

```json
{ "message": "Payment required.", "code": "PAYMENT_REQUIRED", "category": "payment", "retryable": true, "safe_to_retry": true, "next_actions": [{ "type": "submit_payment" }] }
```

```json
{ "message": "Migration failed.", "code": "MIGRATION_FAILED", "category": "deploy", "retryable": false, "safe_to_retry": true, "mutation_state": "rolled_back", "trace_id": "trc_...", "details": { "operation_id": "op_...", "phase": "migrate" }, "next_actions": [{ "type": "edit_migration" }] }
```

## Deploying

### Advanced primitive: `deploy-dir`

`deploy-dir` walks a local directory, hashes each file client-side, and only PUTs bytes the gateway doesn't already have. Re-deploying an unchanged tree returns immediately with `bytes_uploaded: 0`.

```bash
run402 sites deploy-dir ./dist > result.json 2> events.log
```

Skips `.git/`, `node_modules/`, `.DS_Store` automatically. Symlinks throw (no cycles).

`stderr` streams progress events — one JSON object per line:

| phase | When fired | Extra fields |
|---|---|---|
| `plan` | After the planning request | `manifest_size` (file count) |
| `upload` | After each missing file finishes PUTing | `file`, `sha256`, `done`, `total` |
| `commit` | Just before commit | — |
| `poll` | Per server-side copy poll | `status`, `elapsed_ms` |

Pass `--quiet` to suppress events; the final result envelope still goes to stdout.

### The deploy itself: `run402 deploy`

`run402 up` does any missing setup (wallet, tier, project, workspace link) and then deploys; `run402 deploy` only deploys. For a database + migrations + manifest + secret dependencies + functions + site + subdomain, set secret values first, then deploy a value-free manifest:

```bash
run402 secrets set <project_id> OPENAI_API_KEY --file ./.secrets/openai-key
run402 deploy --manifest app.json --final-only
```

Use `--final-only` (alias of `--quiet`) when an agent or CI job only wants the final stdout JSON envelope. Use repeatable `--allow-warning <code>` for reviewed warning codes; reserve broad `--allow-warnings` for reviewed exceptional cases.

After deploys, inspect release state without starting another mutation:

```bash
run402 deploy releases active --project prj_...
run402 deploy status op_... --project prj_...
run402 deploy verify op_... --project prj_... --wait --timeout 120
run402 deploy releases get rel_... --project prj_...
run402 deploy releases diff --from empty --to active --project prj_...
```

After pointer-swap recovery, compose promote with verification:

```bash
run402 deploy promote RELEASE_ID --project PROJECT_ID
run402 deploy verify --operation OPERATION_ID --wait
```

Take `OPERATION_ID` from the promote result. A successful promote means the origin pointer is active, while mutable public URLs can still be converging. Inspect `edge.state`; `edge.verify_url` is the direct operation-scoped HTTP verification endpoint.

Inventories expose site paths, `static_public_paths` when returned, functions, secret keys only, subdomains, materialized routes, applied migrations, `release_generation`, `static_manifest_sha256`, nullable `static_manifest_metadata`, and warnings when returned. `site.paths` is the release static asset inventory; `static_public_paths[]` is the browser reachability inventory with `public_path`, `asset_path`, `reachability_authority`, `direct`, cache class, and content type. `static_manifest_metadata: null` means unavailable, not zero. Release diffs use `migrations.applied_between_releases`, route `added` / `removed` / `changed` buckets, and `static_assets` counters for unchanged/changed/added/removed files, CAS byte reuse, eliminated deploy-copy bytes, and immutable/CAS warning counts.

#### Same-origin web routes

Use `run402 deploy` for `site.public_paths` clean static browser URLs and public browser routes to functions or exact method-aware static aliases. Release static asset paths and public browser paths are distinct: `events.html` can be a private release asset while `/events` is the public static URL.

```json
{
  "project_id": "prj_...",
  "site": { "replace": {
    "index.html": { "data": "<!doctype html><main id='app'></main><script>fetch('/api/hello')</script>" },
    "events.html": { "data": "<!doctype html><h1>Events</h1>" }
  }, "public_paths": {
    "mode": "explicit",
    "replace": {
      "/events": { "asset": "events.html", "cache_class": "html" }
    }
  } },
  "functions": {
    "replace": {
      "api": {
        "runtime": "node22",
        "source": { "data": "export default async function handler(req) { const url = new URL(req.url); return Response.json({ ok: true, path: url.pathname }); }" }
      },
      "login": {
        "runtime": "node22",
        "source": { "data": "export default async function handler(req) { return Response.json({ ok: true }); }" }
      }
    }
  },
  "routes": {
    "replace": [
      { "pattern": "/api/*", "methods": ["GET", "POST", "OPTIONS"], "target": { "type": "function", "name": "api" } },
      { "pattern": "/login", "methods": ["POST"], "target": { "type": "function", "name": "login" } }
    ]
  }
}
```

`site.public_paths.mode: "explicit"` means only the complete `public_paths.replace` table is directly reachable as static URLs. In the example, `/events` serves release asset `events.html`, while `/events.html` is not public unless separately declared. `{ "mode": "implicit" }` restores filename-derived public reachability and can widen access; review gateway warnings before confirming that switch. Public-path-only site specs are meaningful deploy content.

Omit `routes` or pass `routes: null` to carry forward base routes. Use `routes: { "replace": [] }` to clear the route table. Do not use path-keyed maps. Function targets use `{ "type": "function", "name": "<materialized function name>" }`. Prefer `site.public_paths` for ordinary clean static URLs e.g. `/events -> events.html`. Static route targets use exact patterns only, methods `["GET"]` or `["GET","HEAD"]`, and `{ "pattern": "/events", "methods": ["GET", "HEAD"], "target": { "type": "static", "file": "events.html" } }` for route-only aliases; `file` is a release static asset path, not a public path, URL, CAS hash, rewrite, or redirect. Direct `/functions/v1/:name` remains API-key protected; browser-routed paths are public same-origin ingress, so the function owns application auth, CSRF for cookie-authenticated unsafe methods, CORS/`OPTIONS`, cookies, redirects, and spoofed forwarding-header hygiene.

Function routes may declare fixed app x402 pricing: `{ "pattern": "/api/credits", "methods": ["POST"], "target": { "type": "function", "name": "credits" }, "pricing": { "mode": "always", "amount_usd_micros": 250000, "pay_to": "org_default_payout" } }`. `250000` is $0.25 per matching action. Portable ReleaseSpec also accepts `receipt: "on_fulfillment"` inside pricing; compatible hosts require `payment.fulfilled(response)` from `@run402/functions` after completed delivery. Run402-hosted evidence stays gated until the interoperable delegated-signer carrier exists and never silently downgrades receipt intent. Omit `networks` for production mainnet only; include `"testnet"` explicitly for testnet payments. Add `"lightning"` (or `"lightning-testnet"`) to sell for sats over x402 `exact` on Bitcoin Lightning, settled to the org's own node: register a receive-only Nostr Wallet Connect pairing once with `run402 orgs lightning-receiver set <org_id> -` (pairing on stdin; never in a snippet), optionally pin the sats price with `"amount_msat"`; a Lightning-only route needs no payout wallet, and the function sees `amountMsat` with no payer. Static aliases cannot be priced, direct function invocation is not monetized, and service/admin keys do not bypass a priced browser route. Before deploying priced routes, ensure the org has a payout wallet with `run402 orgs payout-wallet <org_id> <wallet_address>`. For conditional credits, use one fixed-price `/api/credits` route and keep the app envelope unpriced behind app-local auth. In the handler, import `getRoutedPaymentContext` from `@run402/functions`, call `getRoutedPaymentContext(req)`, and key idempotency by `payment.paymentId`; audit with `run402 projects tenant-payments <project_id>`.

Matching is exact or final `/*` prefix only. `/admin/*` does not match `/admin`; use both `/admin` and `/admin/*` for a dynamic area root. Query strings are ignored for matching and preserved in the handler's full public `req.url`. Exact beats prefix, longest prefix wins, and method-compatible dynamic routes beat static assets. A `POST /login` route can coexist with static `GET /login` HTML. Unsafe method mismatch returns `405`; matched dynamic route failures fail closed.

Routed functions use the Node 22 Fetch Request -> Response contract: `export default async function handler(req) { ... }`. `req.method` is the browser method, and `req.url` is the full public URL on managed subdomains, hosts, and verified custom domains. Derive OAuth callbacks from it, for example `new URL("/admin/oauth/google/callback", new URL(req.url).origin)`. Append multiple cookies with `headers.append("Set-Cookie", value)`; redirects, cookies, and query strings are preserved. The raw `run402.routed_http.v1` envelope is internal; do not write route handlers against it.

Use `run402 deploy resolve https://example.com/events --project prj_123 --method GET` before mutating deploy state when the question is "what would this public URL serve?" The host/path form is `run402 deploy resolve --host example.com --path /events --project prj_123 --method GET`; never combine a URL with `--host`/`--path`. Output is JSON with `status`, `would_serve`, `diagnostic_status`, `match`, normalized `request`, warnings, full `resolution`, `edge_propagation`, and structured `next_steps`. URL query strings/fragments are disclosed in `request.ignored`. When returned, `asset_path`, `reachability_authority`, and `direct` explain which release asset backs the public URL and whether reachability came from implicit file-path mode, explicit `site.public_paths`, or a route-only static alias. Stable-host diagnostics may also include `authorization_result`, `cas_object` (`sha256`, `exists`, `expected_size`, `actual_size`), hostname-specific `response_variant`, route/static fields e.g. `allow`, `route_pattern`, `target_type`, `target_name`, and `target_file`, plus `edge_propagation` (`settled`, `propagating`, or `sync_pending`; non-settled means retry or run `run402 up verify`). Known `match` literals are `host_missing`, `manifest_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, `spa_fallback_missing`, `route_function`, `route_static_alias`, and `route_method_miss`; preserve unknown future strings. Known `authorization_result` values include `authorized`, `not_public`, `not_applicable`, `manifest_missing`, `target_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `missing_cas_object`, `unfinalized_or_deleting_cas_object`, `size_mismatch`, and `unauthorized_cas_object`. Known `fallback_state` values include `active_release_missing`, `unsupported_manifest_version`, and `negative_cache_hit`; preserve unknown future strings. `result` is diagnostic body status, not CLI process status, so host misses can exit 0 with `would_serve: false`. Do not use diagnostics as a fetch, cache purge, or reason to hard-code `cache_policy` strings; branch on structured JSON e.g. `allow`, `cas_object`, and `edge_propagation`.

Known route warning recovery: `PUBLIC_ROUTED_FUNCTION` is informational (`requires_confirmation: false` — it never blocks and needs no `--allow-warning`): review app auth, CSRF, CORS/`OPTIONS`, and cookies. Only warnings with `requires_confirmation: true` need `--allow-warning <code>`; broad `--allow-warnings` is last resort after every warning is reviewed. `ROUTE_SHADOWS_STATIC_PATH` and `WILDCARD_ROUTE_SHADOWS_STATIC_PATHS` mean inspect affected paths, active routes, `static_public_paths`, and resolve diagnostics before confirming. `STATIC_ALIAS_SHADOWS_STATIC_PATH`, `STATIC_ALIAS_RELATIVE_ASSET_RISK`, `STATIC_ALIAS_DUPLICATE_CANONICAL_URL`, `STATIC_ALIAS_EXTENSIONLESS_NON_HTML`, and `STATIC_ALIAS_TABLE_NEAR_LIMIT` are route-only static alias warnings; prefer `site.public_paths` for ordinary clean URLs, inspect the backing `asset_path`, fix relative assets/canonical URLs, and avoid table-exhausting page-by-page routes. `ROUTE_TARGET_CARRIED_FORWARD` means inspect carried-forward function targets. `METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK` means confirm static fallback is intended. `WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS` means a wildcard API prefix only allows `GET`/`HEAD`; add mutation methods e.g. `POST`, omit methods for an API prefix, or, when the GET/HEAD final-wildcard function route is intentionally read-only, acknowledge with `--allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS`. `ROUTE_TABLE_NEAR_LIMIT` means consolidate routes. `ROUTES_NOT_ENABLED` means deploy without `routes` or request enablement. Runtime route failure codes to branch on: `ROUTE_MANIFEST_LOAD_FAILED` (manifest/propagation), `ROUTED_INVOKE_WORKER_SECRET_MISSING` (custom-domain Worker secret), `ROUTED_INVOKE_AUTH_FAILED` (internal invoke signature), `ROUTED_ROUTE_STALE` (selected route failed release revalidation), `ROUTE_METHOD_NOT_ALLOWED` (method mismatch), `PAYOUT_WALLET_REQUIRED` / `PAYOUT_WALLET_AMBIGUOUS` / `PAYOUT_WALLET_UNRESOLVED` (priced-route payout setup), `PAYMENT_PROOF_MISMATCH` (stale or wrong x402 proof), and `ROUTED_RESPONSE_TOO_LARGE` (body over 6 MiB).

#### Recipe: static home page + SPA shell

A SPA site ships `index.html` as the shell serving every unmatched route (match `spa_fallback`), so by default `GET /` serves the shell too. To serve a real static home page at `/` — real bytes under curl and without JavaScript — while keeping the shell for app routes, ship `home.html` at the site root alongside `index.html`, add an exact root static route alias to the manifest, and run `run402 deploy --manifest app.json`:

```json
{
  "project_id": "prj_...",
  "site": { "replace": {
    "index.html": { "data": "<!doctype html><main id='app'></main><script src='/app.js'></script>" },
    "home.html": { "data": "<!doctype html><h1>Welcome</h1><a href='/dashboard'>Open the app</a>" },
    "app.js": { "data": "/* SPA bootstrap */" }
  } },
  "routes": {
    "replace": [
      { "pattern": "/", "target": { "type": "static", "file": "home.html" } }
    ]
  }
}
```

Route matching runs before all static resolution — including the implicit `/` -> `index.html` root mapping — and SPA-fallback derivation is independent of the route table. So `GET /` serves `home.html` (match `route_static_alias`), unmatched app routes e.g. `/dashboard` still serve the `index.html` shell (match `spa_fallback`), and named static pages keep serving unchanged (match `static_exact`). Root placement of `home.html` keeps its relative asset URLs resolving identically to the direct file and avoids the `STATIC_ALIAS_RELATIVE_ASSET_RISK` warning.

Expect two non-blocking plan lints: `STATIC_ALIAS_SHADOWS_STATIC_PATH` (warn — the alias overrides what `/` would otherwise serve; for this recipe that is accurate and expected, and the commit proceeds) and `STATIC_ALIAS_DUPLICATE_CANONICAL_URL` (info — `/home.html` stays directly reachable in implicit public-path mode; add `<link rel="canonical" href="https://<your-site>/">` to `home.html` if duplicate-content SEO matters). Omitting `routes` on later deploys carries the alias forward (informational `ROUTE_TARGET_CARRIED_FORWARD`); a pipeline that sends `routes.replace` must include the alias every time because replace is total. Verify with `run402 deploy resolve --project prj_123 --url https://<your-site>/ --method GET` and confirm `match: "route_static_alias"` with `target_file: "home.html"`.

#### Routed functions: locale awareness

Declare supported locales as a `spec.i18n` release slice and the gateway negotiates a locale per routed-function request, then surfaces it to user code through two request headers. Add `i18n` to the deploy manifest alongside `functions` and `routes` and run `run402 deploy --manifest run402.deploy.json`:

```json
{
  "project_id": "prj_...",
  "functions": {
    "replace": {
      "api": {
        "runtime": "node22",
        "source": { "data": "export default async (req) => { const locale = req.headers.get('x-run402-locale'); const def = req.headers.get('x-run402-default-locale'); return Response.json({ locale, default: def }); }" }
      }
    }
  },
  "routes": {
    "replace": [
      { "pattern": "/api/*", "target": { "type": "function", "name": "api" } }
    ]
  },
  "i18n": {
    "defaultLocale": "en",
    "locales": ["en", "es", "fr", "zh-Hant"],
    "detect": ["cookie:wl_locale", "accept-language"]
  }
}
```

Carry-forward semantics: omit `i18n` to carry forward from base release; pass `"i18n": null` to clear the slice on the new release; pass `{ defaultLocale, locales, detect? }` to replace. Simpler than `routes` — no `{ replace }` envelope.

Locale-tag rules (strict, no canonicalization):
- `defaultLocale` MUST be byte-identical to one entry in `locales[]`. The gateway does NOT silently canonicalize; the CLI/SDK validate this client-side before planning.
- Each tag MUST match `/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`. Tags are opaque — no BCP-47 semantic validation.
- `locales[]` is non-empty, max 50 entries.
- Negotiation returns canonical casing from `locales[]`, NOT the request's casing.

Detection (`detect[]`, default `["accept-language"]`, max 10, `[]` allowed and means "always default"):
- Walked in order; first match wins.
- `"accept-language"` parses per RFC 9110, drops `q=0` and `*`, sorts by q descending; applies RFC 4647 §3.4 lookup-style truncation (`zh-Hant-TW` → `zh-Hant` → `zh`); longest matching prefix wins. A generic request tag does NOT match a more-specific `locales[]` entry — `Accept-Language: es` does NOT match `locales: ["es-MX"]`.
- `"cookie:<name>"` does a case-sensitive cookie-name lookup; the raw cookie value (no percent-decode) is matched case-insensitively against `locales[]`. Cookie names MUST match RFC 6265 grammar (`/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/`).

Read the negotiated locale inside a routed function (single-arg `(req)` signature, not `(req, ctx)`):

```ts
export default async (req) => {
  const locale = req.headers.get('x-run402-locale');
  const defaultLocale = req.headers.get('x-run402-default-locale');
  if (locale && locale !== defaultLocale) {
    return renderWithTranslations({ locale });
  }
  return renderBase({ locale: defaultLocale ?? 'en' });
};
```

`x-run402-locale` and `x-run402-default-locale` are OMITTED entirely when the active release has no `i18n` slice (additive-compat). The gateway injects them at request time, so already-deployed function bundles see new headers on the next deploy that adds `i18n` — no function redeploy required. The bundled `@run402/functions` runtime translates the routed envelope into a Web-standard `Request` before calling user code, so the routed-envelope `context.locale` is NOT visible to typical user functions — read the headers instead.

Static-route hits do NOT receive locale negotiation; only routed HTTP function invocations do. Run402 does NOT inject `Vary` headers — apps that return public-cacheable responses varying by locale must set their own `Vary` until per-locale edge caching ships.

#### Routed functions: visitor geo (`x-run402-country`)

Routed function invocations carry the visitor's country as an edge-authoritative request header — identical on managed subdomains (`<sub>.run402.com`) and verified custom domains, with zero config (no release slice, always on when the edge knows the country):

```ts
export default async (req) => {
  const country = req.headers.get('x-run402-country') ?? 'unknown'; // e.g. "DE"
  return Response.json({ country });
};
```

- Value is ISO 3166-1 alpha-2 verbatim from the edge, plus the specials `T1` (Tor exit) and `XX` (unknown) on Cloudflare-fronted custom domains.
- When the edge supplies no geo the header is **absent** — never empty, never a guessed value — so `?? 'unknown'` (or your preferred fallback) at the read site.
- Spoof-proof: client-supplied `x-run402-country`, `cf-ipcountry`, and `cloudfront-viewer-country` are scrubbed at both edges; the header your function sees is always minted by the platform from edge geo data.
- Compat: on custom domains only, the legacy `cf-ipcountry` header is also present with the same value. Prefer `x-run402-country` — it works on every hostname.
- Static-route hits do NOT receive geo; only routed HTTP function invocations do. Country-varying public-cacheable responses must set their own `Vary`, same as locale.

#### Client-side gotcha: language switchers must write a cookie

Apps that persist locale to `localStorage` only (a common pattern from Astro/Next i18n tutorials) won't be seen by Run402's server-side negotiation. Mirror the locale to a cookie so the next request hits the right translations, then declare a cookie source in `spec.i18n.detect`:

```js
function setLanguage(lang) {
  localStorage.setItem('wl_locale', lang);
  document.cookie =
    `wl_locale=${encodeURIComponent(lang)}; path=/; max-age=31536000; samesite=lax`;
}
```

```json
{ "i18n": { "defaultLocale": "en", "locales": ["en", "es"], "detect": ["cookie:wl_locale", "accept-language"] } }
```

The deploy manifest is a v2 `ReleaseSpec`; put the auth manifest under `database.expose`:

```json
{
  "project_id": "prj_…",
  "database": {
    "migrations": [{ "id": "001_init", "sql_path": "setup.sql" }],
    "expose": {
      "$schema": "https://run402.com/schemas/manifest.v1.json",
      "version": "1",
      "tables": [
        { "name": "items", "expose": true, "policy": "user_owns_rows",
          "owner_column": "user_id", "force_owner_on_insert": true }
      ]
    }
  },
  "secrets": { "require": ["OPENAI_API_KEY"] },
  "functions": {
    "replace": {
      "my-fn": {
        "runtime": "node22",
        "source": { "data": "export default async (req) => new Response('ok')" },
        "config": { "timeoutSeconds": 30, "memoryMb": 256 }
      }
    }
  },
  "site": {
    "replace": {
      "index.html": { "data": "<!doctype html>…" },
      "logo.png": { "data": "iVBORw0…", "encoding": "base64" }
    }
  },
  "subdomains": { "set": ["my-app"] }
}
```

The `database.expose` entry is auth-as-SDLC — your authorization travels with the release. The gateway validates it against the migration SQL and applies it atomically. If the manifest references a table the migration doesn't create, the deploy is rejected with HTTP 400 and a structured `errors` array listing every violation.

For a new app, load `/_run402/config.js` and read `window.RUN402` at runtime, then deploy the complete files with `run402 up --name <name>`. Manual provisioning and pasted public keys are only needed for deliberate external-host integrations.

### GitHub Actions OIDC deploys

Link once locally, then let GitHub Actions run the same deploy command agents already use:

```bash
run402 ci link github --project prj_... --manifest run402.deploy.json
# Optional route authority for CI route declarations:
run402 ci link github --project prj_... --manifest run402.deploy.json --route-scope /admin --route-scope /api/*
git add .github/workflows/run402-deploy.yml run402.deploy.json
git commit -m "Add run402 deploy workflow"
```

The generated workflow uses a pinned `run402@<current>` CLI via `npx`, includes `permissions: id-token: write` and `contents: read`, and runs:

```bash
run402 deploy --manifest run402.deploy.json --project prj_...
```

Useful follow-ups:

```bash
run402 ci list --project prj_...
run402 ci revoke cib_...
```

V1 intentionally keeps the shape narrow: `push` and `workflow_dispatch` only, no PR deploy flags, no raw subject or wildcard flags, and no soft repository-id binding. Without `--route-scope`, CI cannot deploy `routes`; with repeatable route scopes, it may deploy only matching exact paths e.g. `/admin` or final-wildcard prefixes e.g. `/api/*`. If CI returns `CI_ROUTE_SCOPE_DENIED`, re-link with covering scopes or run the route-changing deploy locally. Revocation stops future CI gateway requests but does not undo already-deployed code, stop in-flight deploy operations, rotate exfiltrated keys, or remove deployed functions.

## Authorization — the expose manifest

Tables you create are dark by default. Until your manifest declares a table with `expose: true`, it's invisible to anon and authenticated callers. This eliminates the "agent forgot RLS, data leaked" footgun. The manifest is the single source of truth for what's reachable via `/rest/v1/*`. A valid anon key against an existing but undeclared table gets a structured `403 TABLE_NOT_EXPOSED` (never a bare Postgres `42501`) whose `next_actions` say exactly that: `expose_table` (declare it in `database.expose.tables` and `run402 up -y`), `edit_request` (the expose endpoint), or `use_function` (keep it dark and read it from a function with `adminDb()`). It is not an RLS problem.

JSON Schema: <https://run402.com/schemas/manifest.v1.json>. Set `$schema` on your manifest file and your editor gets autocomplete for free.

### Preferred: ship `manifest.json` in your bundle

Authorization travels with your code. Put a file named `manifest.json` in the bundle's `files[]` and the gateway reads it, validates it against your migration SQL, applies it, and strips it from `files[]` before the site deploys — so it's never publicly reachable on your subdomain. The deploy response includes `manifest_applied: true` on success.

```json
{
  "$schema": "https://run402.com/schemas/manifest.v1.json",
  "version": "1",
  "tables": [
    { "name": "items", "expose": true, "policy": "user_owns_rows",
      "owner_column": "user_id", "force_owner_on_insert": true },
    { "name": "audit", "expose": false }
  ],
  "views": [
    { "name": "leaderboard", "base": "items", "select": ["user_id", "score"], "expose": true }
  ],
  "rpcs": [
    { "name": "compute_streak", "signature": "(user_id uuid)", "grant_to": ["authenticated"] }
  ]
}
```

If the manifest references a table the migration doesn't create, the deploy is rejected with HTTP 400 and a structured `errors` array listing every violation (not the first).

### Non-mutating validation: `validate-expose`

Before applying, run:

```bash
run402 projects validate-expose [id] --file manifest.json --migration-file migrations.sql
```

This validates the auth/expose manifest used by `manifest.json`, `database.expose`, and `apply-expose`; it is not deploy-manifest validation. Migration SQL is used only for reference checks and is not executed as a PostgreSQL dry run. The command prints `{ "hasErrors": boolean, "errors": [...], "warnings": [...] }` and exits 0 even when `hasErrors` is true — the validator ran, so the command succeeded; read `hasErrors` to decide what to do.

### Imperative escape hatch: `apply-expose`

For ad-hoc changes outside a deploy — same JSON shape, no bundle:

```bash
run402 projects apply-expose <project_id> --file manifest.json
run402 projects get-expose   <project_id>     # source: "applied" | "introspected"
```

`get-expose` returns the live state. `source: "applied"` means it came from a prior `apply-expose` (or a bundled `manifest.json`); `"introspected"` means no manifest has ever been applied and the response was reconstructed from live DB state.

Convergent: applying the same manifest twice is a no-op. Items removed between applies have their policies, grants, triggers, and views dropped. Always include everything you want exposed.

### Built-in policies

| Policy | Allows |
|---|---|
| `user_owns_rows` | Rows where `owner_column = auth.uid()`. With `force_owner_on_insert: true`, a BEFORE INSERT trigger sets it automatically. Default for anything user-scoped. |
| `public_read_authenticated_write` | Anyone reads. Any authenticated user writes any row. For shared boards / collaborative content. |
| `public_read_write_UNRESTRICTED` | Fully open. Requires `i_understand_this_is_unrestricted: true` on the table entry. Only for guestbooks / waitlists / feedback forms. |
| `custom` | Escape hatch. Provide `custom_sql` with `CREATE POLICY` statements. |

Views always run with `security_invoker=true` — they inherit the underlying table's RLS, so they can't accidentally leak hidden columns. RPCs are not exposed unless listed in `rpcs[]` (a database event trigger revokes PUBLIC EXECUTE on every newly-created function).

## Storage — paste-and-go assets

`run402 assets put` returns an `AssetRef`. The URL is content-addressed (`pr-<public_id>.run402.com/_blob/<key>-<8hex>.<ext>`), served through CloudFront, and never needs cache invalidation:

```bash
run402 assets put ./logo.png    --json
run402 assets put ./app.js      --json
run402 assets put ./styles.css  --json
run402 assets put ./asset       --key assets/logo --content-type image/svg+xml --json
run402 assets put ./hero.jpg    --meta uploaded_by=agent_abc --meta tags=hero,banner --exif-policy strip --json
```

Each response includes:

| Field | Use |
|---|---|
| `cdn_url` | The content-addressed URL — paste straight into `src=` / `href=` |
| `sri` | `sha256-<base64>` for `<script integrity="…">` if you build tags by hand |
| `etag` | Strong `"sha256-<hex>"` ETag |
| `cache_kind` | `immutable` / `mutable` / `private` |
| `metadata` | Echo of your `--meta` bag, or `null` |
| `image_format` | Decoded format (`jpeg`/`png`/`webp`/…) or `null` for non-images |
| `image_info` | `has_alpha`, `color_space`, `animated`, `frame_count`, `bit_depth`, `orientation` |
| `image_exif` | EXIF block (`null` when stripped or no EXIF) |
| `image_exif_policy` | `keep` or `strip`, echoing the applied policy |

Immutable upload is the default — the SDK computes the SHA-256 client-side and pairs the URL with SRI. The browser refuses execution on byte mismatch. No invalidation choreography.

`blob put` infers MIME type from the destination key. Use `--content-type <mime>` for extensionless assets, unusual file types, or deliberate overrides.

**`--meta` coercion:** pure digits become a number, `true`/`false` become a boolean, values containing `,` become a `string[]`, everything else is a string. ≤4 KB serialized; bad shapes are rejected client-side with `INVALID_ASSET_METADATA` before any HTTP call.

### List, fetch, remove, sign

```bash
run402 assets ls --prefix images/
run402 assets ls --sort createdAt:desc --filter is_image=true --filter min_width=320 --filter format=webp
run402 assets ls --filter uploaded_by=agent_abc --filter tag=hero
run402 assets get images/logo.png --output /tmp/logo.png
run402 assets rm images/old.png
run402 assets sign secrets/report.pdf --ttl 3600    # presigned URL for private blobs
```

`--sort` takes `key:asc` (default), `createdAt:asc`, or `createdAt:desc`. Cursors are sort-pinned — cross-sort reuse returns `INVALID_CURSOR_FOR_SORT`. `--filter k=v` is repeatable; allowed keys: `uploaded_by`, `tag`, `format`, `is_image`, `min_width`/`max_width`/`min_height`/`max_height`. Unknown keys are rejected client-side with `INVALID_FILTER_KEY`.

### Diagnose a stale CDN

```bash
run402 assets diagnose <url>                                  # exit 0 if fresh, 1 if stale
run402 cdn wait-fresh <url> --sha <hex> --timeout 120       # poll until fresh
```

`diagnose` is shell-loop friendly: `until run402 assets diagnose <url>; do sleep 1; done` blocks until the CDN catches up. Vantage is single-region (us-east-1) — other PoPs may differ. Don't call `wait-fresh` on immutable URLs — they're correct from the moment of upload.

## Database

```bash
run402 projects sql <project_id> "CREATE TABLE items (id serial PRIMARY KEY, title text NOT NULL, user_id uuid)"
run402 projects sql <project_id> --file migrations.sql
run402 projects sql <project_id> "SELECT * FROM items WHERE id = \$1" --params '[42]'

run402 projects rest <project_id> items "select=id,title&order=id.desc&limit=10"
run402 projects validate-expose <project_id> --file manifest.json --migration-file migrations.sql
run402 projects schema <project_id>          # introspect tables, columns, RLS
run402 projects usage  <project_id>          # API calls, storage, lease expiry
run402 projects costs <project_id> --window 30d  # staff-only finance; admin wallet required
```

### Idempotent migrations

`CREATE TABLE IF NOT EXISTS` only handles "already exists" errors — it won't add new columns to an existing table. For evolving schemas, wrap `ALTER TABLE` in a `DO` block:

```sql
CREATE TABLE IF NOT EXISTS items (id serial PRIMARY KEY, title text NOT NULL);
DO $$ BEGIN
  ALTER TABLE items ADD COLUMN priority int DEFAULT 0;
EXCEPTION WHEN duplicate_column THEN NULL;
END $$;
```

This pattern is safe to re-run on every deploy.

### SQL guardrails

The SQL endpoint blocks: `CREATE EXTENSION`, `COPY ... PROGRAM`, `ALTER SYSTEM`, `SET search_path`, `CREATE/DROP SCHEMA`, `GRANT/REVOKE`, `CREATE/DROP ROLE`. Table and sequence permissions are granted automatically — use the expose manifest for access control instead of `GRANT`.

## Functions

Node 22 runtime. Handler: `export default async (req: Request) => Response`.

```bash
run402 functions deploy <project_id> my-fn --file fn.ts \
  --timeout 30 --memory 256 \
  --schedule "*/15 * * * *" \
  --deps "stripe,zod@^3,date-fns@3.6.0"

run402 functions invoke my-fn --project <project_id> --body-file request.json
run402 functions invoke paid-fn --project <project_id> --body-file request.json --idempotency-key paid:call:123 --wait
run402 functions logs   <project_id> my-fn --tail 100 --request-id req_abc123 --follow
run402 functions runs create <project_id> my-fn --event-type reminder.send --idempotency-key reminder:123 --delay 10m
run402 functions update <project_id> my-fn --schedule "0 */6 * * *"
run402 functions rebuild <project_id> my-fn      # refresh ONE function onto the current platform runtime
run402 functions rebuild <project_id> --all      # refresh every function in the project
run402 functions list   <project_id>
run402 functions delete <project_id> my-fn
```

`functions invoke --body <json>` validates JSON locally and never sends malformed or empty input. Prefer `--body-file <path>` for agents and Windows `cmd.exe`; it removes shell quoting from the request-body path.

`run402 functions list` returns the recorded `runtime_version`, gateway `runtime_current_version`, guaranteed `runtime_minimum_version`, and `runtime_stale` for each function. The current `3.7.0` floor includes `getRoutedPaymentContext()` for priced routes. `run402 functions rebuild` is opt-in and never changes your source: it re-bundles the stored source against the platform's current runtime/entry-wrapper (deps pinned to the versions recorded at deploy), so the source `code_hash` is unchanged and no new release is created. This is how a gateway-side wrapper fix (e.g. an SSR `auth.*` fix) reaches an already-deployed function — a plain redeploy with unchanged source does not. Functions deployed before dependency locking return `CANNOT_REBUILD_UNLOCKED_DEPS`; redeploy those from source instead.

`run402 functions runs` manages durable function requests. `create` requires `--event-type` and `--idempotency-key`, accepts `--payload-json`, `--delay` or `--run-at`, expiry, retry options, and optional `--wait`. `list`, `get`, `logs`, `cancel`, and `redrive` inspect and recover work by `fnrun_...`. Use durable runs for delayed work, webhook redrive, and retry-safe background tasks instead of ad hoc cron tables.

`--deps` accepts npm specs: bare names (`lodash`) resolve to the latest version at deploy time, pinned (`lodash@4.17.21`) and ranges (`date-fns@^3.0.0`) are honored verbatim. Max 30 entries, 200 chars each. Native binary modules (`sharp`, `canvas`, native bcrypt, etc.) are rejected — pure JS only. Don't list `@run402/functions` (auto-bundled).

The deploy response surfaces:
- `runtime_version` — the bundled `@run402/functions` version
- `deps_resolved` — `{ name: version }` for every package the gateway pinned, including transitives
- `warnings` — non-fatal notes (e.g. when a bare spec resolved to a version that's later than what's typical)

### Inside the function — `@run402/functions`

The one place to write code, not commands. Built-in helpers are auto-bundled:

```ts
import { db, adminDb, auth, email, ai, assets } from "@run402/functions";

export default async (req: Request) => {
  const user = await auth.user();
  if (!user) return new Response("unauthorized", { status: 401 });

  // Caller-context — Authorization header is forwarded; RLS evaluates against the caller's role.
  // Do not add `.eq("user_id", user.id)`; RLS already binds the visitor's rows.
  const mine = await db(req).from("items").select("*");

  // Bypass RLS — only when the function acts on behalf of the platform.
  await adminDb().from("audit").insert({ event: "items_read", user_id: user.id });

  if (mine.length === 0) {
    await email.send({ to: user.email, subject: "Welcome", html: "<h1>Hi</h1>" });
  }

  return Response.json(mine);
};
```

- `db(req)` — caller-context. Forwards Authorization header. RLS applies. Default choice.
- `adminDb()` — bypass RLS. Use only for audit logs, cron cleanup, webhook handlers, platform-authored writes.
- `adminDb().sql(query, params?)` — raw parameterized SQL. Always bypass.
- `ai.generateImage({ prompt, aspect? })` — live image generation from deployed functions, billed/rate-limited against the project organization through `RUN402_SERVICE_KEY`. Aspects: `square`, `landscape`, `portrait`; result: `{ image, content_type, aspect }`. For public routed functions, authenticate/rate-limit app users before calling it.
- `assets.put(key, source, opts?)` — upload runtime bytes through the same CAS-backed apply substrate as deploy-time assets. `source` is a string, `Uint8Array`, or `{ content | bytes }`; returns an SDK-compatible `AssetRef`.
- `auth.*` — canonical cookie/session auth namespace (`auth.user`, `auth.requireUser`, `auth.requireRole`, `auth.requireMembership`, `auth.fetch`, `auth.sessions.*`, `auth.identities.link`). Bare helpers such as `getUser`, `getUserId`, and `getRole` are not exported — they throw `R402_AUTH_UNKNOWN_EXPORT` and fail `run402 doctor`.
- Function-level gate headers — when `FunctionSpec.requireAuth` / `requireRole` passes, read `req.headers.get("x-run402-user-id")` and `req.headers.get("x-run402-user-role")` directly. Use these inside a gated function instead of re-decoding the JWT; the gate already verified the caller and resolved the application role.
- `getRoutedPaymentContext(req)` (`@run402/functions` 3.7+) — confirmed x402 payment context for priced routed function requests. Returns `{ scheme, paymentId, amountUsdMicros, payer, network, asset, payTo, transaction, settledAt }` or `null`; key app-side idempotency by `payment.paymentId`.

Fluent surface on both `db(req).from(t)` and `adminDb().from(t)`:
- Reads: `.select()`, `.eq()`, `.neq()`, `.gt()`, `.lt()`, `.gte()`, `.lte()`, `.like()`, `.ilike()`, `.in()`, `.order()`, `.limit()`, `.offset()`
- Writes: `.insert(obj | obj[])`, `.update(obj)`, `.delete()` — return arrays of affected rows
- Column narrowing on writes: `.insert({…}).select("id, title")`

For TypeScript autocomplete in your editor: `npm install @run402/functions` in your project. Also works at build time for static-site generation if you set `RUN402_SERVICE_KEY` + `RUN402_PROJECT_ID` in `.env`.

### Function-level auth gates

Skip the hand-rolled "decode JWT → query members table → return 403" boilerplate. Declare the gate on your `FunctionSpec` and the gateway enforces it before invoking the function. Unauthorized callers get `401`/`403` without your code running, and the gateway injects the resolved identity into request headers your function can trust.

Two independent fields on `FunctionSpec`:

- `requireAuth: true` — gateway rejects callers without a valid project user JWT with `401`. No DB lookup.
- `requireRole: { table, idColumn, roleColumn, allowed[], cacheTtl? }` — gateway resolves the caller's role from the project-schema table (RLS-bypass — the gateway is the trusted intermediary, not the caller) and rejects callers whose role is not in `allowed` with `403`. Implies authentication.

Declare in your deploy manifest and run `run402 deploy --manifest run402.deploy.json`:

```json
{
  "project_id": "prj_...",
  "functions": {
    "patch": {
      "set": {
        "list-my-items": {
          "source": { "path": "functions/list.ts" },
          "requireAuth": true
        },
        "delete-content": {
          "source": { "path": "functions/delete.ts" },
          "requireRole": {
            "table": "members",
            "idColumn": "user_id",
            "roleColumn": "role",
            "allowed": ["admin"],
            "cacheTtl": 60
          }
        },
        "moderate-content": {
          "source": { "path": "functions/moderate.ts" },
          "requireRole": {
            "table": "members",
            "idColumn": "user_id",
            "roleColumn": "role",
            "allowed": ["admin", "moderator"]
          }
        }
      }
    }
  }
}
```

Reading the gate result inside the function:

```ts
export default async (req: Request): Promise<Response> => {
  const userId = req.headers.get("x-run402-user-id");
  const role = req.headers.get("x-run402-user-role");
  // For a gated function reached through the gateway:
  //   x-run402-user-id is non-null whenever any gate ran;
  //   x-run402-user-role is non-null whenever requireRole ran (one of `allowed`).
  return Response.json({ actor: userId, role });
};
```

Rules and footnotes:

- One role table per release. All `requireRole` blocks in a single release must share the same `(table, idColumn, roleColumn)` triple. Different `allowed` sets are fine; different tables are rejected at plan time with `INVALID_SPEC`.
- Unqualified identifiers. Schema-qualified names (e.g. `"public.members"`) are rejected. The project schema is resolved server-side.
- Deploy-time validation. Missing table or column at activation fails with `DEPLOY_INVALID_ROLE_GATE` (422) *before* flipping the live release. `run402 deploy` surfaces the error envelope on stderr.
- Cache TTL. Default 60s, max 600s. A demoted user keeps the cached role until expiry — for instant revocation, set `cacheTtl: 0` (fresh lookup per request).
- Gate applies to both routed (`/your/route`) and direct (`POST /functions/v1/:name` with API key) invocation. Direct invocation still requires the API key at the edge; the gate runs after API-key auth, against the user JWT.

### Astro SSR runtime + ISR cache

For Astro apps, scaffold with `run402 init astro` (sets up `astro.config.mjs` with the `@run402/astro` 1.0+ preset, `[slug].astro` SSR template wired to the DB, layouts, `.env.example`). Run `run402 dev` to start `astro dev` with project credentials in scope.

Functions opt into the SSR class declaratively:

```json
{
  "functions": {
    "ssr": {
      "class": "ssr",
      "code": { "data": "...", "encoding": "base64" }
    }
  }
}
```

The gateway provisions SnapStart-enabled Lambda and reverse-validates the published version before activation. Failure ships the function anyway and surfaces `DEPLOY_FUNCTION_SSR_SNAPSTART_VALIDATION_FAILED` as a non-blocking warning.

Cache is bypass-by-default. SSR responses only get cached when `Cache-Control` explicitly allows it AND no `Set-Cookie` AND no auth-taint flag — `auth.*` helpers automatically taint per-request caching so personalized renders never get stored.

**Invalidate from the CLI:**

- `run402 cache invalidate https://eagles.kychon.com/the-guys` — single URL
- `run402 cache invalidate https://eagles.kychon.com/blog --prefix` — path prefix
- `run402 cache invalidate https://eagles.kychon.com --all` — all rows for the host
- `run402 cache inspect https://eagles.kychon.com/the-guys` — peek at cache state

Host ownership is server-validated; cross-project hosts throw `R402_CACHE_INVALIDATION_HOST_FORBIDDEN`. Writes are generation-guarded: in-flight MISS renders started before an invalidate cannot overwrite the freshly-cleared state.

**Agent-DX shortcuts:**

- `run402 doctor` — config/credential/network/tier/vault/source-scan health checks. Output is JSON by default (no flag needed); exit 1 on fail. `--only <check>` (repeatable) runs just the named check(s) and suppresses the rest, including the monorepo source scan — e.g. `run402 doctor --only vault` for a fast, uncluttered vault diagnosis; `run402 doctor --help` lists every check name. `--project <project_id>` scopes the vault check to a specific project instead of the repo-standing default.
- `run402 dev` — runs `npx astro dev` with `.env.local` + Run402 credentials in scope.
- `run402 logs --request-id req_XYZ` — fetch logs for a specific request id across every function in the project (parallel scan, timestamp-ascending merge).

Reference: [`astro/README.md`](../astro/README.md) (top section), [`cli/llms-cli.txt`](../cli/llms-cli.txt) (R402_* SSR Runtime Error Codes section).

## Rehearsals, snapshots, and branches

Rehearsal is automatic: a migration-bearing `run402 up` / `run402 deploy` against a project with a live release is rehearsed on a contained branch and committed only on a passing report (`--no-rehearse` skips it). To rehearse a persisted plan by hand without committing:

```bash
run402 deploy --manifest app.json --plan --json          # a reviewed plan_id, nothing committed
run402 deploy rehearse <plan_id> --project prj_... --teardown on_pass --json
run402 deploy --require-plan <plan_id>                   # commit only if the plan still matches
```

Manual restore points and branches are reference primitives:

```bash
run402 snapshots create prj_... --json
run402 snapshots list prj_... --json
run402 snapshots restore prj_... snap_... --json        # plan only
run402 snapshots restore prj_... snap_... --confirm <token> --json
run402 branches create prj_... --ttl-days 7 --json
run402 branches list prj_... --json
run402 branches renew prj_... br_... --ttl-days 7 --json
run402 branches delete prj_... br_... --json
```

Snapshot restore is deliberately two-step so the person or agent sees the data-loss plan before mutation. Branches default to a 7-day TTL, use derived noindex hosts, sandbox email unless explicitly disabled, and keep scheduled functions off unless requested.

## Portable project archives (Cloud -> Core)

Use portable archives when the person wants no vendor lock-in for the supported Run402 Core runtime slice. This is a portability trust claim: Cloud is the easiest place to start, not the only place the supported application can run. Keep it separate from allowance/spend-cap financial-risk claims.

Canonical CLI path:

```bash
run402 archives create <project_id> --target cloud --scope portable-runtime-v1 --auth stubs --consistency pause-writes --wait --output ./project.r402ar --json
run402 archives inspect ./project.r402ar --json
run402 archives verify ./project.r402ar --json
run402 archives import ./project.r402ar --target core --name imported-project --env-file ./required.env --json
```

`--target cloud` (the default for `create`, `status`, and `download`) exports from Run402 Cloud; `--target core` (the default for `import`) loads into a local Run402 Core. From MCP the same flow is a `run` snippet over `r.archives`. SDK helpers live under `r.archives`; the Node entry adds local `inspect`, `verify`, and `importToCore`, plus standalone `inspectArchive`, `verifyArchive`, and `importArchiveToCore`.

Archive v1 exports active release/apply state, supported Postgres/RLS/REST data, storage/static bytes, functions, Astro SSR artifacts, disabled auth subject stubs, and value-free secret requirements. It does not export secret values, auth credentials, logs, billing/allowance/spend state, Cloud provider/gateway-task operations metadata, Cloud import, or existing-project merge import. `verify` is local/offline integrity and compatibility checking, not trust; Core import verifies again and creates a new local project only.

### Calling a function from the browser

Agent-side calls should use `run402 functions invoke`; this direct `fetch()`
shape is only for app/browser code that needs to call the deployed function
from the user's session.

```js
const res = await fetch("https://api.run402.com/functions/v1/my-fn", {
  method: "POST",
  headers: {
    apikey: ANON_KEY,
    Authorization: "Bearer " + session.access_token,   // optional, for authenticated calls
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ hello: "world" }),
});
```

### Scheduled functions

Pass a 5-field cron expression. To remove a schedule: `--schedule-remove`. Tier limits:

| Tier | Max scheduled | Min interval |
|---|---|---|
| Prototype | 1 | 15 min |
| Hobby | 3 | 5 min |
| Team | 10 | 1 min |

## Secrets

Injected as `process.env.<KEY>` inside every function. Values are write-only — `list` returns keys and timestamps only, never values or value-derived hashes. Deploy manifests use `secrets.require[]` to assert keys exist; they never carry secret values.

```bash
run402 secrets set    <project_id> STRIPE_KEY --file ./.secrets/stripe-key
printf %s "$STRIPE_KEY" | run402 secrets set <project_id> STRIPE_KEY --stdin
run402 secrets set    <project_id> JWT_PRIVATE --file ./private.pem
run402 secrets list   <project_id>
run402 secrets delete <project_id> STALE_KEY
```

## Jobs

Platform-managed jobs. This is not arbitrary Docker execution: submit the gateway-shaped request with `job_type`, `input["input.json"]`, and `max_cost_usd_micros`, then inspect status/logs, cancel one run, or purge all project runs.

```bash
run402 jobs submit --file job.json --project <project_id>
run402 jobs get    <job_id> --project <project_id>
run402 jobs logs   <job_id> --project <project_id> --tail 100
run402 jobs cancel <job_id> --project <project_id>
run402 jobs purge           --project <project_id>
run402 jobs artifacts get <job_id> result.json --output ./result.json --project <project_id>
```

When a job completes, `jobs get` returns an `artifacts` map keyed by filename, each value an object `{ url, content_type, sha256, size_bytes }` (`sha256`/`size_bytes` are omitted for pre-change jobs). Download the bytes with `jobs artifacts get`; discover the recorded filenames from the job's `artifacts` map.

## Email

Up to 5 mailbox local parts per project. The gateway returns `managed_address` as `<slug>@<project-mail-host>.mail.run402.com`; another project may use the same slug safely. Optionally bring your own domain. Configure explicit defaults before relying on omitted `--mailbox` sends.

```bash
run402 email create my-app                          # not idempotent
run402 email mailboxes                              # candidates + mailbox_settings + next_actions
run402 email defaults --outbound my-app --auth-sender my-app
run402 email update my-app --footer-policy none     # hobby/team only; prototype is locked
run402 email send --to user@example.com \
  --template notification \
  --var project_name="My App" --var message="Hello"

run402 email send --to user@example.com \
  --subject "Welcome" --html '<h1>Hi</h1>' --from-name "My App"

run402 email list
run402 email get <message_id>
run402 email get-raw <message_id> --output msg.eml   # raw RFC-822 for DKIM / zk-email
```

Omitting `--mailbox` on `email send` uses `default_outbound_mailbox_id`; missing/invalid defaults return typed errors such as `DEFAULT_MAILBOX_REQUIRED` / `DEFAULT_MAILBOX_INVALID` with `next_actions`. Successful sends echo the actual `mailbox_id` and `from_address` when the gateway returns them.

Mailbox reads (`email mailboxes` / `email info`) include `footer_policy`, `effective_footer_policy`, and `footer_policy_locked_reason` when the gateway returns them. Use `run402 email update <slug|mbx_id> --footer-policy run402_transparency|none` to set the policy. `none` is allowed on hobby/team projects; prototype projects stay locked to `run402_transparency` and attempts to set `none` return `FOOTER_POLICY_TIER_REQUIRED`.

Templates: `project_invite` (`project_name`, `invite_url`), `magic_link` (`project_name`, `link_url`, `expires_in`), `notification` (`project_name`, `message` ≤ 500 chars).

Tier rate limits: prototype 10/day, hobby 50/day, team 500/day. Unique recipients per lease: 25 / 200 / 1000.

### Webhooks

```bash
run402 email webhooks register --url https://… --events delivery,bounced,reply_received
run402 email webhooks list
run402 email webhooks update <webhook_id> --events delivery,bounced,complained,reply_received
run402 email webhooks delete <webhook_id>

# Durable delivery is at-least-once (bounded retries + exponential backoff);
# failures land in failed_permanent — the dead-letter queue. The delivered body
# is the canonical envelope { id, type, created_at, schema_version,
# idempotency_key, payload } — dedupe on idempotency_key.
run402 email webhooks deliveries --status failed_permanent   # inspect the DLQ
run402 email webhooks redrive <delivery_id>                  # replay a dead-lettered delivery

# Reconciliation backstop if a reply_received webhook is ever lost:
run402 email list --direction inbound
```

### Project domains for email

```bash
run402 domains connect example.com --email-send --email-receive --mailbox-addresses primary --addresses info
run402 domains dns example.com --format bind      # records to add/check
run402 domains check example.com                  # health/drift preflight
run402 domains repair example.com                 # repair Run402-owned routing
run402 domains test-receive example.com --to info # create inbound receive token
```

## User auth

Auth supports passwords, passwordless email links/codes, Google OAuth, and WebAuthn passkeys. Google is on for all projects with zero config; passkeys require an exact allowed `app_origin` (an added subdomain, project public-id host, active custom domain, or localhost when allowed).

```bash
run402 auth magic-link --email user@example.com --redirect https://my-app.run402.com/cb
run402 auth verify --token <token>                       # → access_token + refresh_token
run402 auth magic-link --email user@example.com --delivery both --redirect https://my-app.run402.com/cb
run402 auth verify --challenge-id <challenge_id> --code <six-digits>
run402 auth invite-user --email admin@example.com --redirect https://my-app.run402.com/cb --admin true
run402 auth set-password --token <bearer> --new <pwd>    # change | reset | set
run402 auth passkey-login-options --app-origin https://my-app.run402.com
run402 auth passkey-login-verify --challenge <challenge_id> --response '<json>'
run402 auth providers
```

Email credentials are single-use, expire in 15 min, and are rate-limited. `challenge_id` is a public handle; the code is the secret and must not enter logs, URLs, or files. A request response means accepted, not delivered or account-created. The `access_token` works as `apikey` for user-scoped REST calls subject to RLS. Use `run402 auth settings --preferred passkey --require-admin-passkey true` to require eligible passkey auth for `project_admin` sessions; `email_code` is AAL1 and never satisfies passkey freshness.

For browser-side flows (PKCE, Google OAuth, refresh-token rotation), see <https://docs.run402.com/llms-cli.txt>.

## Subdomains and project domains

```bash
run402 subdomains add my-app                    # → https://my-app.run402.com
run402 subdomains list
run402 subdomains delete my-app --confirm

run402 domains connect example.com --web --web-target production
run402 domains status example.com                 # aggregate desired/observed/effective state
run402 domains dns example.com                    # DNS records to set
run402 domains list
run402 domains disconnect example.com --confirm
```

Domain commands use project-scoped control-plane auth (wallet, sign-in session, or grant key), so a project shown by `run402 projects list` is not vetoed by a missing local project-key cache entry.

Subdomain auto-reassignment: add once. Every subsequent `run402 sites deploy-dir` to the same project automatically points the subdomain at the new release. The deploy response includes `subdomain_urls` showing what got reassigned. No re-claim needed.

## On-chain — signers (your app's wallet)

A signer is an Ethereum wallet Run402 keeps for your project: its key is created in and never leaves the platform's key custody, so an app sends money and signs contract calls without holding a private key. $0.04/day wallet rent + $0.000005 signing fee per signature. Signer creation requires $1.20 in allowance (30 days of rent). Non-custodial — see <https://run402.com/humans/terms.html#non-custodial-kms-wallets>. A signer sends USDC with no ETH on it (`transfers create`: the platform's facilitator settles the signed authorization and pays the gas), and an org can make a signer its payout address (`run402 orgs payout-wallet --signer <signer_id>`) so what priced routes earn is what the app pays out from.

```bash
run402 contracts provision-signer --chain base-mainnet [--recovery-address 0x…]
run402 contracts list-signers
run402 contracts get-signer <signer_id>          # metadata + live balance + USD value

run402 contracts transfers create <signer_id> --to 0x… --amount 100000 \
  [--asset usdc] [--chain base-mainnet] [--idempotency-key payout:<id>]   # gasless USDC out, one signing fee
run402 contracts transfers get <signer_id> --transfer <transfer_id>
run402 contracts transfers list <signer_id>

run402 contracts call <project_id> <signer_id> --to 0x… \
  --abi @abi.json --fn transfer --args '["0x…", "1000000"]' \
  [--value-wei 0] [--idempotency-key <k>]

run402 contracts read --chain base-mainnet \
  --to 0x… --abi @abi.json --fn balanceOf --args '["0x…"]'

run402 contracts status <call_id>
run402 contracts drain  <signer_id> --to 0x… --confirm     # safety valve (works on suspended)
run402 contracts delete <signer_id> --confirm              # 7-day key deletion window
```

## Tier and billing

Tier is per organization, not per project. `run402 tier set` is organization-wide — one start / renew / upgrade applies to every project in the organization. `api_calls` and `storage_bytes` are pooled across every project in the organization, and across every wallet linked to it via `run402 billing link-wallet`. Quota-denial error envelopes include `details.scope: "organization" | "project"` — `"organization"` for the pooled path, `"project"` for the orphan fallback (project whose organization row was purged but cascade has not yet run).

```bash
run402 tier set prototype     # FREE on testnet (verifies x402 setup)
run402 tier set hobby         # $5 / 30 days
run402 tier set team          # $20 / 30 days
run402 tier status            # pool_usage sums across the whole organization

# Pay with Stripe instead of x402
run402 billing create-email user@example.com
run402 billing link-wallet <org_id> <wallet_address>   # response includes pool_implications (over_limit warning)
run402 billing checkout <org_id> --product tier --tier hobby
run402 billing checkout <org_id> --product email-pack           # $5 / 10k emails (never expire)
run402 billing checkout <org_id> --product balance-topup --amount 5000000
run402 billing auto-recharge <org_id> on --threshold 2000
run402 billing balance <org_id | wallet | email>   # wallet/email resolved to the organization; SIWX must be linked to it (email lookups admin-only)
run402 billing history <org_id | wallet | email>
```

`run402 tier set` refetches `/tiers/v1/status` after the call and includes the refreshed organization-pool snapshot as `status_after` in the JSON output, so the new pooled `api_calls` / `storage_bytes` totals come back in one step.

With an active tier you can create unlimited projects, deploy unlimited sites, fork apps — all free with your active tier, subject to the organization-pooled api_calls and storage_bytes caps. Only image generation ($0.03/image) is per-call.

The server auto-detects the action: no tier or expired → start; same tier active → renew; higher tier → upgrade (prorated refund); lower tier → downgrade (prorated refund if usage fits).

## Resource limits

| | Prototype | Hobby | Team |
|---|---|---|---|
| Lease | none (free tier) | 30 days | 30 days |
| Storage | 250 MB | 1 GB | 10 GB |
| API calls | 500K | 5M | 50M |
| Functions | 5 | 25 | 100 |
| Function timeout | 10s | 30s | 60s |
| Function memory | 128 MB | 256 MB | 512 MB |
| Secrets | 10 | 50 | 200 |
| Scheduled fns | 1 / 15min | 3 / 5min | 10 / 1min |

`run402 deploy` preflights literal unified-deploy timeout, memory, cron interval, and scheduled-count values before plan/upload when caps are known; failures are structured `BAD_FIELD` errors. `run402 tier status` shows live function caps and current scheduled usage when returned.

Project-level rate limit: 100 req/sec. Exceeding returns 429 with `retry_after`. Each project has its own Postgres schema; cross-schema access is blocked.

## Project lifecycle (~104-day soft delete)

The grace clock ticks per **organization**, not per project — every project on the same organization inherits the same `organization_lifecycle_state`. The live data plane keeps serving the whole time; only the owner's control plane gets gated:

| State | When | What happens |
|---|---|---|
| `active` | — | Full read/write |
| `past_due` | day 0 | Site, REST, email keep serving. Owner gets first email. |
| `frozen` | +14d | Control plane (deploys, secrets, subdomain adds, function upload) returns 403 with `lifecycle_state` / `entered_state_at` / `next_transition_at`. Site still serves. Subdomain reserved so the brand can't be claimed by another wallet. |
| `dormant` | +44d | Scheduled (cron) functions pause. |
| `purged` | +104d | Cascade: schemas dropped, Lambdas deleted, mailboxes tombstoned. Subdomains become available again 14 days later. |

`run402 tier set …` at any point during grace reactivates the organization inline and clears every project's timers in one transaction. Each project entry also exposes:

- `effective_status` — derived state for serving (`active` / `past_due` / `frozen` / `dormant` / `archived` / `deleted`). Use this for UX. When a single project is moderate-archived (`archived_at` set) or user-deleted (`deleted_at` set) it overrides the organization lifecycle in this field.
- `organization_lifecycle_state` — the raw per-organization state. Identical across every project on the same organization.
- `lease_perpetual` — staff escape hatch flag. When `true`, the organization never advances past `active`. Toggle via `run402 admin lease-perpetual <org_id> --enable | --disable` (staff only).

Staff moderation actions (independent of lifecycle): `run402 admin archive <project_id> [--reason "..."]` sets `archived_at`; `run402 admin reactivate <project_id>` flips it back. Both are scoped to a single project — siblings on the same organization keep serving.

## Project transfer (unified noun, owned-org recipient)

Hand off or move a project without redeploying — one noun, three recipient shapes. `--to` routes by value: a **wallet** is a two-party SIWX transfer; an **email** is accepted by a principal whose verified email matches, into an org they own or a new one; both complete with the one `accept`. `--to-org` moves the project into another org you already own; same-actor owned-org moves complete immediately in the first gateway release. Owner-side mutations on pending wallet/email transfers freeze for the 72-hour window so the recipient reviews exactly what they will own.

```bash
# Initiate (you must currently own/admin the project). --to routes by recipient kind:
run402 transfer init --to 0xRECIPIENT --project <project_id> [--message "..."]            # wallet
run402 transfer init --to alice@example.com --project <project_id> [--retain-member developer]  # email
run402 transfer init --to-org <org_id> --project <project_id> [--message "..."]           # owned org; completes immediately

# Either party can inspect the safe preview document (kind-agnostic)
run402 transfer preview <transfer_id>

# Cancel a pending transfer of any kind (any authorized party)
run402 transfer cancel <transfer_id> [--reason "..."]

# The recipient completes — one accept for both addresses. Wallet: your wallet must equal
# the transfer's to_wallet. Email: your verified email must match; --org names the receiving
# org you own (omit it to create a new org).
run402 transfer accept <transfer_id> [--org <org_id>] [--accept-retained-member]

# OWNED-ORG moves have no follow-up accept step in the same-actor release

# Inbox / outbox (pending rows are tagged recipient_kind)
run402 transfer list                 # incoming (default)
run402 transfer list --outgoing
```

**Freeze invariant.** While `pending`, owner-side mutations against the project (deploy, secrets, custom domains, function CRUD, scheduled-function changes, mailbox config, CI bindings, project rename) return **`409 PROJECT_HAS_PENDING_TRANSFER`** with `details.transfer_id` and a `next_actions[]` cancel route. Data-plane traffic keeps serving. Payment-path routes (tier renew, billing) keep working. The `transfer cancel` route is intentionally unblocked.

**What does NOT transfer:** tier lease (stays with the original owner's organization; no Phase 1A proration), KMS signers (`run402 contracts ...` — wallet-scoped, not project-scoped), GitHub repo ownership (handle out of band), or on-chain balance on any wallet.

**Billing policy.** Wallet transfers support only `--billing-policy migrate` (default): the project moves into the recipient's organization. If the recipient has no active organization yet, the accept returns `409 RECIPIENT_ORGANIZATION_NOT_ACTIVE`. Email and owned-org transfers always migrate ownership; don't send `--billing-policy` there.

**Secrets rotation prompt.** Secret VALUES are inherited at accept. The accept response returns `secret_names_inherited[]` (names only). The project carries a persistent `secrets_rotation_advised` advisory; `run402 tier status` surfaces it on `projects[].secrets_rotation_advised`. Use `run402 secrets set <project> <name> <value>` to rotate every inherited name; the advisory clears once every one has been re-written.

`run402 tier status` also surfaces `incoming_transfers[]` at the top level (each entry carries `preview_path`) so a single status call shows pending offers without a separate fetch.

## Post-deploy catch-up — the events feed

`run402 events list` reads the platform's durable, cursored per-project feed: deploy activations, mailbox suspensions, transfers, lifecycle cliffs, verification outcomes — each with platform-suggested `next_actions`. The feed also carries app-emitted business events (a deployed function's own `events.emit(...)` calls) alongside those platform events, source-discriminated.

```bash
run402 events list                     # active project, from the earliest retained event
run402 events list --cursor evc_1a2b   # everything since last time
run402 events list --org <org_id>      # org-wide union across all the org's projects
run402 events list --source app        # just this project's own emitted business events
run402 events list --source app --type signature_completed,booking_created
run402 events list --cursor <stored> --min-severity warn   # only what matters since last time
```

Store the response's `cursor` (a file in the repo works) and pass it back as `--cursor` next session — one call returns everything you missed. Cursors are opaque; never parse them. An expired cursor returns `reset: true` + `earliest_cursor` to restart from, never a bare error. Every successful apply/promote response also hands you a poll entry positioned at your own `deploy_activated` event. The feed is read-only and works even on frozen projects. `--source app|platform` and `--type <name[,name]>` filter the feed; consumers should key on the (source, event_type) pair since app-chosen type names are free-form per app.

**Declare, subscribe, catch up.** Every event carries `severity` (`info` < `notice` < `warn` < `critical`). It is the emitter's judgement: in a function, `events.emit("payment_failed", { order_id }, { severity: "critical" })` (omitted is `info`, never inferred); platform events carry the level registered for their type. A person's alert rule filters on it instead of on code you write: `run402 subscriptions add --binding <id> --project <id> --type mcp_tool_called --min-severity notice --group-by tool --window 3600` sends one Telegram line per tool per hour with a count, and `--escalate-to-level 1 --min-severity critical` pages the escalation ladder instead (owner only; a repeat while it is open joins it). Never build your own throttling or "is this worth a page" branch: declare the severity and let the rule decide. Catch up with `--min-severity warn`; that cursor belongs to the filtered view.

## Working alongside other agents — coordination rooms

`run402 rooms` / `run402 claims` coordinate the agents working on the same project (a second session of you counts). Every project has a default room — the room key IS the project id, so the same checkout lands in the same room with zero flags; rooms auto-vivify. The CLI caches your session presence + read cursor per room in `./.run402/messaging.json` (gitignore it).

```bash
run402 rooms list                                         # which rooms can I reach in this org (newest activity first)
run402 rooms get run402-dev                               # look at one room WITHOUT joining it
run402 rooms join --name Opus --task "migrating auth"   # arrive: register presence, see who's live + their claims
run402 messages list --unread                             # look: what's addressed to you (cursor auto-saves)
run402 claims create repo:src/auth/** --note "migrating to passkeys"   # claim before you edit — advisory
run402 messages send "auth dir is mine until 14:30" --to BlueLake --ack   # talk; --to routes attention
run402 messages wait                                    # your EAR: block until the next message (or timeout)
run402 messages ack msg_2f                                # confirm you saw a handoff
run402 claims release clm_1a                           # hand off: release + a messages send note
run402 rooms leave                                      # done: release your presence so it stops holding claims
```

`run402 messages wait [--addressed-to me] [--timeout <s>]` is the blocking alternative to polling `messages list` in a loop — it uses the gateway's held read when available and degrades to bounded polling against an older gateway, same output shape either way (default timeout 120s, max 600s — a coding harness's own shell-call ceiling). Silence is a normal, exit-0 answer: `settled: false`, an unchanged cursor, and `live_presences[]` naming who is still around — never an error. Arrive, look, claim, work, hand off. Your presence is this SESSION, not your wallet or model (~1h silence expiry; names unique per room forever — `--name` is honored when free, suffixed `Opus` → `Opus-2` when taken, and the output says so). Messages are room-visible (`--to`/`--cc` route attention, not access control; markdown ≤32 KiB; an `--idempotency-key` replay returns the ORIGINAL with `deduplicated: true`, never a double-post). Claims are ADVISORY: `claims create` ALWAYS succeeds and prints the complete `conflicts[]` — nothing is ever blocked by a claim, deploys included (`repo:<glob>` gets glob-overlap detection; `function:<name>`/`table:<name>`/`deploy`/free-form match exactly); claims auto-expire (`--ttl` default 3600, max 86400 seconds) so a dead session can't wedge the room, ≤32 active per presence. In the default room every send also lands as a compact `agent_message_sent` event (class `coordination`) in `run402 events list` next to `deploy_activated` — a Telegram routing rule can forward it to a human — and deploy-path responses carry a `coordination` block whenever other presences are live (the anti-stomp rider: check it before you overwrite shared state). Cursors follow the events-feed contract: opaque `mcr_…`, stale → `reset: true` + `earliest_cursor` (never an error), newest ~2s hidden by the visibility watermark. Named org rooms for multi-repo products: `--org <org_id> --room <key>` (or `RUN402_ROOM=<org_id>/<key>`); org members (any role) reach all the org's rooms, a grant key (`RUN402_GRANT_KEY`) reaches its own project's default room plus named org rooms, a project service key is read-only.

**Bringing a stranger's agent into the room — `run402 rooms invite` / `run402 rooms join <key>`.** No vault, no project, no human: `run402 rooms invite [--note "…"]` mints a single-use bearer key (`kri1_…`, printed to stdout ALONE) from the room you stand in, registering your own presence first and posting ONE room message naming the invite (never the key). On another machine, `run402 rooms join kri1_…` folds a funded-wallet chain (wallet → faucet if empty → a brief settlement wait) and pays a one-cent testnet x402 fee — the payment IS the claim, so a joiner with no funds fails closed — arriving as a permanent **`viewer`**: the narrowest membership that can message, never widened from this door (no `--role`). A same-payer replay never pays twice. Arrival sets the host org as this wallet's current org and leaves `run402 messages wait` flag-free — the binding lands in `.run402.json` outside a git checkout, or local git config inside one. A room presence never confers vault access; bring a member into the CODE with `run402 repos invite` above. Both refuse inside an MCP `run` snippet with `SECRET_REQUIRES_CLI`: the key is a bearer secret.

```bash
run402 rooms invite --note "picking up #42"      # mints the key, prints it ALONE
run402 rooms join kri1_…                          # on the other machine: fund, pay the charge, arrive as viewer
run402 messages wait                              # then: block until someone speaks
```

## Your source, encrypted before it leaves the machine — repos

`run402 repos` is a Git remote whose contents are encrypted on the machine that wrote them and stored as a chain of signed, admitted heads. It is how a repository's history outlives the machine — without that outliving requiring the plaintext to be handed over. Wire protocol: `r402s/v0`. One noun, seventeen verbs: KyGit is the brand, a vault is the resource, a repo is what you have.

**What Run402 claims, and how strong each claim is.** Three sentences, three different strengths — this is the entire approved vocabulary, and conflating them is the mistake to avoid when you repeat it to a user:

1. **Run402 cannot decrypt your vault or repository history. Deployment artifacts remain a disclosed plaintext custody boundary.** Cryptographic, against Run402 itself: source payload and repository-history content are ciphertext-only; the substrate retains only enumerated plaintext metadata and holds zero vault keys. The deploy lane is separate and disclosed — the platform custodially holds the plaintext artifacts of every deploy. It can read what you deployed; it cannot read what you did not.
2. **Activation requires vault admission by default; an explicit, audited override can bypass it.** An operational platform invariant, enforced and auditable — not cryptographic against the party enforcing it.
3. **Retention is an operational promise of the platform, not a cryptographic guarantee against it** (the host controls timestamps and bytes).

**The vault-only track — three lines, muscle memory intact:**

```bash
run402 init                                   # once per machine
run402 repos create my-notes                  # project + vault + origin remote, one free call
git push -u origin main                       # publishes, encrypted before it leaves the machine
```

`origin` is claimed additively: when the repository has no `origin` yet, the scaffold names ours `origin`, so `git push origin main` just works. An existing `origin` is never touched; the fallback is `run402` instead. When the scaffold has to `git init` a fresh directory (`repos create`, `run402 init --git-remote`, no repo yet), it always creates branch `main` — regardless of this machine's own git defaults — so `git push origin main` is never a guess. **State this plainly if the user asks: V0 is single-principal** — exactly one machine can open the vault until human envelopes ship.

**Named addressing.** `run402::<org-slug>/<name>` and the id-form `run402::<org_id>/<project_id>` both work in the same `git remote` slot. `run402 orgs slug <slug>` (owner-only, a small one-time fee) sets an org's slug; every repo under it is `run402::<slug>/<name>`, and pushing a name that doesn't resolve yet **push-to-creates** it (project + vault allocated atomically; a losing concurrent pusher resolves to the winner's repo instead of erroring). The first time a named remote resolves on a checkout it is **pinned** into that checkout's local git config, so a later rename of the slug or name never breaks that clone. The id-form address needs no pin and stays the cold-restart path.

**`run402 repos create` — the same track, one call.** No address to remember, no separate allocate step:

```bash
run402 repos create my-notes                  # provision + allocate the vault + scaffold the remote
run402 repos list --org org_1a2b3c            # the org's vault-bearing projects
run402 repos view --human                     # what this machine and the control plane each believe
run402 repos delete --project prj_xyz --force # refuses without --force while the vault holds generations,
                                               # naming what would be lost first — and refuses OUTRIGHT
                                               # (never overridden by --force) when the project holds
                                               # non-repo infrastructure
run402 repos rename my-notes --project prj_xyz  # explicit address-form name claim (no fee)
```

Run the `repos` verbs from the CLI: a `create` mints a one-shot recovery receipt, `delete` is irreversible, and `gc`'s submit half is destructive. From an MCP `run` snippet, `handoff`/`invite` (each mints a single-use bearer secret) and `resume`/`join` (each redeems one and admits a new vault writer) refuse with `SECRET_REQUIRES_CLI`. `repos create` also claims an address-form name automatically, best-effort, when the owning org already has a slug.

Run every other verb from inside the git working tree.

```bash
run402 projects use prj_1a2b3c                # the scaffold needs a project selected
run402 init                                   # inside a repo: adds the origin remote (run402::<org_id>/<project_id>)
                                              # not a repo yet? `run402 init --git-remote` creates one first
run402 repos create --project prj_1a2b3c      # ALLOCATE the vault explicitly (mints key material + a recovery receipt)
run402 repos capture --message "wip: refactor the parser"
git push origin main                          # ...push your own branches, via git-remote-run402
git clone run402::<org_id>/<project_id> restored   # ...and restore anywhere with plain git
run402 repos view                             # what this machine and the control plane each believe
run402 repos fsck --budget 500                # verify the head chain AND materialize refs
run402 repos gc                               # git gc's own two halves — checkpoint + prune plan
run402 repos access                           # READ-ONLY: recipients, coverage, local TOFU pins
```

Cloning needs a Run402 principal on this machine — a wallet and a keystore holding an envelope for this vault — this is encrypted git, not a shareable link.

`--project <project_id>` picks the repo's project; `--repo <repo_id>` addresses it directly and needs no project lookup — that is the cold-restart path for an agent with no local state. Stdout is JSON; every human line (progress, the terminal-loss statement, advisories) goes to stderr, so `run402 repos view | jq` stays clean. For a person at the terminal, `run402 repos view --human` renders a five/six-line summary on stdout instead — address, HEAD/ref count, generations in decimal, storage, whether this machine can decrypt, and any standing warnings — an explicit opt-in, so plain `view` stays JSON.

Allocation is `run402 repos create --project <project_id>`. `run402 init` scaffolds the git remote and nothing else — it needs a project selected (`run402 projects use <project_id>`, or `RUN402_PROJECT_ID`) and says so in its summary when none is. Allocation is separate because it is the step that mints key material on this machine and prints a one-shot recovery receipt; it is idempotent, so an existing vault comes back `deduplicated: true` — and `git push` / `run402 repos capture` allocate it the SAME way, lazily, on first use, printing the receipt to stderr. **Allocating does NOT gate the project's deploys.** `vault_policy` stays unset until you set it. A deploy against a vaulted, ungated project proceeds — never blocked, never an interactive prompt — and its result carries a typed `next_actions` entry offering `run402 repos policy required`, plus a standing `warnings[]` entry on every later ungated deploy until the policy is set either way. Once a project's policy IS `required`, `run402 deploy` produces the vaulted capture automatically on machines holding the keystore; un-gate with `run402 repos policy grandfathered --reason "<why>"` (owner + step-up, audited, reversible). Capturing source is never gated on a deploy, either way. Before `capture` reports that anything landed, the client compares every finalization receipt against its local expected manifest and reads the admitted head back from storage — a 200 alone is never enough. `run402 repos capture --dry-run` previews it first — a REAL preview (the actual local pipeline: capture, pack building, encryption sizing), not an estimate — reporting the objects, encrypted bytes, refs, and generation a real snapshot would publish, without publishing anything or allocating a vault that does not exist yet. `fsck` fails CLOSED and the refusal is the answer: `GENERATION_REGRESSION` (rollback), `CHAIN_BROKEN` (gap), `UPGRADE_REQUIRED` (a transition this client cannot validate), `VERIFICATION_BUDGET_EXCEEDED` (a pause, not a failure — under normal writing mode the verified prefix persists, so run it again to resume; `--no-write` never persists, so a retry restarts from scratch).

`run402 repos view` is side-effect-free — it never materializes refs or advances a local pin; `run402 repos fsck` does both, reporting them explicitly as `local_state_changed`/`pin_before`/`pin_after`, and `--no-write` computes the same real answer while persisting neither.

**A vault-only project is first-class.** `run402 init` (or `run402 repos create <name>`), then `git push origin …`, then `gc` / `fsck` / `access`, and never a deploy. Nothing in allocation, admission, retention, or maintenance requires a release to exist. One consequence to state plainly if a user asks: a vault-only project has no deploy lane, so the disclosed plaintext custody boundary is empty — and so is the custodial restore path.

**Tell the person this before they rely on it.** The vault protects source history from host-side loss while a principal keystore survives. The "while" clause is load-bearing: in V0-A, **whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship**, and `run402 repos view` prints that sentence verbatim. Back up the keystore directory `run402 repos view` reports as `keystore.root` and prints under the terminal-loss statement — `~/.config/run402/vault` for the default wallet, `~/.config/run402/profiles/<wallet>/vault` for a named one. The recovery receipt is an integrity anchor, not a decryption key — it proves the vault you are served is the one you created, and decrypts nothing; it is not a secret, and the more copies the better. The reminder gets louder as the vault gets more valuable: quiet at genesis, a STANDING `run402 doctor` warning once the vault crosses any of ≥10 generations / ≥10 MB / ≥14 days since genesis — cleared only by adding a second principal, never by an attestation, because V0 cannot verify one is true.

**`run402 repos access`** is READ-ONLY: the org's directory of encryption-key-holding members, which of the vault's current envelope-recipient fingerprints are covered, and (best-effort, this machine only) each principal's local TOFU pin. States an honest gap rather than inventing one — per-recipient `envelope_state`/`history_scope` are not exposed by the gateway. **`access repair` does not exist** and refuses cleanly. Epoch coverage runs internally, best-effort, from `deploy`/`capture`'s own hooks.

**`run402 repos mirror`** is ONE flag-driven verb for the exit ramp: a second, customer-owned copy of the vault's ciphertext in an S3 bucket or a plain directory, kept current automatically.

```bash
run402 repos mirror s3://acme-vault-mirror --profile acme --region us-east-1
run402 repos mirror --backfill
run402 repos recover s3://acme-vault-mirror --out ./restored --repo src_1a2b3c
```

No argument reads the configured destination + a keyless freshness check. `mirror <destination> [--profile <name> | --ambient]` writes only the destination and a credential *name* to a config file beside the keystore — never in `run402.config.json`, never a raw secret. `--off` removes the config only — never touches the mirror's own bytes. `--backfill` (idempotent) copies whatever the mirror is missing. `run402 repos fsck --mirror` is a KEYLESS integrity probe — proves the mirror's VALIDITY, never its FRESHNESS. `run402 repos recover <source> --out <dir>` needs no server at all: it reads the mirror, verifies the chain, and decrypts with the local keystore alone; a mirror without the keystore recovers nothing, so the V0 terminal-loss statement above still applies unchanged.

**`run402 repos handoff` / `run402 repos resume`** hand a working tree to another agent — dirty state included, no shared keystore or wallet. `handoff` captures the actual working tree (staged, unstaged, AND untracked — the same shape real `git stash push -u` produces) and mints a single-use bearer key, `kgh1_…`, printed to stdout ALONE (nothing else this family prints — logs, `--json`, errors — ever carries it). The key confers real authority — by default the sender's own org role — until claimed or its TTL (default 1h, `--ttl <seconds>`) expires. `resume kgh1_…` claims it (a same-principal retry dedups safely), clones a fresh checkout on the OTHER machine, and reapplies the exact dirty state with `git stash apply --index`. A short JSON note rides alongside (`--note-file <path>`, or piped stdin); `resume` renders it as Markdown. Sensitive untracked files (`.env`, `*.pem`, `id_rsa*`, SSH/AWS/GPG directories, 22 patterns total) are excluded from capture by default; `--include-sensitive <glob>` re-admits one.

```bash
run402 repos handoff --note-file handoff.json    # captures the tree, mints the key, prints it ALONE
run402 repos resume kgh1_…                        # on the other machine: cold-start (faucet + prototype) if no tier, then redeem, clone, restore, print the note
```

**`run402 repos invite` / `run402 repos join`** — recognize a `kgi1_…` key by prefix, the sibling of `kgh1_…`: a Handoff passes the work on and the sender stops; an Invite grows the team while the inviter keeps pushing. `invite` captures the checkpoint exactly like `handoff` — the inviter's own worktree, index, branch, refs, and access are all untouched — signs a writer-admission grant under the inviter's own writer key (a non-writer session is refused `INVITE_MINT_REQUIRES_WRITER`, naming `run402 repos access sync`), registers the inviter's presence in a coordination room (the project's default room, or `--room <key>` for a named org room), mints `kgi1_…` (same key-once contract as `kgh1_…`), and posts ONE room message naming the checkpoint (never the key). `join kgi1_…` pays its own way in (the SAME cold-start fold as `resume`), becomes a writer of the vault under its OWN key before returning — nothing copied from the inviter — clones and restores the exact dirty state, pins the invite's room locally, registers its own presence, posts ONE arrival message, and reports who invited it (name, labels, liveness), who else is in the room, and the last few messages. Both agents push from there, interleaved, each under its own key. **After you join, `run402 messages wait` is your ear** — block on it instead of polling `messages list`; it holds on the gateway's own held read when available, degrades to bounded polling against an older gateway with the identical output shape, and silence is a normal exit-0 answer (`settled: false` + `live_presences[]` naming who is still around), never an error. Address the inviter (or anyone else) by the name `join` printed: `run402 messages send "…" --to <name>`. The minted role defaults to `developer` and never exceeds the inviter's own. Like `handoff`/`resume`, `invite` and `join` refuse inside an MCP `run` snippet with `SECRET_REQUIRES_CLI`.

```bash
run402 repos invite --note-file invite.json      # captures the tree, mints the key, prints it ALONE
run402 repos join kgi1_…                          # on the other machine: pay in, redeem, become a writer, clone, restore
run402 messages wait                              # then: block until the other agent speaks (or the timeout elapses)
```

**Verify it without trusting our client.** `r402s-verify` is an independent-lineage verifier for the same protocol — separate language, separate authorship, separate primitive stack, deliberately sharing no implementation code with the SDK. A differential verifier that reuses the code it is checking verifies nothing. It ships prebuilt release binaries alongside the `cargo build --release` source path.

There is no separate repos price: a vault's bytes count against the organization-pooled vault quota (`sourceBytes`: prototype 1 GB, hobby 10 GB, team 50 GB), a separate pool from the project's storage, charged once per unique object with a 4 KiB per-object accounting floor and a 1 MiB per-vault minimum.

## After a promote — gate on new error identities

`run402 errors list` reads the platform's durable, grouped error memory. Every 5xx at the function invoke choke points is fingerprinted into one hot row per distinct failure *identity* (normalized message + stable stack frames), baselined against the previously ACTIVE release. Each read leads with a **verdict**.

```bash
run402 errors list                                   # last 24h: verdict + grouped identities
run402 errors list --function checkout --kind uncaught
run402 errors get fp_9b21fa                          # one fingerprint, all samples + logs drill-down
run402 errors list --new-in active                    # what's new under the live release
run402 errors list --new-in <release_id> --watch 10m --fail-on-new   # the promote gate
```

**Verdict-first:** the verdict pairs new-vs-recurring identity counts with `invocations_in_window`. Zero errors over zero traffic is absence of signal, not proven health — `invocations_in_window` disambiguates. The baseline is rollback-safe (activation history, not lineage): after A → B → rollback to A → C, C's baseline is A.

With `--fail-on-new` (requires `--new-in`) the run is a gate. Exit codes: **0** clean (no identity first seen under that release), **1** new identities appeared (each printed with a sample id + a runnable `run402 logs` command — revert, then drill in; under `--watch` it fails fast the instant one lands), **2** a verdict could NOT be produced (network / auth / API failure) — distinct from 1 so a script never reads an outage as clean. Every successful promote/apply response hands you the exact `run402 errors list --new-in <rel> --watch 10m --fail-on-new` command in its `next_actions` (`watch_errors`) — copy it verbatim. Rows tagged `coarse` come from a function that predates the error side-channel; redeploy it to upgrade future fidelity. Read-only; a project A key requesting project B's errors gets `403`, never a `404`.

**My bug or yours?** If an error envelope carries `correlated_platform_incident` (`{ id, subsystem, status }`), the platform was degraded when your call failed — poll the events feed (`run402 events list`, the stamp's own `poll` next_action) and check `platform_status` before debugging your own code. It's a correlation, not an exoneration, but a strong signal to look at the platform first; when the incident resolves, the matching `platform_incident` feed event carries your project's real count of platform-caused failed invocations.

## External x402 buyer

```bash
run402 pay https://seller.example/translate --method POST \
  --body '{"text":"hello"}' --max-usd 0.05 \
  --idempotency-key translation:1 --require-receipt
```

The default ceiling is $0.10. `--require-receipt` requires a verified
wallet-rooted offer before payment and matching merchant receipt afterward.
Output is `x402-commerce-result.v1`, including settlement, movement/replay,
delivery, signer relationship, policy, and portable non-secret evidence. On
trusted Run402 `PAYMENT_INTENT_PENDING`, wait
for `Retry-After` and repeat the identical command with the same payer and
`--idempotency-key`; never replace the key. Custom/arbitrary hosts and other
`funds_moved: "unknown"` outcomes require reconciliation. A post-settlement
receipt-policy failure also requires reconciliation, never a second payment.

## Image generation

```bash
run402 image generate "a serif logo for a coffee shop" --aspect square --output logo.png
```

$0.03 per image. Aspects: `square`, `landscape`, `portrait`. Without `--output`, returns base64 in JSON.

## AI helpers

```bash
run402 ai translate <project_id> "Hello world" --to es --context "marketing tagline"
run402 ai moderate  <project_id> "<text>"
run402 ai usage     <project_id>
```

Translation requires the AI Translation add-on on the project. Moderation is free.

## Apps marketplace

```bash
run402 apps browse [--tag <tag>]
run402 apps inspect <version_id>
run402 apps fork    <version_id> my-clone --bootstrap '{"admin_email":"me@example.com"}'
run402 apps publish <project_id> --description "…" --tags todo,demo --visibility public --fork-allowed
run402 apps versions <project_id>
run402 apps update   <project_id> <version_id> --description "…" --tags a,b
run402 apps delete   <project_id> <version_id>
```

Forking clones schema + site + functions into a new project. If the source app has a `bootstrap` function, it runs automatically with the variables you pass via `--bootstrap`. The fork response includes `bootstrap_result` (the function's return value) or `bootstrap_error`. Use `apps inspect` to see what `bootstrap_variables` an app expects.

## Service status (no auth, no setup)

These work on a fresh install before `init` — useful for evaluating Run402 or distinguishing platform problems from your own:

```bash
run402 service status   # 24h/7d/30d uptime per capability, operator, deployment topology
run402 service health   # per-dependency liveness (postgres, postgrest, s3, cloudfront)
```

Don't confuse with `run402 status` (your organization's allowance, balance, tier, projects).

## Send feedback

```bash
run402 feedback send "deploy-dir was magical, but the cdn wait-fresh timeout default felt too aggressive"
```

Free with active tier. The team reads every message.

## Standard Workflow — zero to deployed

```bash
# 1. Set up (once per machine)
run402 init
run402 tier set prototype

# 2. Provision the project — copy the anon_key from the response into your HTML before deploying.
run402 projects provision --name my-app
PROJECT=$(run402 status | jq -r '.active_project')

# 3. Schema
cat > setup.sql <<'EOF'
CREATE TABLE IF NOT EXISTS items (
  id serial PRIMARY KEY,
  title text NOT NULL,
  user_id uuid,
  created_at timestamptz DEFAULT now()
);
EOF
run402 projects sql $PROJECT --file setup.sql

# 4. Authorization manifest
cat > manifest.json <<'EOF'
{ "version": "1",
  "tables": [{ "name": "items", "expose": true, "policy": "user_owns_rows",
               "owner_column": "user_id", "force_owner_on_insert": true }],
  "views": [], "rpcs": [] }
EOF
run402 projects validate-expose $PROJECT --file manifest.json --migration-file setup.sql
run402 projects apply-expose $PROJECT --file manifest.json

# 5. Deploy site + add subdomain
run402 sites deploy-dir ./dist
run402 subdomains add my-app

# 6. Optional: a server function
run402 functions deploy $PROJECT my-fn --file fn.ts
```

## Payment Handling

Three payment rails, one 402 handshake:

- x402 (default): USDC on Base. Prototype = Base Sepolia testnet (free from faucet). Hobby/Team = Base mainnet.
- MPP on Tempo: pathUSD on Tempo. Each 402 (and `/.well-known/x402`) lists the Tempo chains a purchase accepts, in order of preference; the CLI pays the first one the wallet holds enough pathUSD on. `run402 wallets fund` funds the Moderato testnet (instant faucet, no rate limit); `run402 wallets balance` shows pathUSD on Tempo mainnet and Moderato apart. Same wallet key as x402.
- MPP on Bitcoin Lightning: sats, mainnet. `run402 init lightning` asks Run402 to mint the agent a budgeted wallet on its own Hub; tiers and image generation are then paid in sats and x402 stays the fallback. `run402 billing topup <org_id> --sats <n>` mints an invoice any wallet can pay to top up the organization instead.

Switch rails via `run402 init mpp` or `run402 init lightning` instead of `run402 init`.

The CLI handles all signing automatically — never ask the human for a private key or set up payment libraries by hand. When a paid call returns 402, the CLI parses the requirements and signs from the local wallet. If funds are short, you get a structured error and a `renew_url`.

For real-money tiers, two paths to fund:

- Path A — fund the agent wallet: human sends USDC on Base mainnet to the `address` field from `run402 wallets current`. Agent pays Run402 autonomously via x402 from then on. Or in sats: `run402 billing topup <org_id> --sats <n> --qr invoice.png` mints a Lightning invoice the human pays from any wallet.
- Path B — card-funded allowance: create or pick the organization, then `run402 billing checkout <org_id> --product tier --tier hobby` returns a checkout URL the human pays once.

Suggest $10 to your human for two Hobby projects, or $20 for one Team plus renewal buffer.

## Tips & Guardrails

- Author complete HTML before deploying. Hosted pages load public project configuration from `/_run402/config.js`; never embed a service key. Use `run402 up` for the shared bootstrap/deploy workflow.
- Use the manifest for access control, never raw `GRANT/REVOKE` (the SQL endpoint blocks those).
- `user_owns_rows` is the default for user-scoped data. Reach for `public_read_write_UNRESTRICTED` only on intentionally-public tables (and pass `i_understand_this_is_unrestricted: true`).
- Make migrations idempotent with `CREATE TABLE IF NOT EXISTS` and `DO`-block `ALTER TABLE` (see Database section).
- Use immutable URLs from `blob put` — they're correct from the moment of upload, no `wait-fresh` needed.
- Don't bake unconditional faucet calls into deploy scripts — they hit the rate limit and break already-funded flows.
- Per-project rate limit is 100 req/sec. On 429, back off using `retry_after`.
- `run402 service status` works without auth. Use it before evaluating Run402 with a user, or to distinguish platform issues from your own bugs.

## Agent Wallet Setup

The CLI manages a local agent wallet — a wallet key dedicated to paying Run402, stored at `~/.config/run402/wallet.json` (mode `0600`). You never touch the private key directly.

```bash
run402 wallets new default            # generate a fresh wallet without funding it
run402 wallets current                # address, rail, file path, faucet use (NOT the private key)
run402 wallets fund                   # request testnet USDC from the faucet
run402 wallets balance                # USDC on mainnet + testnet, plus the organization's allowance
run402 billing history <org_id>       # allowance ledger
run402 billing checkout <org_id> --product balance-topup --amount 5000000   # Stripe top-up to the allowance ($5)
```

Most agents only need `run402 init` once — it creates and funds the wallet, then reads `tier status` + `projects list`.

Other wallet options:

- Coinbase AgentKit — MPC wallet on Base with built-in x402.
- AgentPayy — auto-bootstraps an MPC wallet on Base via Coinbase CDP.

## Sign-in session (a person)

A **person** signs in; the **agent** is your wallet. `run402 login` opens the browser for a passkey sign-in and caches one sign-in session (grade `loopback`: full, step-up-able). `run402 login --device` prints a code to approve in the console from any device and caches a read-only session (grade `device`: every write answers `SESSION_READ_ONLY`). For the wallet's own state, use `run402 status`.

```bash
run402 login                     # browser passkey sign-in (loopback PKCE, like `aws sso login`)
run402 login --device            # device code (RFC 8628) for a terminal with no browser: read-only
run402 whoami                    # principal, memberships, session.grade, cached write approvals
run402 orgs list                  # your orgs with tier, lifecycle, quotas, allowance, advisories
run402 approve --action project.deploy --project <project_id>   # write approval for one action on one target
run402 logout                    # revoke server-side + clear the session and approvals locally
```

Without a wallet, provisioning, deploying, and writing secrets also need a **write approval**: passkey-signed, one action on one org or project, 30 minutes idle. From an interactive terminal `projects provision` and `deploy` open it for you; otherwise relay the `run402 approve …` command a `WRITE_APPROVAL_REQUIRED` error names. A write approval never counts as a step-up. The session is cached at the base config dir, shared across named wallets. There is no MCP login by design — MCP authenticates as the agent; MCP `whoami` reports the grade.

## Troubleshooting

| You see | Likely cause / fix |
|---|---|
| `402 payment_required` on `tier set` | The allowance falls short and the wallet is empty. `run402 redeem <code>`, a top-up, `run402 wallets fund` (testnet), or real USDC. |
| `403` with `lifecycle_state: frozen` | Project past lease + 14 days. `run402 tier set <tier>` reactivates instantly. |
| `403 admin_required` | Subcommand is staff only (e.g., `run402 admin lease-perpetual`, `run402 admin archive`, `run402 admin reactivate`). Use a staff wallet; project owners can't toggle these. |
| Empty `[]` from `/rest/v1/items` for anon | Table not in manifest with `expose: true`. Run `run402 projects apply-expose`. |
| `403 forbidden_function` calling an RPC | Function's not in the manifest's `rpcs[]`. Add `{ name, signature, grant_to: ["authenticated"] }`. |
| `409 reserved` on subdomain add | Original owner's grace period — subdomain held until +118 days from lease expiry. |
| `429 rate_limited` | 100 req/sec project cap. Back off using `retry_after`. |
| CDN serves old bytes | Use the immutable URL from the upload response, or `run402 cdn wait-fresh <url> --sha <hex>`. |
| `422 relation already exists` on redeploy | Wrap migrations in `CREATE TABLE IF NOT EXISTS` + `DO`-block `ALTER TABLE`. |
| `insufficient_funds` right after faucet | Wait for the faucet tx to confirm (~5s on Base Sepolia) before setting the tier. |

## Tools Reference

This skill is the CLI — every action above is `run402 <verb>`. The full command reference (every flag, every subcommand) lives at <https://docs.run402.com/llms-cli.txt>. Treat that file as canonical when this body is silent on detail.

Top-level command groups:

```
run402 init | status | message | service
run402 wallets     | tier      | projects | sites      | subdomains
run402 domains     | functions | secrets  | jobs       | assets     | cdn
run402 email       | auth       | apps      | image
run402 deploy      | ai        | contracts | billing   | agent
```

Agent contact binding commands: `run402 agent contact --name <name> [--email <email>] [--webhook <url>]`, `run402 agent status`, `run402 agent verify-email`, and `run402 agent passkey enroll`. The assurance labels are `wallet_only`, `email_pending`, `email_verified`, `passkey_pending`, and `operator_passkey`; they describe mailbox/passkey continuity, not a humanhood claim.

Renewal: `run402 tier set <same-tier>` extends the lease in place and clears any grace-state timers. The CLI handles 402 negotiation automatically — call the same command again if a payment was just made.

## Links

- Full CLI reference: <https://docs.run402.com/llms-cli.txt>
- HTTP API reference: <https://run402.com/llms.txt>
- Status: <https://api.run402.com/status>
- Health: <https://api.run402.com/health>
- npm: [`run402`](https://www.npmjs.com/package/run402) · [`@run402/sdk`](https://www.npmjs.com/package/@run402/sdk) · [`@run402/functions`](https://www.npmjs.com/package/@run402/functions) · [`run402-mcp`](https://www.npmjs.com/package/run402-mcp)
- Remote MCP (no install, free discovery tools; never handles funds): `https://mcp.run402.com/mcp`
- Homepage: <https://run402.com>

First-deploy diagnostics: human principal names are preserved; client detection is separate. A synthetic vault snapshot backs up source without creating a local branch commit. Exposed-table permission denials may be native PostgREST `42501`; `REST_PERMISSION_DENIED` preserves the upstream status and does not infer RLS. An expected function 400/429 remains an application response even with CloudFront’s error cache header.
