Skip to content

CLI reference

CLI: npm install -g run402@latest SDK: npm install @run402/sdk (typed TS client; same namespaces; Node/Deno/Bun/V8 isolates) Docs URL: https://docs.run402.com/llms-cli.txt HTTP API reference: https://run402.com/llms-full.txt Operated by: Kychee, Inc. Terms: https://run402.com/humans/terms.html Contact: run402 feedback send "your message" (requires active tier)

Run402 = Postgres + REST + Auth + Storage + static & Astro-SSR site hosting + same-origin routes + Node 22 functions + email + image generation behind one CLI.

Run402 is agent-first because agents are first-class participants, not because people disappear. Use your own principal and authenticator rather than a borrowed human account. Identity records who invoked the command; organization roles, grants, grant keys, freshness, and spend policy determine what that principal may do. A founder agent may remain owner of its org-of-one.

Agent-critical events:

  • Shared full-stack workflow: run402 up coordinates planning, staging, migrations, activation and verification. Failures can leave resumable or partial work; promoting an earlier release does not undo migrations. Inspect returned operation state and next actions. deploy is the advanced apply primitive.
  • No platform token: the local wallet (~/.config/run402/wallet.json) signs requests. Per-project anon_key / service_key are runtime data-plane keys (PostgREST/Storage/Functions), permanent, and embeddable/server-side respectively.
  • Agent-paid usage: x402 USDC on Base, MPP pathUSD on Tempo, or sats over Bitcoin Lightning (MPP) from the agent’s platform-minted wallet, signed by the local wallet. The organization’s allowance (card, Lightning, or voucher funded) settles first when it covers the price; CLI behavior is unchanged. run402 wallets <current|fund|balance> — current reports the active wallet’s address, rail, file path, and faucet use; balance reads on-chain funds plus the organization’s allowance_usd_micros; run402 wallets lightning status reports a Lightning profile’s lightning block (wallet id, status, custody run402_hub, address, budget, starter, balance_sats, budget_remaining_sats; never the pairing).

