run402: full-stack backend infrastructure for AI agents

MCP serverDatabases & data

x402 pay-per-call infra for agents: $0.03 image generation, Postgres, auth, storage, functions.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the run402 quickstart tool from run402: full-stack backend infrastructure for AI agents

From the project's README

As published by kychee-com/run402 in README.md.

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 in one atomic deploy, and pays for the usage itself. Comparable in surface to Supabase, Firebase or Vercel; different in that there is no dashboard to sign into 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 full backend (Apache-2.0), 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 Stripe credits). 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, delegates, 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.

This monorepo ships every surface an agent can pick up:

SurfaceUse when…
@run402/sdkCalling run402 from TypeScript: typed kernel, isomorphic (Node 22 / Deno / Bun / V8 isolates) with a Node entry that auto-loads the local keystore + allowance + x402 fetch
run402 CLITerminal, scripts, CI, agent-controlled shells: JSON in, JSON out, exit code on failure
run402-mcpClaude Desktop, Cursor, Cline, Claude Code: core run402 operations as MCP tools
OpenClaw skillOpenClaw agents (no MCP server required)
Run402 for BuzzBuzz 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/functionsImported 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/astroAstro 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. Pick whichever interface fits your runtime.

30-second start

npm install -g run402@latest
run402 up --name my-app -y                           # bootstrap allowance/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
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.

Buy from any x402 seller with the same allowance 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 use pay_url with require_receipt: true. 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, then approved active-project fallback. --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.

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.

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. --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 facts still match.

import { defineConfig, dir, nodeFunction, sqlFile } from "@run402/sdk/config";

