Source profileQuality 93/100Review permissions

kychee-com/run402/openclaw/SKILL.md

run402

Provision Postgres + REST API + auth + content-addressed storage + serverless functions + email — paid with x402 USDC on Base. Prototype tier is free on testnet.

Source repository stars
23
Declared platforms
0
Static risk flags
3
Last source update
2026-08-26
Source checked
2026-08-26

Decision brief

What it does: where it fits

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…

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/kychee-com/run402 --skill "openclaw"
    Safe inspection promptEditorial

    Inspect the Agent Skill "run402" from https://github.com/kychee-com/run402/blob/4cd2c9aed8e95e2b2c7b24e0221158405a3bfe3c/openclaw/SKILL.md at commit 4cd2c9aed8e95e2b2c7b24e0221158405a3bfe3c. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      How to think about it

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

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

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

      Review the “OWNED-ORG moves have no follow-up accept/claim step in the same-actor release” section in the pinned source before continuing.

      Review and apply the “OWNED-ORG moves have no follow-up accept/claim step in the same-actor release” source section.
    3. 03

      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:

      These work on a fresh install before init — useful for evaluating Run402 or distinguishing platform problems from your own:Don't confuse with run402 status (your organization's allowance, balance, tier, projects).
    4. 04

      Standard Workflow — zero to deployed

      Review the “Standard Workflow — zero to deployed” section in the pinned source before continuing.

      Review and apply the “Standard Workflow — zero to deployed” source section.
    5. 05

      Agent Allowance Setup

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

      Coinbase AgentKit — MPC wallet on Base with built-in x402.AgentPayy — auto-bootstraps an MPC wallet on Base via Coinbase CDP.The CLI manages a local agent allowance — a wallet key dedicated to paying Run402, stored at /.config/run402/allowance.json (mode 0600). You never touch the private key directly.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 16

    The documentation includes network, browsing, or remote request actions.

    run402 subdomains claim my-app # → https://my-app.run402.com

    Network access

    medium · line 28

    The documentation includes network, browsing, or remote request actions.

    run402 init --api-base=http://my-core:4020

    Writes files

    medium · line 68

    The documentation asks the agent to create, modify, or delete local files.

    run402 wallets bind kychon # write ./.run402.json - this repo uses kychon (safe to commit)

    Runs scripts

    medium · line 393

    The documentation asks the agent to run terminal commands or scripts.

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

    Runs scripts

    medium · line 399

    The documentation asks the agent to run terminal commands or scripts.

    git add .github/workflows/run402-deploy.yml run402.deploy.json

    Writes files

    medium · line 1090

    The documentation asks the agent to create, modify, or delete local files.

    run402 claims create repo:src/auth/** --note "migrating to passkeys" # claim before you edit — advisory

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score93/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars23SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    kychee-com/run402
    Skill path
    openclaw/SKILL.md
    Commit
    4cd2c9aed8e95e2b2c7b24e0221158405a3bfe3c
    License
    MIT
    Collected
    2026-08-26
    Default branch
    main
    View the original SKILL.md

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

    Every example below is a CLI command. The CLI prints JSON to stdout, JSON errors to stderr, and exits 0 on success / 1 on failure — designed for shells, scripts, and agent loops.

    30-second start

    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
    run402 subdomains claim my-app             # → https://my-app.run402.com
    

    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 deploy apply; 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, then approved active-project fallback. --dry-run prints planned steps[] without mutating. 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.

    Self-hosted Core target

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

    run402 init --api-base=http://my-core:4020
    run402 projects provision --name my-app    # → anon_key, service_key, project_id
    run402 deploy apply --manifest app.json    # uses the active Core project
    

    For Core, init --api-base stores the target in the active profile and does not create a Cloud allowance, 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 scratchrun402 init
    Buy from an x402-priced URLrun402 pay <url> --max-usd <amount>
    Redeem a promo code you were givenrun402 redeem <code> (or run402 init --voucher <code>)
    Bootstrap/link/deploy a manifest reporun402 up --name <name> -y
    Bootstrap/link/deploy and wait for edge coherencerun402 up --name <name> -y --verify
    Rerun app HTTP verificationrun402 up verify --project <project_id>
    Point at self-hosted Corerun402 init --api-base=<url>
    Make a databaserun402 projects provision
    Run SQL on itrun402 projects sql
    Check an auth manifest before applyingrun402 projects validate-expose
    Make a table reachable from the browserrun402 projects apply-expose
    Deploy a frontend from a directoryrun402 sites deploy-dir <path>
    Link GitHub Actions deploysrun402 ci link github
    Stash a file with a paste-able CDN URLrun402 assets put <file>
    Run code on the serverrun402 functions deploy
    Send emailrun402 email send
    Sign on-chainrun402 contracts call
    One-call full-stack deployrun402 deploy apply --manifest app.json

    The active project is sticky — run402 projects use <id> server-validates the project and makes it the default for every subsequent <id>-taking command. Most commands work without an explicit <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>/.

    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 handoff, 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 handoffs.

    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.

    run402 credentials project-keys list
    run402 credentials project-keys status --project <id>      # redacted cache status
    run402 credentials project-keys export --project <id> --reveal
    run402 projects get <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 <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 apply, follow the chain it hands back — no allowance -> 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 apply 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:

    { "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" }] }
    
    { "message": "Payment required.", "code": "PAYMENT_REQUIRED", "category": "payment", "retryable": true, "safe_to_retry": true, "next_actions": [{ "type": "submit_payment" }] }
    
    { "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

    deploy-dir — the modern path

    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.

    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:

    phaseWhen firedExtra fields
    planAfter the planning requestmanifest_size (file count)
    uploadAfter each missing file finishes PUTingfile, sha256, done, total
    commitJust before commit
    pollPer server-side copy pollstatus, elapsed_ms

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

    deploy — one-call full stack

    For a database + migrations + manifest + secret dependencies + functions + site + subdomain, set secret values first, then deploy a value-free manifest:

    run402 secrets set <project_id> OPENAI_API_KEY --file ./.secrets/openai-key
    run402 deploy apply --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:

    run402 deploy release active --project prj_...
    run402 deploy verify op_... --project prj_... --wait --timeout 120
    run402 deploy release get rel_... --project prj_...
    run402 deploy release diff --from empty --to active --project prj_...
    

    After pointer-swap recovery, compose promote with verification:

    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 deployment-copy bytes, and immutable/CAS warning counts.

    Same-origin web routes

    Use run402 deploy apply 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.

    {
      "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 tenant 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. 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 org 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, deployment 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 diagnose --project prj_123 https://example.com/events --method GET before mutating deploy state when the question is "what would this public URL serve?" For lower-level parity use run402 deploy resolve --project prj_123 --url https://example.com/events?utm=x#hero --method GET or run402 deploy resolve --project prj_123 --host example.com --path /events --method GET; never combine --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 means review app auth, CSRF, CORS/OPTIONS, and cookies before retrying with --allow-warning PUBLIC_ROUTED_FUNCTION; 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 set acknowledge_readonly: true on an intentionally read-only GET/HEAD final-wildcard function route. 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 apply --manifest app.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 apply --manifest run402.deploy.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-TWzh-Hantzh); 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)):

    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):

    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:

    function setLanguage(lang) {
      localStorage.setItem('wl_locale', lang);
      document.cookie =
        `wl_locale=${encodeURIComponent(lang)}; path=/; max-age=31536000; samesite=lax`;
    }
    
    { "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:

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

    Provision first (run402 projects provision) so you have the anon_key to embed in your HTML before deploying.

    GitHub Actions OIDC deploys

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

    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:

    run402 deploy apply --manifest run402.deploy.json --project prj_...
    

    Useful follow-ups:

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

    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.

    {
      "$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:

    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:

    run402 projects apply-expose <id> --file manifest.json
    run402 projects get-expose   <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

    PolicyAllows
    user_owns_rowsRows 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_writeAnyone reads. Any authenticated user writes any row. For shared boards / collaborative content.
    public_read_write_UNRESTRICTEDFully open. Requires i_understand_this_is_unrestricted: true on the table entry. Only for guestbooks / waitlists / feedback forms.
    customEscape 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:

    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:

    FieldUse
    cdn_urlThe content-addressed URL — paste straight into src= / href=
    srisha256-<base64> for <script integrity="…"> if you build tags by hand
    etagStrong "sha256-<hex>" ETag
    cache_kindimmutable / mutable / private
    metadata (v1.50)Echo of your --meta bag, or null
    image_format (v1.50)Decoded format (jpeg/png/webp/…) or null for non-images
    image_info (v1.50)has_alpha, color_space, animated, frame_count, bit_depth, orientation
    image_exif (v1.50)EXIF block (null when stripped or no EXIF)
    image_exif_policy (v1.50)keep or strip, echoing the applied policy

    Immutable upload is the default since v1.45 — 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.

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

    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 (v1.50) 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

    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

    run402 projects sql <id> "CREATE TABLE items (id serial PRIMARY KEY, title text NOT NULL, user_id uuid)"
    run402 projects sql <id> --file migrations.sql
    run402 projects sql <id> "SELECT * FROM items WHERE id = \$1" --params '[42]'
    
    run402 projects rest <id> items "select=id,title&order=id.desc&limit=10"
    run402 projects validate-expose <id> --file manifest.json --migration-file migrations.sql
    run402 projects schema <id>          # introspect tables, columns, RLS
    run402 projects usage  <id>          # API calls, storage, lease expiry
    run402 projects costs <id> --window 30d  # operator-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:

    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.

    run402 functions deploy <id> my-fn --file fn.ts \
      --timeout 30 --memory 256 \
      --schedule "*/15 * * * *" \
      --deps "stripe,zod@^3,[email protected]"
    
    run402 functions invoke my-fn --project <id> --body-file request.json
    run402 functions invoke paid-fn --project <id> --body-file request.json --idempotency-key paid:call:123 --wait
    run402 functions logs   <id> my-fn --tail 100 --request-id req_abc123 --follow
    run402 functions runs create <id> my-fn --event-type reminder.send --idempotency-key reminder:123 --delay 10m
    run402 functions update <id> my-fn --schedule "0 */6 * * *"
    run402 functions rebuild <id> my-fn      # refresh ONE function onto the current platform runtime
    run402 functions rebuild <id> --all      # refresh every function in the project
    run402 functions list   <id>
    run402 functions delete <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 ([email protected]) 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:

    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 legacy helpers such as getUser, getUserId, and getRole were retired in @run402/functions v3.0 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 (v1.51+)

    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 apply --manifest run402.deploy.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:

    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 apply 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 (v1.52+)

    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:

    {
      "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 — 5 health checks (credentials, allowance, project, network, SDK build). Output is JSON by default (no flag needed); exit 1 on fail.
    • 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 (top section), cli/llms-cli.txt (R402_* SSR Runtime Error Codes section).

    Rehearsals, snapshots, and branches

    For database-bearing deploys, rehearse before commit:

    run402 apply --manifest app.json --rehearse --json
    run402 deploy rehearse <plan_id> --project prj_... --teardown on_pass --json
    

    run402 apply --rehearse is the canonical one-shot path: plan, upload missing CAS bytes, create a contained branch, apply the candidate plan there, run checks, and exit nonzero on a failed rehearsal. Add --commit only after the rehearsal report is acceptable.

    Manual restore points and branches are reference primitives:

    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 user/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 user 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:

    run402 cloud archives create <project_id> --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 core projects import ./project.r402ar --name imported-project --env-file ./required.env --json
    run402 projects export <project_id> --output ./project.r402ar --json
    run402 core projects apply ./project.r402ar --name imported-project --env-file ./required.env --json
    

    projects export aliases the Cloud archive export flow; core projects apply aliases Core archive import. MCP tools mirror the same flow: export_project_archive, inspect_project_archive, verify_project_archive, and import_project_archive. 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/fleet 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.

    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:

    TierMax scheduledMin interval
    Prototype115 min
    Hobby35 min
    Team101 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.

    run402 secrets set    <id> STRIPE_KEY --file ./.secrets/stripe-key
    printf %s "$STRIPE_KEY" | run402 secrets set <id> STRIPE_KEY --stdin
    run402 secrets set    <id> JWT_PRIVATE --file ./private.pem
    run402 secrets list   <id>
    run402 secrets delete <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.

    run402 jobs submit --file job.json --project <id>
    run402 jobs get    <job_id> --project <id>
    run402 jobs logs   <job_id> --project <id> --tail 100
    run402 jobs cancel <job_id> --project <id>
    run402 jobs purge           --project <id>
    run402 jobs artifacts get <job_id> result.json --output ./result.json --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 } (the old run402:// ref strings were retired; 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.

    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 [email protected] \
      --template notification \
      --var project_name="My App" --var message="Hello"
    
    run402 email send --to [email protected] \
      --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

    run402 email webhooks register --url https://… --events delivery,bounced,reply_received
    run402 email webhooks list
    run402 email webhooks update <id> --events delivery,bounced,complained,reply_received
    run402 email webhooks delete <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

    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 (claimed subdomain, project public-id host, active custom domain, or localhost when allowed).

    run402 auth magic-link --email [email protected] --redirect https://my-app.run402.com/cb
    run402 auth verify --token <token>                       # → access_token + refresh_token
    run402 auth magic-link --email [email protected] --delivery both --redirect https://my-app.run402.com/cb
    run402 auth verify --challenge-id <opaque-id> --code <six-digits>
    run402 auth invite-user --email [email protected] --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 <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

    run402 subdomains claim 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, operator session, or delegate), so a project shown by run402 projects list is not vetoed by a missing local project-key cache entry.

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

    On-chain — KMS signers

    For agents that need to sign Ethereum transactions. Private keys never leave AWS KMS — there is no export, ever. $0.04/day rental + $0.000005/call. Signer creation requires $1.20 in cash credit (30 days prepaid). Non-custodial — see https://run402.com/humans/terms.html#non-custodial-kms-wallets.

    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 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 KMS deletion window
    

    Tier and billing

    Tier is per organization, not per project. run402 tier set is organization-wide — one subscribe / 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).

    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 [email protected]
    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.

    After subscribing 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 → subscribe; same tier active → renew; higher tier → upgrade (prorated refund); lower tier → downgrade (prorated refund if usage fits).

    Resource limits

    PrototypeHobbyTeam
    Lease7 days30 days30 days
    Storage250 MB1 GB10 GB
    API calls500K5M50M
    Functions525100
    Function timeout10s30s60s
    Function memory128 MB256 MB512 MB
    Secrets1050200
    Scheduled fns1 / 15min3 / 5min10 / 1min

    run402 deploy apply 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)

    Gateway v1.57 moved the lifecycle state machine from internal.projects to internal.organizations. The grace clock now 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:

    StateWhenWhat happens
    activeFull read/write
    past_dueday 0Site, REST, email keep serving. Owner gets first email.
    frozen+14dControl plane (deploys, secrets, subdomain claims, 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+44dScheduled (cron) functions pause.
    purged+104dCascade: schemas dropped, Lambdas deleted, mailboxes tombstoned. Subdomains become claimable 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 — operator escape hatch flag. When true, the organization never advances past active. Replaces the v1.56 per-project pinned. Toggle via run402 admin lease-perpetual <org_id> --enable | --disable (platform-admin only).

    Operator 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 v1.96+)

    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 (recipient completes with accept); an email is an email→org transfer (recipient completes with claim, claiming into an org they own). --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.

    # 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 [email protected] --project <project_id> [--retain-collaborator 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 "..."]
    
    # WALLET recipient completes (your wallet must equal the transfer's to_wallet)
    run402 transfer accept <transfer_id>
    
    # EMAIL recipient completes — claim into an org (omit --into to create a new org)
    run402 transfer claim <transfer_id> [--into <org_id>] [--accept-retained-collaborator]
    
    # OWNED-ORG moves have no follow-up accept/claim 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 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 facts (a deployed function's own events.emit(...) calls) alongside those platform events, source-discriminated.

    run402 events                     # active project, from the earliest retained event
    run402 events --cursor evc_1a2b   # everything since last time
    run402 events --org <org_id>      # org-wide union across all the org's projects
    run402 events --source app        # just this project's own emitted business facts
    run402 events --source app --type signature_completed,booking_created
    

    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.

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

    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 ack msg_2f                                # confirm you saw a handoff
    run402 claims release clm_1a                           # hand off: release + a messages send note
    

    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 OpusOpus-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 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 delegate (RUN402_DELEGATE_TOKEN) reaches its own project's default room plus named org rooms, a project service key is read-only.

    Your source, encrypted before it leaves the machine — gitvault

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

    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 gitvault 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:

    run402 init                                   # once per machine
    git remote add origin run402::<org-slug>/<name>
    git push -u origin main                       # push-to-creates the repo on first push, publishes
    

    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. State this plainly if the user asks: V0 is single-principal — exactly one machine can open the vault until human envelopes ship.

    Named addressing (design D6). run402::<org-slug>/<name> and the id-form run402::<org_id>/<project_id> both work in the same git remote slot. run402 org slug <slug> (owner-only, a small one-time fee) claims 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 — the same track, one call. No address to remember, no separate gitvault init:

    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 delete prj_xyz --force           # refuses without --force while the vault holds generations,
                                                   # naming what would be lost first
    run402 repos name my-notes --project prj_xyz  # explicit address-form name claim (no fee)
    

    repos create|list|delete|name are CLI/OpenClaw-only — no MCP tool exists or will exist for this family, the same law that keeps gitvault's own mutating verbs off MCP (a create mints a one-shot recovery receipt; delete is irreversible). 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.

    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 gitvault init                          # ALLOCATE the vault explicitly (mints key material + a recovery receipt)
    run402 gitvault snapshot --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 gitvault status                        # what this machine and the control plane each believe
    run402 gitvault verify --budget 500           # verify the head chain from your authenticated pin
    run402 gitvault compact                       # checkpoint under a maintenance lease (owner)
    run402 gitvault prune                         # plans; add --submit --intent-core/--verifier-receipt to submit
    

    --project <id> picks the vault's project; --repo <repo_id> addresses the vault 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 gitvault status | jq stays clean.

    Allocation is its own verb: run402 gitvault init. 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 gitvault snapshot allocate it the SAME way, lazily, on first use, printing the receipt to stderr. Allocating does NOT gate the project's deploys (design D3). gitvault_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 gitvault 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, deploy apply produces the vaulted capture automatically on machines holding the keystore; un-gate with run402 gitvault policy grandfathered --reason "<why>" (owner + step-up, audited, reversible). Capturing source is never gated on a deploy, either way. Before snapshot 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. verify 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 — the verified prefix persists, so run it again to resume).

    run402 gitvault push still works, one release, printing a deprecation warning — it is the OLD name for snapshot. Prefer snapshot: push now means exactly one thing everywhere, an ordinary git push.

    A vault-only project is first-class. run402 init (or run402 repos create <name>), then git push origin …, then compact / prune / verify, and never a deploy. Nothing in allocation, admission, retention, or maintenance requires a deployment 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 user 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 gitvault status prints that sentence verbatim. Back up the keystore directory run402 gitvault status reports as keystore.root and prints under the terminal-loss statement — ~/.config/run402/gitvault for the default wallet, ~/.config/run402/profiles/<wallet>/gitvault 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.

    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.

    There is no separate gitvault price: a vault's bytes count against the same organization-pooled storage budget the project already has, charged once per unique object with a 64 KiB per-object accounting floor.

    After a promote — gate on new error identities

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

    run402 errors                                   # last 24h: verdict + grouped identities
    run402 errors --function checkout --kind uncaught
    run402 errors fp_9b21fa                          # one fingerprint, all samples + logs drill-down
    run402 errors --new-in active                    # what's new under the live release
    run402 errors --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 --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, 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

    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

    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

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

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

    Apps marketplace

    run402 apps browse [--tag <tag>]
    run402 apps inspect <version_id>
    run402 apps fork    <version_id> my-clone --bootstrap '{"admin_email":"[email protected]"}'
    run402 apps publish <id> --description "…" --tags todo,demo --visibility public --fork-allowed
    run402 apps versions <id>
    run402 apps update   <id> <version_id> --description "…" --tags a,b
    run402 apps delete   <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:

    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

    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

    # 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 + claim subdomain
    run402 sites deploy-dir ./dist
    run402 subdomains claim my-app
    
    # 6. Optional: a server function
    run402 functions deploy $PROJECT my-fn --file fn.ts
    

    Payment Handling

    Two payment rails work with the same wallet key:

    • x402 (default): USDC on Base. Prototype = Base Sepolia testnet (free from faucet). Hobby/Team = Base mainnet.
    • MPP: pathUSD on Tempo Moderato. Prototype = testnet (instant faucet, no rate limit). Hobby/Team = Tempo mainnet.

    Switch rails via run402 init mpp 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 allowance. 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 allowance: human sends USDC on Base mainnet to the address field from run402 allowance export. Agent pays Run402 autonomously via x402 from then on.
    • Path B — Stripe credits: 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

    • Provision before authoring HTML. The anon_key is permanent and must be embedded in your frontend; write the HTML after provision returns it.
    • 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 Allowance Setup

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

    run402 allowance create               # generate a fresh allowance
    run402 allowance status               # address, network, funding state
    run402 allowance fund                 # request testnet USDC from the faucet
    run402 allowance balance              # USDC on mainnet + testnet + billing balance
    run402 allowance history              # ledger
    run402 allowance export               # print {"address":"0x..."} (NOT the private key)
    run402 allowance checkout --amount 5000000   # Stripe top-up to billing balance ($5)
    

    Most agents only need run402 init once — it composes create + fund + tier status + projects list.

    Other allowance options:

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

    Operator session (human / email)

    The operator is the human, identified by email — distinct from the agent (your wallet). One browser login spans every wallet that verified your email, so operator overview returns the cross-wallet union. For a single wallet's state, use run402 status (there is no operator status).

    run402 operator login            # browser device-auth (RFC 8628, like `aws sso login`): magic-link OR passkey
    run402 operator overview         # organization view across ALL wallets controlling your email (requires login)
    run402 operator whoami           # cached session: email, wallets, expiry - local, no network
    run402 operator logout           # revoke server-side + clear the local cache
    

    login prints a verification URL + short code (and opens the browser on a TTY); you approve in the browser, and the CLI brokers the resulting session token (cached at the base config dir, ~30m TTL, shared across named wallets). overview requires login and never falls back to a single wallet's slice. These are CLI-only (no MCP tool) by design — MCP authenticates as the agent, not the human.

    Troubleshooting

    You seeLikely cause / fix
    402 payment_required on tier setAllowance is empty. run402 allowance fund (testnet) or fund with real USDC.
    403 with lifecycle_state: frozenProject past lease + 14 days. run402 tier set <tier> reactivates instantly.
    403 admin_requiredSubcommand is platform-admin only (e.g., run402 admin lease-perpetual, run402 admin archive, run402 admin reactivate). Use a platform admin allowance wallet; project owners can't toggle these.
    Empty [] from /rest/v1/items for anonTable not in manifest with expose: true. Run run402 projects apply-expose.
    403 forbidden_function calling an RPCFunction's not in the manifest's rpcs[]. Add { name, signature, grant_to: ["authenticated"] }.
    409 reserved on subdomain claimOriginal owner's grace period — subdomain held until +118 days from lease expiry.
    429 rate_limited100 req/sec project cap. Back off using retry_after.
    CDN serves old bytesUse the immutable URL from the upload response, or run402 cdn wait-fresh <url> --sha <hex>.
    422 relation already exists on redeployWrap migrations in CREATE TABLE IF NOT EXISTS + DO-block ALTER TABLE.
    insufficient_funds right after faucetWait for the faucet tx to confirm (~5s on Base Sepolia) before subscribing.

    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 allowance   | tier      | projects | sites      | subdomains
    run402 domains     | functions | secrets  | jobs       | assets     | cdn
    run402 email       | auth       | apps      | image
    run402 deploy      | ai        | contracts | billing   | agent
    

    Agent operator 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

    Frequently asked questions

    What to verify before installation and use

    What does the run402 source document cover?

    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…

    How do I install run402?

    The source record exposes this install command: npx skills add https://github.com/kychee-com/run402 --skill "openclaw". Inspect the command and pinned source before running it.

    Which permission-related actions were detected?

    Static rules flagged network, write-files, exec-script in the source; the page lists the matching lines and excerpts.

    Alternatives

    Compare before choosing

    Computed 9523

    kychee-com/run402

    run402

    Provision Postgres + REST API + auth + content-addressed storage + serverless functions + email — paid with x402 USDC on Base. Prototype tier is free on testnet. Use when the user asks to build a webapp, deploy a site, create a database, generate images, or mentions Run402.

    Computed 973,106

    NVIDIA/skills

    vss-deploy-detection-tracking-2d

    Use this skill when the user wants to deploy, run, debug, tear down, or call the REST API of the RTVI-CV 2D detection / tracking microservice. Trigger when the user says things like 'deploy rtvi-cv', 'start warehouse 2d', 'add a stream', 'check rtvi-cv health', or 'stop the perception container'. Not for VLM, embedding, or analytics — use the matching vss-* skill.

    Computed 97149

    UiPath/skills

    uipath-coded-apps

    UiPath Coded Apps — scaffold, build, run, and deploy Coded Web Apps and Coded Action Apps: React/TypeScript apps that call UiPath Cloud APIs via the `@uipath/uipath-typescript` SDK and ship to Automation Cloud (push/pull to Studio Web, pack, publish, deploy, OAuth-PKCE). Also generates live analytics & governance dashboards from a plain-language request, wired to tenant data via the Insights real-time API, with edit and deploy flows. For RPA→uipath-rpa, Python agents→uipath-agents, Maestro flows

    Computed 963,106

    NVIDIA/skills

    vss-deploy-detection-tracking-3d

    Deploy and operate the RTVI-CV-3D microservice as MV3DT (`MODE=mv3dt`): per-camera DeepStream perception plus BEV Fusion over calibrated cameras. Supports the bundled sample dataset, custom video files, and RTSP streams, and chains to `vss-generate-video-calibration` when calibration is missing. Use `vss-deploy-profile` for the full warehouse blueprint and `vss-deploy-detection-tracking-2d` for single-camera 2D detection.