Install + deploy — the only first-deploy path (the front door at https://run402.com/llms.txt is the same three lines with a full manifest):

Terminal window
npm install -g run402@latest
run402 up --name "my-app" -y # wallet, tier, project, your name, rehearsal when warranted, deploy
run402 redeem <code> # only if you were given a promo code — see Promo codes below

up is a thin CLI shim over the SDK action runner; the SDK owns manifest validation, project resolution, recursive prerequisites, idempotency-key derivation, and deploy. A static page never needs a key copied into it: every Run402 host serves /_run402/config.js (window.RUN402 = { project_id, api_base, anon_key }) — see “Runtime config on every host” below. prototype is free with the testnet faucet; use hobby / team for mainnet.

Rehearsal is up’s decision, not a flag: a migration-bearing plan against a project with a live release is rehearsed on a contained branch and committed only on a passing report; a first deploy has nothing to protect and commits directly — see “Protecting a live database” below. run402 deploy, run402 projects provision, and run402 deploy rehearse are the primitives underneath up: up does any missing setup and then runs the same deploy run402 deploy runs on its own.

CLI update awareness is advisory and fail-open. Normal commands never wait for npm; they use cached update state and keep success stdout as the command payload. Stale notices appear as structured JSON on stderr, or as {"type":"cli.update_available",...} in --json-stream. RUN402_NO_UPDATE_CHECK=1 suppresses notices/checks; CI skips live checks unless RUN402_UPDATE_CHECK=1; run402 doctor --refresh is the explicit bounded live check.

App build scripts should read the same target/profile store through resolveRun402TargetProfile() from @run402/sdk/node, not by parsing target.json or local project-key cache files themselves.

  • Wallet: ~/.config/run402/wallet.json (0600); active project state: profile state.json; local project-key cache: profile credentials/project-keys.v1.json (0600). Legacy projects.json is migration input only.
  • Project keys are cached automatically after provision or fork for operations that truly need anon/service keys. They are not project inventory.
  • <project_id> in commands = project_id from run402 projects list
  • Output: JSON stdout on success; JSON stderr on failure; exit 0 success, non-zero error. See Output Contract.
  • CLI handles x402 / MPP signing; do not request private keys or payment libraries.
  • run402 up is the only compound CLI command. It emits natural JSON with steps[] (no top-level success status). Use --check for local-only validation, --plan for gateway-reviewed intent, and --require-plan for exact reviewed apply.
  • GitHub Actions deploys use OIDC: link once with run402 ci link github; generated workflow calls run402 deploy with permissions: id-token: write.
  • Projects, sites, subdomains, forks, functions, secrets, asset storage: free with active tier. Only image generation ($0.03) is per-call
  • Env overrides: RUN402_API_BASE (overrides stored target; default https://api.run402.com), RUN402_CONFIG_DIR (base credential dir, default ~/.config/run402), RUN402_WALLET (active named wallet/profile, default default; alias RUN402_PROFILE), RUN402_WALLET_PATH (custom wallet file path, default {config_dir}/wallet.json), RUN402_GRANT_KEY (a grant-key bearer from run402 grants create --key; when set it is the only credential sent, so a process with no wallet can deploy), RUN402_AGENT_NAME (the name this agent runtime declares for itself; run402 up sets it as an agent principal’s display name, overriding an existing agent name; human/unknown principals are preserved — reported as identity.source: "explicit"; promotion credit and room presence use it; equivalent to run402 whoami --set-name <name>), RUN402_CLIENT (declares the coding-agent client when up has no environment marker for it, e.g. RUN402_CLIENT=grok; checked before every marker; it names an unnamed agent principal but never overrides an existing name — use RUN402_AGENT_NAME for that), RUN402_PROGRAM / RUN402_MODEL (room-presence program / model labels; program is otherwise inferred as claude-code, codex, cursor, or grok), RUN402_TRACE (any non-empty value: one stderr trace line per SDK request — see Observability below), RUN402_VAULT_TRACE (debug-only, 1: one stderr line per vault transport operation — see Observability below). run402 init --api-base=<url> persists the active target in {config_dir}/target.json or {config_dir}/profiles/<name>/target.json.
  • Wallets: run402 wallets manages named profiles. Select via --wallet <name> (--profile), RUN402_WALLET, or nearest .run402.json binding (commit-safe name only). Precedence: flag > env > .run402.json/.run402.local.json > wallets use default > default. Env/binding conflict hard-fails unless flag passed. default stays at config root; named wallets live under {base}/profiles/<name>/. Non-default active wallet is echoed on stderr and shown in status / wallets current.

Uniform contract:

  • Success: stdout emits the natural payload, never wrapped; no top-level status.
  • Reads/lists: resource directly, e.g. projects get -> { project_id, public_id, name, ... }, projects list -> { projects: [...], scope?, has_more?, next_cursor? }, credentials project-keys status -> local-cache provenance.
  • Mutations without natural payload: affected ids + boolean action field, e.g. { key, project_id, set: true }, { name, project_id, deleted: true }, { domain, project_id, released: true }; never {}.
  • Local-state reads (status, wallets current): nullable typed fields, e.g. { wallet: null, hint: "Run: run402 init" } or configured: false; absence exits 0.
  • Raw/text stdout is opt-in only (functions invoke --raw, file-output commands, help/version/dev human surfaces). Machine-readable command defaults emit parseable JSON; for example, wallets current emits { "local_label": "default", "address": "0x...", ... }.
  • Failure: stderr JSON envelope with top-level status: "error" + non-zero exit. That sentinel appears on stderr only.
  • Validation commands may exit 0 with payload issues, e.g. validate-expose prints has_errors: true; branch on payload fields.
  • Payload-internal status fields are not envelopes, e.g. doctor.checks[].status.
  • JSON is ALWAYS the default on stdout. --json is a universally-accepted NO-OP: every command takes it, and passing it never changes stdout. Never pass it to “get JSON” — you already have JSON. The one exception is assets put --json, a deprecated alias for --stream (NDJSON progress).
  • Human-readable rendering is an explicit opt-out, spelled --human (run402 up, run402 errors list). --human combined with --json is a BAD_USAGE error.
  • cli-output-contract.test.mjs and cli-json-noop-contract.test.mjs guard this; violations are regressions.
  • v3.0 breaking change: success wrapper { status: "ok", ...payload } removed; gate on exit code. Stderr error envelope unchanged.

The SDK’s request kernel (sdk/src/kernel.ts — the one place that touches fetch) carries two always-available diagnostics, for every SDK caller (CLI, MCP, git-remote-run402):

  • RUN402_TRACE — set it to any non-empty value and every request writes one line to stderr: r402 <METHOD> <path> -> <status> <ms>ms attempt=<n>. <path> never carries its query string, and the line never carries headers, bodies, or tokens (the same redaction posture as the payment-attempt journal). <status> is ERR for a request that never got a response (network failure).
  • Per-instance stats — every SDK instance accumulates round_trips, wire_ms (summed), bytes_up, and bytes_down (Content-Length when present, measured otherwise), monotonic for the instance’s lifetime, read via sdk.stats(). Every run402 repos <verb> result and run402 deploy’s final result carry this as a stats: { round_trips, wire_ms, bytes_up, bytes_down } block, always — no flag needed. It reflects only the calls that one command’s own SDK instance made; a helper that resolves its own SDK internally (e.g. org/wallet context resolution shared across command families) is not reflected in that command’s stats.
  • -v / --verbose — on repos <verb> and deploy, prints one extra stderr summary line with the same numbers (stats: round_trips=… wire_ms=… bytes_up=… bytes_down=…). Coexists with --human.

MCP tool output is markdown, not the stats envelope field — this is a CLI/SDK-edge feature by design.

RUN402_VAULT_TRACE — debug-only, vault-specific, and out of scope of the two kernel diagnostics above: set it to 1 and every git push/git fetch/run402 repos <verb> call against a vault writes one stderr line per vault transport OPERATION (vault-trace: <op-kind> [path=… | paths=N | objects=N | gen=… | repo=…] [bytes=N] <ms>ms), plus a session summary at process exit (op count, time spent in the vault transport, wall-clock). This is the one place presigned-object PUT/GET traffic is visible at all — RUN402_TRACE and sdk.stats() above are scoped to the request kernel’s own apiBase-relative calls, so they never see the direct-to-bucket presigned reads/writes the vault’s carrier, WAL, and checkpoint objects ride. A traced GET for one of the five edge-eligible kinds (wal_pack, ref_state, retention_roots, checkpoint_manifest, checkpoint_pack) may in event be answered by the platform’s CDN edge rather than the bucket directly when the gateway offers one for that read (vault-read-edge-cache, design D5) — automatic, verified byte-for-byte identically either way, and falling back silently to the direct presigned URL on any edge failure; the trace line reports the same op regardless of which one actually served it. Not a canonical surface, not what any round-trip budget is measured against — it is a debugging aid for understanding WHERE a slow git push/fetch spent its round trips.

run402 up [repo-or-path] [--name <name>] [--project <project_id>] [--manifest <path>] [--dir <path>] [--tier <prototype|hobby|team>] [-y|--yes] [--check|--print-spec|--print-manifest|--plan|--require-plan <plan_id>] [--no-rehearse] [--verify] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--json|--json-stream|--human] [--quiet] [--allow-warning <code> ...] [--allow-warnings]

run402 up verify [repo-or-path] [--project <project_id>] [--manifest <path>] [--dir <path>] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--json|--json-stream|--human] [--quiet]