export default defineConfig(({ env }) => ({
  project: 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

assets.put() returns an AssetRef whose scriptTag() / linkTag() / imgTag() emitters produce HTML with the URL, the SRI integrity hash, and modern best-practice attributes (defer, loading="lazy", decoding="async", crossorigin) already wired. The URL is content-addressed (pr-<public_id>.run402.com/_blob/<key>-<8hex>.<ext>), served through the CDN, and never needs invalidation:

import { run402 } from "@run402/sdk/node";
const r = run402();
const p = await r.project(projectId);

const logo  = await p.assets.put("logo.png", { bytes: pngBytes });
const app   = await p.assets.put("app.js",   { content: jsSource });
const style = await p.assets.put("app.css",  { content: css });

const html = `
<!doctype html>
<html>
  <head>${style.linkTag()}${app.scriptTag({ type: "module" })}</head>
  <body>${logo.imgTag("Company logo")}</body>
</html>
`;

Binary files must enter the SDK as bytes. In Node, use readFile(path) without an encoding; in browsers, use File.arrayBuffer(). Never read PNG, WASM, fonts, audio, video, archives, or other binary formats as UTF-8 and then hash or re-encode the resulting string: CAS can verify only the bytes it receives. The SDK rejects string sources for known binary keys/MIME types with BINARY_CONTENT_REQUIRES_BYTES before making a request. Directory helpers such as fileSetFromDir, dir, and assets.uploadDir are byte-safe by construction.

immutable: true is the default: the SDK computes the SHA-256 client-side, the gateway returns a content-hashed URL, and the browser refuses execution on byte mismatch. No cache-invalidation choreography, no waiting, no integrity-attribute construction.

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. 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 or the MCP validate_manifest tool 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.

Slick deploys: deployDir + plan/commit + progress

deployDir walks a local directory, hashes every file client-side, asks the gateway which bytes it doesn't already have, and PUTs only those. Re-deploying an unchanged tree returns immediately with bytes_uploaded: 0.

import { run402 } from "@run402/sdk/node";

const r = run402();
const { url, bytes_uploaded, bytes_total } = await r.sites.deployDir({
  project: projectId,
  dir: "./dist",
  onEvent: (e) => process.stderr.write(JSON.stringify(e) + "\n"),
});

Progress events stream over onEvent (or stderr from the CLI) as unified DeployEvent JSON objects from the v2 deploy primitive.

CLI:

run402 sites deploy-dir ./dist --project prj_… > result.json 2> events.log

Same-origin web routes: static site + function ingress

Apply-v1 routes and static public paths are release resources: they activate atomically with the site, functions, migrations, secrets, and subdomains in the same deploy apply. 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. 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 r.org(orgId).setPayoutWallet({ walletAddress }), run402 org payout-wallet <org_id> <wallet_address>, or MCP set_org_payout_wallet. Conditional credit systems should expose one fixed-price route such as POST /api/credits, then keep the rest of the app behind unpriced routes and app-local authorization.

Matching is exact or final-prefix-wildcard only. /admin and /admin/ are exact trailing-slash equivalents; /admin/* matches children but not /admin, /admin/, /admin.css, or /administrator, so deploy both /admin and /admin/* for a routed section root. Query strings are ignored for matching and preserved in the handler's full public req.url. Exact routes beat prefix routes; longest prefix wins; method-compatible dynamic routes beat static assets. A POST /login route can coexist with static GET /login HTML. Unsafe method mismatch returns 405, and matched dynamic route failures fail closed instead of falling back to static files.

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. On priced routes, import getRoutedPaymentContext from @run402/functions, read const paymentContext = getRoutedPaymentContext(req), and key app-side idempotency by paymentContext.paymentId. For a receipt-enabled route, return payment.fulfilled(response) only after the response represents completed delivery; the helper fails closed outside a settled, current, receipt-enabled routed invocation. The context helper reads gateway-confirmed x-run402-payment-* headers and returns null for unpriced or direct calls. The raw run402.routed_http.v1 envelope is internal; do not write route handlers against it.

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 / while keeping the shell for app routes, ship home.html at the site root alongside index.html and add an exact root static route alias: "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 (route_static_alias), unmatched app routes such as /dashboard still serve the shell (spa_fallback), and named static pages keep serving unchanged (static_exact). Expect two non-blocking plan lints: STATIC_ALIAS_SHADOWS_STATIC_PATH (warn: the alias overrides what / would otherwise serve; accurate and expected here) and STATIC_ALIAS_DUPLICATE_CANONICAL_URL (info: /home.html stays directly reachable in implicit public-path mode; add <link rel="canonical"> to home.html if duplicate-content SEO matters). Omitting routes on later deploys carries the alias forward; routes.replace is total, so a pipeline that sends it must include the alias every time. Verify with run402 deploy resolve --url https://<your-site>/ --method GET or deploy_diagnose_url and confirm match: "route_static_alias" with target_file: "home.html".

Avoid routing every static file, broad method lists by default, wildcard static route targets, leading-slash static files, directory shorthand, and one-static-route-target-per-page tables that exhaust route limits. Also watch wildcard function routes that shadow direct public static paths. Warning codes to handle include 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; inspect active routes, static_public_paths, and resolve diagnostics to distinguish the route pattern from the backing asset_path.

Diagnose public URLs with the URL-first CLI or MCP/SDK equivalents:

run402 deploy diagnose --project prj_123 https://example.com/events --method GET
run402 deploy resolve --project prj_123 --url https://example.com/events?utm=x#hero --method GET
run402 deploy resolve --project prj_123 --host example.com --path /events --method GET

Shortened here. Read the whole README on GitHub.

Tools it offers (4)

What this server listed when ahel dialed its public endpoint in Sep 2026, with no key and no account of yours. The names are the server’s own.

  • run402_quickstart
  • x402_price_check
  • submit_wall_claim
  • experiment_scoreboard

Signals

GitHub stars
24
Forks
3
Last commit
Sep 2026
Weekly downloads
5k
Advanced
Delivery
mcp MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
Catalog kind
mcp-server
Gateway key
com-run402-mcp
Source
github.com/kychee-com/run402
Hosted endpoint
https://mcp.run402.com/mcp