run402: full-stack backend infrastructure for AI agents
Postgres, auth, storage, serverless functions and staged deploys — provisioned, operated and paid for by an agent through the CLI, with a typed SDK for scripting and MCP for tool-native hosts. No cloud console, no signup. Open source.
Run402 is open-source backend infrastructure for AI agents and coding agents — a backend-as-a-service addressed to a machine rather than to a person. An autonomous agent provisions a Postgres database, user auth, file storage, serverless functions and site hosting, ships them through one staged deploy workflow, and pays for the usage itself. Comparable in surface to Supabase, Firebase or Vercel; different in that there is no dashboard you have to sign into to get started (the console exists for people; the agent never needs it) and no human-issued API key to copy.
This is the backend Kychee's open products run on. We needed a layer an agent can drive end to end, with room for whatever each app turns out to need, and nothing off the shelf had all of it, so we built it and opened it the same way we open the apps: this repo holds the agent surfaces (MIT), run402-core holds the open self-hostable runtime slice (Apache-2.0; the managed Cloud control plane remains proprietary, see CLOUD_VS_CORE.md), and kysigned is the first product running on it.
One call to run402 gives an agent a full Postgres database, REST API, user auth, content-addressed file storage, static site hosting, serverless functions, and image generation, paid with x402 (USDC on Base) or MPP (pathUSD on Tempo, or sats over Bitcoin Lightning) — or card-funded allowance. The prototype tier is free on testnet.
Run402 is agent-first because agents are first-class participants, not because people disappear. A person or agent acts through its own Run402 principal and authenticator, and its actions remain attributable. Identity answers who acted; memberships, roles, grants, grant keys, freshness, and spend policy determine what that principal may do.
An autonomous agent may remain the legitimate owner of the org-of-one it creates. People may join through explicit co-ownership. Agents entering somebody else's organization receive bounded authority instead of borrowing a human account. Different keys. Equal standing. Explicit authority.
Use the CLI by default to provision, deploy, inspect and recover. Use the typed, opinionated SDK when writing programmatic TypeScript/JavaScript workflows; shell scripts and CI can keep using CLI. MCP serves MCP-native hosts, and direct HTTP supports deliberate lower-level integrations.
This monorepo ships these interfaces:
| Surface | Use when… |
|---|---|
run402 CLI |
Terminal, scripts, CI, agent-controlled shells: JSON in, JSON out, exit code on failure |
@run402/sdk |
Calling run402 from TypeScript: typed kernel, isomorphic (Node 22 / Deno / Bun / V8 isolates) with a Node entry that auto-loads the local keystore + wallet + x402 / Lightning fetch |
run402-mcp |
Claude Desktop, Cursor, Cline, Claude Code: core run402 operations as MCP tools |
| OpenClaw skill | OpenClaw agents (no MCP server required) |
| Run402 for Buzz | Buzz people and agents: install from run402.com, preflight/link one agent's dedicated identities, deploy a contextual demo, then offer human co-ownership through a normal HTTPS/passkey handoff; Buzz remains unchanged |
@run402/functions |
Imported inside deployed functions (db(req?), adminDb(), auth.user(), email, ai, assets) and for TypeScript autocomplete in your editor. Source lives in the public run402-core repo under packages/functions; run402 Cloud consumes the published npm package when it bundles function zips. |
@run402/astro |
Astro integration for SSR, ISR cache, hosted auth components, and image variants |
These interfaces share a single typed kernel where appropriate: @run402/sdk. MCP tools, CLI subcommands, and OpenClaw scripts are thin shims over SDK calls. @run402/functions is the in-function helper that runs inside deployed code; the npm package on the registry is the artifact Cloud bundles. @run402/astro layers the SDK and functions runtime into Astro's build and SSR flow. The HTTP API is the foundation; the SDK owns shared client workflows and orchestration; CLI and MCP expose them in machine-friendly forms. Native SDK/MCP references explain intentional alternatives.
Deploy summaries share the SDK workflow view. CLI writes redacted detail under .run402/diagnostics/; MCP retains it through expand_result. Typed SDK callers keep the full result. Snapshot collection excludes platform runtime files automatically.
30-second start
First create the complete run402.json and index.html from Your first deploy. Run these commands in that application directory; --name requests a new project.
npm install -g run402@latest
run402 up --name my-app -y # bootstrap wallet/tier/project/link, then deploy manifest
run402 up verify # rerun app HTTP verification without deploying
run402 up --verify # deploy, then wait for gateway/edge coherence
That's a real Postgres database + a deployed static site, paid for autonomously with testnet USDC.
Buy from any x402 seller with the same wallet and a default $0.10 ceiling:
run402 pay https://seller.example/translate --method POST \
--body '{"text":"hello"}' --max-usd 0.05 \
--idempotency-key translation:1 --require-receipt
The SDK equivalent is
r.pay.fetch(url, init, { maxUsdMicros, idempotencyKey, requireReceipt });
MCP callers run the same SDK call as a run snippet. All three return the
same x402-commerce-result.v1 settlement, movement/replay, delivery, offer,
merchant-receipt, signer-relationship, policy, and raw-evidence fields and pass
unpriced URLs through with payment: null. Requiring a receipt rejects before
payment when no wallet-rooted offer is eligible. If a promised receipt cannot
be verified after settlement, PaymentPolicyError retains the upstream
response and paid result and tells the caller to reconcile—never to pay again.
For a
trusted Run402 PAYMENT_INTENT_PENDING, all three surfaces prescribe one
recovery path: wait for Retry-After, then repeat the same request with the
same payer and key. Never replace the key. The SDK and MCP can also re-present
an ambiguous proof while their process remains alive; custom/arbitrary sellers
remain ambiguous and require reconciliation.
Prefer run402 up when a repo has run402.deploy.json or app.json. The CLI stays a thin shim over the Node SDK action runner (r.actions.run(...) / r.up(...)): it validates the manifest first, then recursively performs only the missing prerequisites. Project resolution is --project, .run402/project.json, manifest project_id, approved creation from --name; global active state never selects a deploy target. --name is project creation/link metadata only; it is not part of the deploy manifest and never renames an existing project. Use --check for local validation and --plan for gateway-reviewed intent before applying. Local validation covers every file the manifest references (migration sql_path/sql_file, function sources and files, site paths and dir() targets, assets.put sources): a missing one fails with MANIFEST_FILE_MISSING (details.missing[] of { field_path, path, kind }, one create_file next action per file) before any gateway call, in every mode and in run402 deploy. With no manifest in the working directory, UP_MANIFEST_REQUIRED looks one directory down and names what it found (details.nearby_manifests[], a read-only run_in_directory action for a single candidate, or one unranked select_application action for multiple apps); --manifest <path> to a missing file is a typed MANIFEST_NOT_FOUND.
If an app manifest defines verify.http[], run402 up verifies those URLs after deploy. Fresh run402 edge sentinel misses are reported as propagation_pending rather than permanent failures while the binding is still converging; tune that wait with --propagation-budget-s (default 120) or return immediately with --no-propagation-wait. run402 up verify reruns the same HTTP checks without uploading, deploying, creating projects, or mutating resources.
The CLI checks for newer run402 releases opportunistically and fail-open. Success stdout stays the command result; stale-version notices are advisory JSON on stderr, or cli.update_available NDJSON events in --json-stream. run402 doctor --refresh is the explicit live npm check and reports the install context plus the safest upgrade command for local, global, or ephemeral installs. run402 doctor answers { ok, blocking[], warnings[], checks[] }: ok is true exactly when blocking[] is empty, every check carries severity: "blocking" | "advisory" | "info", advisory findings (an unbound passkey, a stale CLI, vault gaps, a tier-less own org that can still reach another org's projects: TIER_MISSING_ON_OWN_ORG) land in warnings[] without changing ok or the exit code, and the tier check's status is a fixed vocabulary (ok | inactive | frozen | past_due | dormant | purged | missing | unknown | error, never a tier name; the name and raw lifecycle are in value.tier / value.lifecycle).
Typed deploy configs use the same commands. Executable configs are trusted local code, so v1 only runs them when passed explicitly:
run402 up --manifest run402.deploy.ts --check
run402 up --manifest run402.deploy.ts --plan
run402 up --manifest run402.deploy.ts --require-plan plan_...
--check and --print-spec are local-only (both verify that every referenced file exists). --plan asks the gateway for a reviewed plan with plan_id, plan_fingerprint, warnings, diff, and one next action. --require-plan reapplies only if the normalized spec and reviewed gateway events still match.
import { defineConfig, dir, nodeFunction, sqlFile } from "@run402/sdk/config";
export default defineConfig(({ env }) => ({
project_id: env.required("RUN402_PROJECT_ID"),
database: { migrations: [sqlFile("db/001_init.sql")] },
site: { replace: dir("dist"), public_paths: { mode: "implicit" } },
functions: { replace: { api: nodeFunction("dist/functions/api.js") } },
secrets: { require: ["OPENAI_API_KEY"] },
}));
Helpers normalize to the same ReleaseSpec as JSON manifests. dir() walks deterministically and rejects unsafe files unless explicitly allowed, sqlFile() derives the migration id from the filename unless supplied, and nodeFunction() currently expects JavaScript output; point TypeScript functions at built .js files.
The patterns
Paste-and-go assets: content-addressed URLs with SRI
Upload files with the CLI. Keep the returned AssetRef, including immutable identity and image variants, when saving references in application data. Do not reconstruct content hashes or variant URLs yourself.
run402 assets put ./logo.png ./app.js ./app.css --project prj_example
Use a manifest asset slice when these files must activate with a release. See the storage guide and native SDK AssetRef helpers for HTML emitters and programmatic composition. Binary files must remain bytes; never read them as UTF-8 before uploading.
Dark-by-default tables + the expose manifest
Tables you create are unreachable via /rest/v1/* until you declare them in a manifest. That closes the "agent created a table, forgot to set RLS, data leaked" footgun. A valid anon key against an existing but undeclared table gets a structured 403 TABLE_NOT_EXPOSED (never a bare Postgres 42501) whose next_actions say exactly that: expose_table (declare it and redeploy), edit_request (the expose endpoint), or use_function (keep it dark and read it from a function with adminDb()); it is not an RLS problem. The manifest is convergent: applying it twice is a no-op; items removed between applies have their policies, grants, triggers, and views dropped.
cat > manifest.json <<'EOF'
{
"$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"] }
]
}
EOF
run402 projects validate-expose <project_id> --file manifest.json
run402 projects apply-expose <project_id> --file manifest.json
run402 projects get-expose <project_id>
Built-in policies: user_owns_rows (rows where owner_column = auth.uid(); with force_owner_on_insert: true a BEFORE INSERT trigger sets it), public_read_authenticated_write (anyone reads, any authenticated user writes), public_read_write_UNRESTRICTED (fully open; requires i_understand_this_is_unrestricted: true), and custom (escape hatch: your own CREATE POLICY SQL).
Use run402 projects validate-expose for a non-mutating feedback loop before applying. Optional migration SQL is used only to check manifest references; it is not executed as a PostgreSQL dry run, and this does not validate deploy manifests.
Auth-as-SDLC: put the same JSON under database.expose in your v2 ReleaseSpec. The gateway validates it against your migration SQL during deploy and rejects mismatches with a structured errors array listing every violation.
Directory deploy (advanced primitive)
For a standalone static directory on an existing project:
run402 sites deploy-dir ./dist --project prj_example > result.json 2> events.log
Use run402 up for a complete application with a deploy manifest. The SDK owns file hashing, upload deduplication and release orchestration; the CLI renders progress and the result.
Same-origin web routes: static site + function ingress
Apply-v1 routes and static public paths are release resources: the release pointer activates after the required deploy stages in run402 deploy. Applied migrations and external side effects are not rolled back by changing that pointer. Release static asset paths such as events.html are distinct from browser-visible public static paths such as /events. Use site.public_paths for ordinary clean static URLs; keep routes for function ingress and exact, method-aware static aliases.
{
"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 the release asset events.html, while /events.html is not public unless separately declared. mode: "implicit" restores filename-derived public reachability and can widen access, so review gateway warnings before confirming it.
Omit routes or pass routes: null to carry forward base routes. Use routes: { "replace": [] } to clear the route table. Route entries are an ordered replace list, not a path-keyed map. Function targets use { "type": "function", "name": "<materialized function name>" }. Static route targets use exact patterns only, methods ["GET"] or ["GET","HEAD"], and { "pattern": "/events", "methods": ["GET","HEAD"], "target": { "type": "static", "file": "events.html" } } where file is a release static asset path, not a public path, URL, CAS hash, rewrite, or redirect. Use static route targets for method-aware aliases such as static GET /login plus function POST /login; in explicit public path mode the backing asset can stay private by filename. Direct /functions/v1/:name calls remain API-key protected; browser-routed paths are public same-origin ingress.
Function routes can charge a fixed tenant x402 price before the handler runs by adding pricing: { "mode": "always", "amount_usd_micros": 250000, "pay_to": "org_default_payout" } to the route entry. 250000 is $0.25 per matching action. The portable ReleaseSpec contract also accepts receipt: "on_fulfillment" on a priced function route; a compatible host then requires the function to return payment.fulfilled(response) before it authors a receipt. Run402-hosted advertising remains gated off until the standard delegated-signer carrier is available—receipt intent never silently downgrades. Omit networks for production mainnet only; include "testnet" explicitly for testnet acceptance. Add "lightning" (or "lightning-testnet") to sell for sats over the x402 exact scheme on Bitcoin Lightning, settled to your own node: register a receive-only Nostr Wallet Connect pairing once with run402 orgs lightning-receiver set <org_id> - (the pairing on stdin), and pin the sats price with "amount_msat" or let each challenge quote the USD price. A Lightning-only route needs no payout wallet; Run402 never holds the sats, a pairing that can spend, or a preimage. Static aliases cannot be priced, direct function invocation is not monetized, and service/admin keys do not bypass a priced browser route. The owning org must have a resolvable payout wallet: set it with run402 orgs payout-wallet <org_id> <wallet_address>. Conditional credit systems should expose one fixed-price rout