Use up for a repo-level app deploy when the workspace has a deploy manifest. It discovers run402.json, run402.deploy.json, then app.json under --dir / cwd; when none is there it looks one directory down (immediate children, skipping node_modules and dot-directories) and reports what it finds in UP_MANIFEST_REQUIRED details.nearby_manifests[] with a leading run_in_directory next action (run402 up --check --dir <dir>) — up never walks into subdirectories on its own. --manifest <path> naming a file that does not exist fails with MANIFEST_NOT_FOUND (details.path is the resolved absolute path) and a create_manifest next action, for .json manifests, run402.json, and executable .ts/.js configs alike. A run402.json with the app schema or app-specific markers uses the app-install graph; a release-shaped run402.json uses the same ReleaseSpec normalization accepted by deploy. Malformed app manifests return APP_SPEC_INVALID with the failing field instead of an internal JavaScript exception. Validation and filesystem-reference checks finish before any mutation, then the SDK action plan executes: every local file the manifest references — migration sql_path/sql_file, function source.path and files, site { path } entries and dir() targets, assets.put[].source — is checked to exist, and a missing one fails with MANIFEST_FILE_MISSING (exit 1) whose details.missing[] lists { field_path, path, kind } (migration_sql | function_source | site_file | site_dir | asset) and whose next_actions[] carries one create_file entry per file. The same check runs in every mode, before any gateway call.

Project resolution order:

  • explicit --project
  • workspace link .run402/project.json (schema_version: "run402.workspace-project.v1", project_id, optional name, target)
  • manifest project_id
  • approved project creation from --name

Conflicting selectors fail before wallet, tier, identity, link or Git mutations. Missing intent returns UP_PROJECT_REQUIRED with one select_project action. -y approves work only after the target is known. On an interactive terminal without -y, enter an existing project ID or new <name>; there is no default project choice. Use RUN402_PROJECT_ID as an explicit CLI environment selector.

--name is only project creation/link metadata. It is not part of the deploy manifest, does not select a project when another selector already resolved one, and never renames an existing project. The workspace link is a local convenience file; it is written atomically and skipped in local check / reviewed-plan modes.

Approval and recursion:

  • Non-interactive recursive mutations require -y/--yes; without it the command fails before mutating and returns a structured approval-required error.
  • If wallet/tier/project/workspace link are already configured, plain run402 up runs the requested deploy without -y.
  • In a TTY, the CLI prompts for SDK-planned mutations. In SDK code, pass { approval: "yes" }, { approval: "never" }, or an interactive approval callback.
  • --check returns local validation steps[] without wallet creation, faucet request, tier payment, project creation, workspace-link write, upload, gateway plan, or deploy commit. It is not a file-exists check: it normalizes the manifest and verifies every referenced file (see above), so a missing sql_path, function source, or site file fails here with MANIFEST_FILE_MISSING instead of at upload. For a run402.json app manifest that declares no build.commands, the release slice’s references are verified before any build; a declared build may produce those files, so the check is deferred until after the build runs (the step says file_references: "deferred_to_post_build").
  • --print-spec performs the same local validation and prints advanced SDK-native ReleaseSpec JSON. Use --print-manifest for reloadable snake_case authoring JSON, relative to the original manifest directory. Unsupported dynamic/secret values fail explicitly. result.preflight carries nullable target selection, local evidence and gateway_validated: false; see the deploy slice for deferred checks.
  • --plan calls the gateway reviewed-plan mode without upload or commit; it does not provision projects or write workspace links. The response includes a require-able plan_id, plan_fingerprint, expiration, warnings, diff, and next_actions[].
  • --require-plan <plan_id> applies only if the reviewed plan still matches; optional --plan-fingerprint <fingerprint> tightens the check.
  • Run402 Cloud up can create/fund a local wallet, ensure a prototype tier by default, create a project from --name, write the workspace link, make sure this principal has a display name, join the project room under it, then apply the manifest.
  • Identity: result.identity.principal records authenticated principal ID/type/name; result.identity.client records detected client and declared agent name separately. Human and unknown principals are preserved even with RUN402_AGENT_NAME. For agent principals only, that explicit name wins; otherwise a detected client names an unnamed principal. Detection never overwrites an existing name. detection.reason: principal_identity_preserved explains human/unknown cases. Failed identity lookup reports unavailable and does not rename. Promotion credit uses the principal name, never room presence or client detection. An intentional principal rename uses run402 whoami --set-name <name>; up --name is project metadata.
  • Rehearsal: result.deploy.rehearsal is { status: "passed", report, … } when the plan was rehearsed on a contained branch first, or { status: "skipped", reason: "no_live_release" | "no_migrations" | "migrations_unchanged" | "ci_session_unsupported" | "disabled" | "reviewed_plan" | "unsupported" } (migrations_unchanged: every migration in the manifest is already applied with an identical checksum, so a page-only redeploy that still carries schema/seed ships in seconds instead of spending a branch; ci_session_unsupported: a GitHub Actions deploy, which commits directly because CI sessions cannot rehearse). A failed rehearsal exits non-zero with REHEARSAL_FAILED and the report; nothing is committed. --no-rehearse skips it.
  • Git: on a local directory up scaffolds git on the APP ROOT only (the manifest’s directory): git init when it is not a repository, a run402 remote (origin is never claimed), and a first vault push. A repository up just created is all-untracked by definition, so that first push captures it as a synthetic commit (first_push.captured_dirty: true with the modified_captured / untracked_captured disclosure); an existing repository keeps the clean-tree rule and a SNAPSHOT_DIRTY_TREE refusal is reported under first_push_error, never overridden. An app root that lies inside ANOTHER repository (a monorepo workspace — how most agent workspaces look) is left untouched by default: result.repo is { status: "skipped", reason: "inside_other_repository", toplevel, next_actions: [{ type: "create_nested_repo", command: "run402 up --nested" }] }, and --human prints the same line. --nested gives that app its own nested repository with the encrypted remote: git init -b main there, the run402 remote and first push from there, and exactly one line (/<relative path>/) appended to the enclosing repository’s local .git/info/exclude — nothing else in the enclosing repository is touched (no .gitignore, index, or submodule), it never shows the app as untracked noise, and the flag is a no-op when the app root is already its own repository. A nested scaffold reports result.repo.vault.nested: true, enclosing_toplevel, and excluded_in_enclosing. projects provision never touches git.
  • Run402 Core up skips Cloud wallet/tier prerequisites and fails closed if no Core project is selected by --project, workspace link, or manifest.
  • The SDK derives child idempotency keys for recursive gateway mutations from the root action key; pass --idempotency-key when you need a stable external key.
  • Deploy warnings use the same review surface as deploy: prefer repeatable --allow-warning <code> and reserve broad --allow-warnings for reviewed exceptional cases.
  • App manifests AND deploy manifests can define verify.http[]. After apply, up fetches those URLs and records per-check status in result.app_result.verification.http[] (app manifests) or result.verification.http[] + a result.verify rollup (deploy manifests; a hard verify failure exits 1). Fresh managed-subdomain or custom-domain misses that carry Run402 edge sentinels (x-run402-edge or JSON codes such as SUBDOMAIN_NOT_CONFIGURED) are treated as propagation, not as permanent verify failure, while the deploy binding is fresh or deploy resolve reports edge_propagation.status !== "settled".
  • --propagation-budget-s controls the wall-clock wait for those fresh edge misses (default 120 seconds). --no-propagation-wait returns immediately with app status: "propagation_pending" and verify.status: "propagation_pending"; the result includes propagation_wait_ms, warnings, next_action, and diagnostic edge_propagation / resolve payloads when available.
  • --verify waits after a successful deploy for gateway/edge release coherence and attaches result.edge_coherence plus result.deploy.edge_coherence. It uses the same --propagation-budget-s budget (default 120 seconds), emits deploy.verify.poll progress events only in explicit stream/human mode, and exits 2 if the report is valid but still not coherent.
  • run402 up verify reruns the manifest HTTP verification (app or deploy manifest) without resource mutation, upload, deploy, or project creation. It resolves the project from --project, .run402/project.json, the manifest project id, then active project, and is the recovery command to run after propagation settles. A manifest without verify.http[] fails VERIFY_CHECKS_REQUIRED.

Output: stdout is the action result, e.g. { "action": "up", "dry_run": false, "target": "cloud", "steps": [...], "result": { "project_id": "prj_...", "manifest_path": "...", "deploy": {...} } }. Stderr carries JSON action-step events unless --quiet.

Protecting a live database (rehearsal is automatic)

Section titled “Protecting a live database (rehearsal is automatic)”

When a plan carries migrations and the target project already has a live release, run402 up and run402 deploy rehearse it before committing: a snapshot of the project becomes a contained branch, the candidate plan is applied there, built-in checks run (static 200s, function invocability, migration registry) plus any checks the spec declares, and only a passed report is committed — bound to the report’s required_plan, so the plan that was rehearsed is the plan that ships. A first deploy has no live release to protect, so nothing is rehearsed and the result says rehearsal: { status: "skipped", reason: "no_live_release" }. A redeploy whose migrations are all checksum-identical noops (the plan’s migrations.new is empty) has nothing to rehearse either: rehearsal: { status: "skipped", reason: "migrations_unchanged" } — keep schema/seed in the manifest and iterate on pages in seconds. --no-rehearse opts out for speed.

The gateway decides, in the plan response: rehearsal: { available, rehearse_url, reason: null | "no_migrations" | "no_live_release" | "migrations_unchanged" | "ci_session_unsupported", next_actions? }; a CI (GitHub OIDC) session always gets available: false with ci_session_unsupported and commits directly. The SDK honours that verdict and, against an older gateway that still advertises rehearsal for an all-noop plan, derives migrations_unchanged from the plan’s own migrations: { new: [], noop: [...] } buckets — the only client-side re-derivation, and it can only ever skip work.

Primitives, for when you want the pieces: run402 deploy rehearse <plan_id> rehearses an already-persisted plan without committing (a project with no live release rehearses on an EMPTY branch — never a refusal); run402 snapshots … are manual restore points; run402 branches … are contained, expiring data copies.

Runtime config on every host (/_run402/config.js)

Section titled “Runtime config on every host (/_run402/config.js)”

Every host that serves a project’s site — managed subdomain, branch’s host, custom domain — answers GET /_run402/config.js with window.RUN402 = { project_id, api_base, anon_key }; and GET /_run402/config.json with the same object, for the project that host resolves to at request time. A branch’s host answers for the branch; a transferred project answers with its rotated anon key; the HTML never changes. Cache-Control: public, max-age=60. The service key is never served.

<script src="/_run402/config.js"></script>
<script>
const { api_base, anon_key } = window.RUN402;
fetch(`${api_base}/rest/v1/items`, { headers: { apikey: anon_key } });
</script>

/_run402/ is reserved: a route pattern, static alias, public path, or site file under it is rejected at plan time.

CLI errors: JSON stderr with outer "status": "error". Run402 JSON bodies may merge into the envelope. Branch on code, not message/legacy error.

Canonical fields:

  • code: stable machine-readable reason, e.g. PROJECT_FROZEN, PAYMENT_REQUIRED, MIGRATION_FAILED, MIGRATE_GATE_ACTIVE. Client-side validation failures (missing flag, malformed JSON) default to BAD_USAGE; specific client-side cases use richer codes e.g. UNKNOWN_FLAG, BAD_FLAG, PROJECT_CREDENTIAL_NOT_FOUND (with details.source: "local_cache"), NO_DEPLOYMENT, NO_WALLET, BAD_JSON_FLAG, CONFIRMATION_REQUIRED.
  • 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 this when reporting the issue
  • request_id: routed/function handle; diagnose with run402 functions logs <project_id> <name> --request-id <req_...>. Distinct from gateway trace_id.
  • details: structured route-specific context
  • next_actions: advisory typed suggestions e.g. authenticate, submit_payment, renew_tier, check_usage, retry, resume_deploy, edit_request, edit_migration, create_project, initialize_wallet, deploy, deploy_site_first, or poll. CLI-resolvable entries carry a literal command, e.g. { "type": "create_project", "command": "run402 projects provision" }. Do not execute route-like suggestions without validating method/path/auth/safety.
  • correlated_platform_incident: present ONLY while an OPEN platform incident correlates with this error’s code — { id: "inc_…", subsystem, status: "ongoing" | "resolved" }, with a poll entry appended to next_actions. It is a CORRELATION, not an exoneration: the platform states it was degraded when your call failed and lets you judge (an app can still cause its own throttling). Poll the events feed (run402 events list) and check platform_status before debugging your own code; when the incident resolves, the matching platform_incident feed event carries your project’s real failed-invocation count. Absent when no open incident correlates — never a false confession.
  • Cold-start chain: a fresh agent that knows only run402 deploy is walked to a deployed result by following next_actions — no local wallet -> run402 init, no tier -> run402 tier set prototype, no project -> run402 projects provision — each step idempotent, then retry the deploy. You do not need to memorize the sequence; follow what each failure hands back.
  • Prefer run402 up when starting from a local repo: it plans and runs that same cold-start chain through the SDK instead of executing advisory next_actions[].command strings.

Retry policy:

  • Retry same request only when retryable: true and safe_to_retry: true; reuse idempotency key for mutations when available.
  • safe_to_retry: true alone means duplicate-safe, not likely-to-succeed. Lifecycle-gated writes, auth token exchanges, and passkey verifies need the indicated action first.
  • run402 deploy already handles safe BASE_RELEASE_CONFLICT release races for omitted/current-base deploy specs, and the transient OPERATION_INTERRUPTED / IMAGE_ENCODE_TIMEOUT / TOO_MANY_ENCODES_QUEUED / EXPORT_CONSISTENCY_UNAVAILABLE codes the gateway marks safe_to_retry: true: it re-plans, emits deploy.retry events on stderr, and stops after its bounded SDK retry budget. Exhausted deploy retries include attempts, max_retries, and last_retry_code in the error envelope. Do not hand-roll this specific retry loop around the CLI unless you intentionally disabled SDK retries upstream.
  • For mutating 5xx with safe_to_retry: false, or mutation_state in committed|partial|unknown, inspect/poll/reconcile before retry. For deploys prefer deploy events/deploy resume over duplicate apply.
  • Lifecycle/payment: PROJECT_FROZEN/PROJECT_DORMANT/PROJECT_PAST_DUE -> projects usage <project_id> or tier set <tier>; PAYMENT_REQUIRED/INSUFFICIENT_FUNDS -> top up the allowance, redeem a voucher, or fund the wallet.
  • NOT_AUTHORIZED (HTTP 403) is an org-owned-control-plane authorization denial, distinct from auth or payment: the wallet authenticated, but its resolved principal lacks the org role or per-project grant the action needs. details carries required_role / required_capability / reason. Not retryable without obtaining a covering org membership/role or grant; high-stakes ops (delete, transfer-of-ownership, membership change) require an active owner membership. The gateway returns 403 even when the project does not exist (so existence isn’t leaked) — re-check the id too. The CLI envelope adds an actionable hint.
  • STEP_UP_REQUIRED (HTTP 403) is a freshness/provenance demand for a high-stakes control-plane op: the session is valid but not fresh enough, or was minted by a read/device-flow path that can’t satisfy a passkey step-up. details carries required_amr / max_age_seconds / challenge_url / reason, plus next_actions[]. The SDK raises a typed StepUpRequiredError (isStepUpRequired() guard). Resolve by signing in again with run402 login (a fresh passkey) on the same client, then retry. A run402 login --device session and a write approval never satisfy it. Distinct from NOT_AUTHORIZED (a role/grant gap, not a freshness gap).
  • WRITE_APPROVAL_REQUIRED / WRITE_APPROVAL_SCOPE_MISMATCH / WRITE_APPROVAL_BINDING_MISMATCH (HTTP 403) and WRITE_APPROVAL_SESSION_INVALID (HTTP 401) — a person signed in without a wallet needs a passkey write approval scoped to this (action, target) (the SIWX wallet path never hits this). The SDK raises a typed WriteApprovalRequiredError (isWriteApprovalRequired() guard) carrying capability, target, and a fully-resolved approveCommand / approve_write next action (e.g. run402 approve --action project.deploy --project prj_x). SCOPE_MISMATCH = the attached approval covers another action or target; BINDING_MISMATCH = it belongs to another sign-in session; SESSION_INVALID = it expired or was revoked. Resolve by running the surfaced run402 approve command (or let an interactive provision/deploy auto-approve).
  • SESSION_READ_ONLY (HTTP 403) — the sign-in session came from run402 login --device and can only read. Run run402 login (the browser passkey sign-in) to write.
  • Client-side BAD_JSON_FLAG errors include details.flag (the offending flag, e.g. --abi) and details.value_preview (truncated value) so callers know which flag value to fix.
  • CLI commands reject unknown flags and missing flag values locally with UNKNOWN_FLAG or BAD_FLAG before network work. Numeric and wei-like flags are strict decimal integers: malformed, fractional, negative, and scientific-notation values fail locally instead of being forwarded to the API.
  • Commands with fixed positional shapes also reject extra positional arguments locally. This includes deploy resume/list/events/release subcommands, functions list/delete, and blob get/rm/sign/diagnose.

Examples:

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

Terminal window
npm install -g run402
Terminal window
run402 init # creates the wallet if absent; checks balance; faucets testnet USDC if zero; shows tier/projects
run402 wallets current # The active wallet: address, rail, file path, faucet use
run402 wallets fund # Get free testnet USDC (Base Sepolia)
run402 wallets balance # On-chain USDC (mainnet + testnet) plus the organization's allowance

The wallet lives at ~/.config/run402/wallet.json (0600). CLI signs x402 / MPP payments automatically; never handle private keys/payment libs manually.

For a self-hosted Run402 Core Gateway, skip Cloud wallet setup and configure the target instead:

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

After that, the same run402 projects provision and run402 deploy commands target Core.

If you were given a promo code — a string like R402-K8F3-Q2W9, sometimes called a voucher or a gift/starter code — redeem it. It adds to your organization’s allowance, and the allowance pays for the next step (a tier) with no on-chain payment.

Terminal window
run402 redeem R402-K8F3-Q2W9 # any time: before or after `run402 init`
run402 init --voucher R402-K8F3-Q2W9 # same redemption, folded into setup
  • Order does not matter. Redeeming works as your very first authenticated call (the organization is created on demand) or long after run402 init.
  • Codes are forgiving. Case-insensitive, hyphens optional — R402-K8F3-Q2W9 and r402k8f3q2w9 are the same code. Send it verbatim; the server normalizes.
  • Retrying is safe. A repeat by the same organization returns the original result with already_redeemed: true and never adds the amount twice.
  • A bad code never blocks setup. init --voucher warns, records voucher_error in its JSON summary, and finishes normally.
  • Failures: 404 VOUCHER_NOT_FOUND (unknown or malformed), 410 VOUCHER_EXPIRED, 409 VOUCHER_ALREADY_REDEEMED (a different organization used it), 403 PROMO_LIMIT_REACHED (this org is at its lifetime ceiling for that code’s issuer; a code from another issuer has its own).

Minting codes is not an agent operation — it needs an issuer key no tenant holds.

Terminal window
run402 tier set prototype # FREE on testnet — faucet USDC verifies your x402 setup ($0 real money); free tier, no lease
run402 tier set hobby # $5 for 30 days (real money)
run402 tier set team # $20 for 30 days (real money)

The allowance pays first. A promo code (run402 redeem <code>) or a top-up adds to the organization’s allowance, and tier set settles from it ahead of the payment paywall: no 402, no signed authorization, no USDC in the wallet, receipt paid_with: "allowance" with allowance_used_usd_micros and allowance_remaining_usd_micros. Only an allowance that falls short goes to x402 / MPP, and that X402_INSUFFICIENT_FUNDS error carries details.allowance (allowance_usd_micros, price_usd_micros, shortfall_usd_micros) with redeem_voucher / top_up next actions.

Tier is organization-scoped. A start/renew/upgrade applies to every project in the org; api_calls / storage_bytes quota is org-pooled across linked wallets (billing link-wallet). Quota errors include details.scope: "organization" | "project" (project = orphan fallback after org purge before cascade). tier set refetches status and returns status_after with refreshed pool usage.

Retry-safety: tier set and projects provision accept --idempotency-key <key> so a retried start/renew/create collapses onto one charge instead of double-billing. provision auto-derives the key from --name when omitted (re-running provision --name X returns the same project); tier set is caller-supplied only — use a fresh key for a deliberate second renewal.

Server action detection (reported as action: start | renew | upgrade):

  • No tier or expired -> start
  • Same tier, active -> renew (extends from current expiry)
  • Higher tier -> upgrade (prorated refund to the allowance)
  • Lower tier, active -> downgrade (prorated refund if usage fits)
Terminal window
run402 tier status

tier status pool_usage sums api_calls and storage_bytes across every project on the organization (across every linked wallet), not the requesting wallet’s projects.

With active tier: unlimited projects/sites/forks/functions/secrets/storage subject to org-pooled api_calls/storage_bytes; only image generation is per-call ($0.03/image).