Skip to content

Core commands

  • 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>] [--verify] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--quiet] — SDK-owned recursive app deploy. Validates run402.deploy.json/app.json, requires explicit --manifest for executable .ts/.js configs, ensures missing Cloud prerequisites when approved, resolves/creates/links a project, then applies the manifest. Output includes steps[]; success has no top-level status. Use --check for local-only validation, --print-spec for normalized ReleaseSpec, --plan for a gateway-reviewed non-deploying plan, and --require-plan for exact reviewed apply. App HTTP verification reports fresh edge misses as propagation_pending, waits up to --propagation-budget-s (default 120), and --no-propagation-wait returns the pending state immediately. Add --verify to wait for gateway/edge release coherence after the deploy and attach edge_coherence; non-coherence exits 2.
  • run402 up verify [repo-or-path] [--project <project_id>] [--manifest <path>] [--dir <path>] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--quiet] — rerun manifest HTTP verification (app manifest verify.http[] or deploy-manifest top-level verify) without deploying, uploading, creating a project, or mutating resources. Use it after propagation_pending or before declaring a consumer copy healthy.
  • run402 init — set up with x402 (Base Sepolia). Creates the local wallet, requests faucet, checks tier, lists projects. A blocked or unconfirmed faucet is reported in funding; next actions carry the server cooldown or balance-check recovery instead of recommending immediate deploy. Identity setup can succeed while funding remains blocked.
  • run402 init --api-base <url> — configure the active profile to target a Run402 Core/API base. For Core, this does not create a wallet, request faucet funds, or require a Cloud tier.
  • run402 init mpp — set up with MPP: pathUSD on Tempo, faucet-funded on the Moderato testnet. Same steps, different payment rail; a purchase pays the first Tempo chain its 402 offers that the wallet can cover.
  • run402 init lightning — the Lightning wallet: the same Base wallet (the x402 fallback) plus a budgeted Lightning wallet the platform mints on Run402’s own Hub (POST /agent/v1/lightning-wallet; custody run402_hub, a starter amount funded by the platform). The one-time pairing is stored in wallet.json beside the Base key and never printed; rail becomes lightning, so tier purchases and image generation answer a Lightning challenge first and fall back to x402. Image generation pays the x402 exact scheme on Lightning when Run402 offers it, and MPP Lightning otherwise. The summary carries lightning: { wallet_id, status, lightning_address, budget_sats, starter_sats, balance_sats, custody, outcome }; outcome: "minting" means rerun in a few seconds, "unavailable" means no Hub on this gateway (x402 keeps paying).
  • run402 init <rail> --switch-rail — switch the persisted rail; re-running with the same rail is idempotent.

run402 pay <url> [--method <M>] [--body <json-or-text>] [--max-usd <amount>] [--max-sats <n>] [--lightning-pairing <file|->] [--idempotency-key <key>] [--require-receipt] calls an arbitrary x402- or MPP-priced HTTP endpoint through the SDK buyer. On the mpp rail a 402 carrying an MPP Tempo charge (WWW-Authenticate: Payment) is paid in pathUSD on the first offered Tempo chain the wallet covers, bounded by --max-usd before signing; the printed payment carries protocol: "mpp", method: "tempo", the eip155:4217 / eip155:42431 network, the Tempo transaction hash, and evidence.protocol_receipt (the seller’s Payment-Receipt), with offer.status and merchant_receipt.status "unavailable". Over MPP --idempotency-key is correlation only: repeat the identical command from the same client to resolve an unknown outcome, which re-presents the same credential and is never charged twice. A 402 that offers nothing the wallet’s rail pays fails PAYMENT_NETWORK_UNSUPPORTED before signing with details.offers[] and a run402 init <rail> --switch-rail next action naming the rail that pays it. On the lightning rail a 402 that names an lnbtc entry on the wallet’s network is paid in sats from the agent’s Lightning wallet, bounded by --max-sats (invoice plus routing headroom) or the rail’s ceiling; an lnbtc entry on another network is refused with LIGHTNING_NETWORK_MISMATCH and nothing is paid. --lightning-pairing <file|-> pays with a Nostr Wallet Connect pairing read from a file or stdin (never the command line, never printed) on whatever network its node is on, which is how a testnet desk or an operator’s own node is paid. A host’s priced routes, with the methods that buy each and the networks it takes (eip155:…, lnbtc:…), are listed at https://<host>/.well-known/x402, with mppResources and paymentCapabilities for the routes that also take an MPP Tempo charge; a route priced on Lightning or Tempo alone is listed only there, never in the x402 Bazaar. --require-receipt cannot be satisfied by a Lightning entry (LIGHTNING_RECEIPT_UNAVAILABLE). The request body goes in --body and nowhere else: --json is the CLI-wide output-format flag, takes no value, and is a no-op here because pay always prints JSON. Writing --json '<payload>' does not send the payload — it fails BAD_USAGE with a hint naming --body. The default ceiling is $0.10; --max-usd accepts up to six decimal places and is converted exactly to USD micros. --require-receipt requires a verified wallet-rooted offer before payment and a matching receipt afterward. JSON output is the complete x402-commerce-result.v1 envelope with settlement, movement/replay, delivery, offer, merchant-receipt, signer-relationship, policy, and portable evidence. Unpriced URLs return payment: null. A seller that settles and then answers a non-2xx with a valid PAYMENT-RESPONSE receipt prints a normal result: http_status is the seller’s status, body is the seller’s body, and payment.funds_moved: true with payment.delivery.status: "failed". For PAYMENT_INTENT_PENDING on a trusted Run402 host, wait for Retry-After and repeat the identical command with the same payer, request, and key. Never substitute a fresh key. Custom/arbitrary hosts and other funds_moved: "unknown" outcomes remain ambiguous and require reconciliation.

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

A route priced in sats, paid with a pairing on its network (the pairing on stdin):

Terminal window
run402 pay https://kychange.com/api/usdc/10k --method POST \
--body '{"destination":"0x0000000000000000000000000000000000000001"}' \
--max-sats 10500 --lightning-pairing - < ./payer.nwc

run402 status — show full organization state in one shot (wallet, rail, balances, tier, projects, active project). Read-only, JSON output. Includes a wallet: { local_label, server_label, address } object naming the active named wallet (local_label is the local selector, server_label the server-synced display name or null), a top-level rail, and a balances: { on_chain_usd_micros, on_chain_by_network, on_chain_token, allowance_usd_micros, held_usd_micros } object: on_chain_usd_micros is the wallet’s on-chain balance summed over the networks its rail pays on, on_chain_by_network each of those networks apart (base_mainnet_usd_micros and base_sepolia_usd_micros on x402, tempo_mainnet_pathusd_micros and tempo_moderato_pathusd_micros on mpp; null where a read failed), and allowance_usd_micros the organization’s allowance. The on-chain token tracks the rail (USDC on x402, pathUSD on mpp); the allowance is rail-independent. Remote reads carry remote_status entries for tier, billing and projects (available or unavailable, with safe error code/category). projects_source distinguishes server inventory from local_cache. Null remote values after failed reads are unknown, not evidence that initialization or funding is needed.

Manage multiple named wallets (profiles) on one machine. Keys never leave the machine (non-custodial). The default wallet lives at the config-dir root; named wallets live under {config_dir}/profiles/<name>/.

  • run402 wallets list — JSON array of { local_label, server_label, address, address_short, rail, active }. Reads non-secret meta.json; never loads private keys.
  • run402 wallets current — the resolved active wallet { local_label, source, source_detail, address, server_label, configured, created, rail, faucet_used, path, warnings }; with no local wallet yet, configured: false and an initialize_wallet next action. faucet_used records faucet use, not pay-readiness. source ∈ flag|env|binding|config|default. warnings surfaces env-vs-binding conflicts and local-name-vs-server-label drift.
  • run402 wallets new <name> [--mpp | --rail <x402|mpp|lightning>] — create a new wallet (generates a key); wallets new default creates the root wallet without funding it. { local_label, address, rail, created: true, next_actions }. A lightning wallet is minted on the platform by run402 --wallet <name> init lightning.
  • run402 wallets fund — request testnet funds from the faucet into the active wallet (Base Sepolia USDC, or Tempo pathUSD on the mpp rail) and wait briefly for them to land.
  • run402 wallets balance — the active wallet’s on-chain balances (onchain: base_mainnet_usd_micros, base_sepolia_usd_micros, tempo_mainnet_pathusd_micros, tempo_moderato_pathusd_micros; null where a read failed) plus its organization’s allowance_usd_micros (null with no organization).
  • run402 wallets lightning [status|mint|revoke] — the active wallet’s Lightning wallet. status reads the platform record plus a best-effort balance and remaining budget over NWC ({ rail, lightning: { wallet_id, status, custody, lightning_address, budget_sats, starter_sats, has_pairing, balance_sats, budget_remaining_sats } }); mint is init lightning’s wallet step alone; revoke deletes the sub-wallet on Run402’s Hub, forgets the pairing, and returns the rail to x402. The pairing secret is never printed.
  • run402 wallets use <name> — set the global default wallet (config.json active_wallet). { name, active: true }.
  • run402 wallets rename <old> <new> — rename a wallet; renaming default migrates its root files into profiles/<new>/. { from, to, renamed: true }.
  • run402 wallets bind [<name>] — write ./.run402.json binding this directory to a wallet (defaults to the active one). Safe to commit (holds only a name). { wallet, file, bound: true, safe_to_commit: true }.
  • run402 wallets unbind — remove ./.run402.json. { file, unbound }.
  • run402 wallets import <name> --key <path|-> — adopt an existing 0x-prefixed 64-hex private key (file path or - for stdin) as a named wallet. { local_label, address, imported: true, signer: "local" }.
  • run402 wallets import <name> --kms <key-arn> [--aws-profile <profile>] — adopt an AWS KMS key (ECC_SECG_P256K1, SIGN_VERIFY) by reference: reads its public key, derives the address, and writes a profile that holds the ARN and no secret. Every command then signs through KMS under --wallet <name> (SIWX, x402, MPP on Tempo, transactions); the key never leaves KMS. { local_label, address, imported: true, signer: "aws_kms", key_arn }. Errors: KMS_KEY_UNSUITABLE (wrong key type), KMS_ACCESS_DENIED (the AWS identity is not on the key policy; names the caller), KMS_SIGNER_UNAVAILABLE (expired session: aws sso login --profile <p>), SIGNER_ADDRESS_MISMATCH (the key at the ARN is not the imported one).
  • run402 wallets send <amount> --to <address> [--from <smart-account>] [--network base|base-sepolia|tempo|tempo-moderato] [--confirm] — send USDC (Base) or pathUSD (Tempo; the fee is paid in pathUSD too) from the active wallet, key-backed or KMS-backed. --from sends from a CoinbaseSmartWallet the wallet owns (Base only; refused with SMART_ACCOUNT_NOT_OWNER otherwise). Without --confirm it runs every check and prints a preview ({ preview: true, network, asset, amount_usd_micros, from, to, sender, balance_usd_micros, estimated_gas, hint }) without signing; with it, it reports { tx_hash, from, to, amount_usd_micros, network, asset, explorer_url } once the transaction is mined, or SEND_REVERTED with the hash. A short balance is SEND_INSUFFICIENT_BALANCE naming the asset and shortfall. The MCP sandbox refuses it (VALUE_TRANSFER_REQUIRES_CLI).
  • run402 wallets rm <name> --yes — delete a wallet and its keys. Requires --yes (agent-first: no interactive prompt). Refuses to remove default. Removing a KMS-backed wallet deletes the local profile only; the KMS key is untouched. { name, removed: true }.
  • Server-side display label: new/rename/import push the wallet’s name to a server-side label (signed by the wallet — proof of control) so the same name shows cross-machine and in the console (WEB). Best-effort and on by default; RUN402_WALLET_LABEL_SYNC=0 opts out (fully offline wallet ops). The local folder name is the source of truth; the label is a mirror, and wallets current flags any drift.
  • Selection for ANY command: --wallet <name> (alias --profile) > RUN402_WALLET > nearest ./.run402.json/.run402.local.json > wallets use default > default. A conflicting env + binding errors with WALLET_SELECTION_CONFLICT (resolve via --wallet, unset RUN402_WALLET, or wallets unbind). Selecting a non-existent wallet errors with WALLET_NOT_FOUND.

Tier and quotas are per organization (not per project) — set is organization-wide, status.pool_usage is the pooled total across every project in the organization. set refetches status after the call and includes it as status_after.

  • run402 tier status
  • run402 tier set <prototype|hobby|team>

Two surfaces under one command. credentials <verb> acts on PROJECT CREDENTIALS (rows on the gateway); credentials project-keys <verb> acts on the LOCAL key cache on this machine. A project credential (r402_…) is named, listable, expiring and individually revocable, and several may be live per kind at once — that overlap is how you rotate with no downtime. It replaces the derived anon_key/service_key, which come from the platform signing key, never expire, and cannot be revoked one at a time.

Secrets are returned EXACTLY ONCE, from issue, rotate and token. Full JSON goes to stdout so it can be piped (| jq -r .secret); warnings go to stderr. Never write one of these responses to a result cache or tmp file.

  • run402 credentials status [--project <project_id>] — am I still on the retiring key? Returns state: "legacy"|"rotatable", rotatable_credentials, credentials[], and retirement.gated_on[]. retirement.deadline is ALWAYS null and that is deliberate: retirement is gated on conditions (every project migrated, 30 consecutive days of zero legacy-key use, explicit owner approval), never a date — do not plan against one. Needs only project.read, so automation can check its own posture.
  • run402 credentials issue --kind <anon|service> --name <name> [--project <project_id>] [--expires <iso8601>] [--import] — mint one; secret printed ONCE. --name must be unique among LIVE credentials; re-using a live name returns 409 CREDENTIAL_NAME_TAKEN, and that collision IS the idempotency story (a retried create never silently mints a second credential). --expires must be in the future and within one year. --import additionally writes the minted secret into this machine’s local key cache (what deploys and data-plane commands read) — the cold-restart re-key path in one command per kind instead of issue-then-project-keys import. A first --kind anon --import on a machine with no cached entry still needs a service key first (the same rule as project-keys import), and that refusal comes BEFORE the mint, so a show-once secret is never burned on a usage error.
  • run402 credentials list [--project <project_id>] [--include-revoked] — metadata only; never a secret or a secret hash.
  • run402 credentials rotate <credential_id> [--project <project_id>] — mint a replacement and revoke the old one in one transaction, keeping the name and recording replacement_of. New secret printed ONCE. For a rotation with NO downtime window, prefer issuing a second credential, deploying it, then revoking the first; use rotate when the old secret is already compromised.
  • run402 credentials revoke <credential_id> [--project <project_id>] [--reason <text>] — immediate, and frees the name for reuse.
  • run402 credentials token [--project <project_id>] [--kind <anon|service>] — mint a SHORT-LIVED token (defaults to service). This is the cold-restart recovery path and the ONE credential call an agent can make with no human present: a grant key is accepted. No step-up, because there is nobody to prompt; what it returns expires, so it cannot become a durable root.

Authority split: issue/rotate/revoke require owner membership on the project’s owning org PLUS a fresh step-up, and a grant key can NEVER satisfy them — otherwise a scoped agent credential could escalate itself into a permanent root. Authenticate with a wallet (SIWX) or a sign-in session (run402 login). token is the deliberate exception.

  • run402 credentials project-keys list — LOCAL CACHE read. Lists cached project-key entries with source: "local_cache", cache_path, wallet/profile, key presence, prefixes, fingerprints, and timestamps. Never prints full keys.
  • run402 credentials project-keys status --project <project_id> — LOCAL CACHE read for one project id. configured: false means this selected wallet/profile lacks cached keys; it does not mean the server project is missing.
  • run402 credentials project-keys import --project <project_id> --service-key-stdin — import a service key from stdin. Optional anon key comes from --anon-key-env <env>.
  • run402 credentials project-keys import --project <project_id> --service-key-env <env> — import a service key from an environment variable. Do not pass service keys as argv values.
  • run402 credentials project-keys import --project <project_id> --anon-key-env <env> — anon-only rotation. Import writes the whole cache entry, so the FIRST import for a project must supply a service key; once an entry exists, --anon-key-env alone rotates the anon key and keeps the cached service key. Rotating the anon key therefore never requires exporting the service key with --reveal and passing it back through a shell. Passing --anon-key-env with no cached service key fails with BAD_USAGE naming that flag.
  • run402 credentials project-keys export --project <project_id> --reveal — print cached secret key material. Requires --reveal.
  • run402 credentials project-keys delete --project <project_id> — delete one local cache entry without deleting or changing the server project.
  • run402 projects quote
  • run402 projects list [--org <org_id>] [--all] — SERVER read of the named, domain-aware inventory (NOT the local project-key cache). Membership-scoped by default: every project owned by an org your wallet is an active member of, each row { project_id, name, site_url, custom_domains, org_id, status, active } (active from local state). --org <org_id> filters to one org (authorize-before-reveal: non-member/guessed id -> 403, non-UUID -> 400). --all reads every project you can reach across all your orgs (GET /agent/v1/me/projects) — as your sign-in session after run402 login, else as the current wallet — and echoes scope. Bare run402 projects list is the cold-start path (no login needed). Tier/lifecycle live on the organization — use run402 status / run402 tier status.
  • run402 projects rename <project_id> --name <label> — rename a project (fix an auto-generated name). Needs org admin+ (or a project:write grant) on the owning org; authorize-before-reveal (unauthorized/guessed id -> 403, never a not-found oracle). Works even if the project isn’t in the local project-key cache. Server-validated name (1-200 chars, no control characters).
  • run402 projects provision [--name <name>] [--org <org_id>] — --org provisions into an EXISTING org (you need developer+ on it); omit for the cold-start path (the wallet’s organization). Tier is org-governed — a client-supplied --tier is ignored when targeting an org. Against a configured Core target, creates a local Core project without Cloud payment and saves it as the active project.
  • run402 projects use <project_id> — SERVER validation, then stores only the active project id in profile state. It does not require a matching local project-key cache entry.
  • run402 projects current — LOCAL STATE read of the active project pointer with profile-state provenance and validation status.
  • run402 projects get <project_id> — SERVER read of one project’s authoritative view: { project_id, public_id, name, org_id, tier, effective_status, organization_lifecycle_state, site_url, custom_domains[], last_deploy, mailbox[], usage{api_calls, storage_bytes, api_calls_limit, storage_bytes_limit, storage_limit}, created_at }. Caller-authed (SIWX/control-plane, no project keys) and works even if the project isn’t in the local project-key cache; authorize-before-reveal (unauthorized/guessed id -> 403, never a not-found oracle). Returns NO keys — use run402 credentials project-keys ... for local credential-cache inspection/export.
  • run402 projects sql <project_id> "<sql>" [--file <path>] [--params '<json_array>'] [--read-only] — POST /projects/v1/:project_id/sql with the cached service key, or, with no cached key, as you (an org member with the viewer role reads, read-only and enforced by PostgreSQL; developer and above can write; --read-only forces read-only for any caller); prints { rows, row_count, fields, statements, warnings }. warnings[] never changes the result: SCHEMA_CHANGE_OUTSIDE_MIGRATION means a schema change on a project with a live release belongs in spec.database.migrations (it will be refused outside migrations in a later release); MULTI_STATEMENT_TEXT_BODY points at --batch.
  • run402 projects sql <project_id> --batch <file.json> [--transaction all|each] — runs a JSON array of { "sql", "params"? } statements (one statement each, up to 100). all (default) rolls back every statement on the first failure (SQL_BATCH_STATEMENT_FAILED, details.index); each commits each on its own, prints every results[i].status, and exits 1 when any failed.
  • run402 projects rest <project_id> <table> "<query>"
  • run402 projects costs <project_id> [--window <24h|7d|30d|90d>] — staff-only per-project finance: revenue, direct cost, margin, and cost breakdown. Uses the configured wallet for admin auth; RUN402_ADMIN_COOKIE='run402_admin=...' is an optional browser-session override.
  • run402 projects promote-user <project_id> <email> — promote a user to project_admin role
  • run402 projects demote-user <project_id> <email> — demote a user from project_admin role
  • run402 projects <usage|schema> <project_id>
  • run402 projects delete <project_id> --confirm — cascade deletes all project resources: Lambda functions, subdomains, S3 site files, releases, secrets, and published releases. The schema slot is dropped and recreated. This is irreversible. --confirm is required.
  • run402 projects validate-expose [id] <manifest_json> — validate an auth/expose manifest without applying it
  • run402 projects validate-expose [id] --file manifest.json [--migration-file setup.sql] — validate file input with optional migration-reference SQL
  • run402 projects apply-expose <project_id> <manifest_json> — apply a declarative authorization manifest
  • run402 projects apply-expose <project_id> --file manifest.json — apply from a JSON file
  • run402 projects get-expose <project_id> — print the current manifest (source: applied | introspected)

Provisioning automatically sets the new project as the active project. Other commands that take <project_id> default to the active project when omitted.

SQL supports DDL + queries, returns JSON. REST uses PostgREST syntax (select=, eq., order=, limit=).

User auth: password + Google OAuth. See “User Auth” section below.

  • run402 admin lease-perpetual <org_id> --enable | --disable — toggle the organization-level escape hatch. When enabled, the organization never advances past active regardless of lease expiry; every project on the organization is pinned. Enabling on a grace-state organization (past_due / frozen / dormant) reactivates inline (reactivated: true in the response).
  • run402 admin archive <project_id> [--reason "..."] — staff moderation. Sets projects.archived_at = NOW() on a single project; sibling projects on the same organization keep serving. No-op when already archived (returns note: "already archived").
  • run402 admin reactivate <project_id> — un-archive a project (flips archived_at back to NULL). It does not touch organization-level lifecycle. To reactivate a grace-state organization, run run402 tier set <tier> (the tier flow runs the lifecycle advance inline) or enable run402 admin lease-perpetual <org_id> --enable.

All admin subcommands require a staff wallet (or a staff OAuth session). Project owners with a non-admin wallet receive 403 admin_required.

  • run402 deploy rehearse [<plan_id>] [--manifest <path>] [--project <project_id>] [--teardown keep|on_pass|always] [--json] — ADVANCED: rehearse without committing. With no <plan_id> it discovers the manifest in the current directory the way up does (or takes --manifest), creates a reviewed plan, uploads its bytes, and rehearses it. With a <plan_id> whose bytes were never uploaded (a --plan-only plan), the gateway answers 409 REHEARSAL_CONTENT_MISSING and the CLI uses the manifest to upload and retry; if the project changed since that plan a fresh plan is rehearsed and the result says so under replanned. Source project data and the original plan stay untouched; a project with no live release rehearses on an empty branch. The result names the plan rehearsed and its bound commit command.
  • run402 snapshots create [project_id] [--label <text>] [--metadata <json>] [--json] — capture a manual internal restore point. --label (1–120 characters) and --metadata (a flat JSON object, up to 4 KB) are stored with the snapshot, outside the project’s database, so they survive restores; never put secrets in metadata. Every snapshot reports label, metadata, created_by (credential_kind, principal_id), and restore_of (set on the pre_restore snapshot a restore takes).
  • run402 snapshots list [project_id] [--kind manual|pre_migration|pre_restore|scheduled] [--limit <n>] [--after <cursor>] [--json] — keyset-paginated snapshot list.
  • run402 snapshots get [project_id] <snapshot_id> [--json] — inspect one snapshot and its next actions.
  • run402 snapshots restore [project_id] <snapshot_id> [--release keep|snapshot] [--include-auth] [--json] — plan a restore and print restore_plan.confirm.token plus a confirm_command; no mutation. --release snapshot also puts back the release that was live when the snapshot was taken (live release pointer, static site, routes, subdomains) in the same transaction as the data; restore_plan.release says whether that release is restorable, lists in functions_to_redeploy the functions it re-deploys to their capture-time code right after the flip, and names in warnings (FUNCTION_VERSION_MISMATCH) any function whose capture-time source is no longer stored (it keeps its current code).
  • run402 snapshots restore [project_id] <snapshot_id> --confirm <token> [--release keep|snapshot] [--include-auth] [--json] — execute the atomic offline-materialize-then-flip restore and wait for its result (it runs in the background on the gateway; the CLI polls). Pass the same --release and --include-auth as the plan, or the confirm is refused 409 STALE_RESTORE_CONFIRMATION. Auth users/passkeys are restored only with --include-auth; sessions and tokens are never restored. One restore runs per project at a time (409 SNAPSHOT_RESTORE_IN_PROGRESS). The result names live_release_id; undo with the pre_restore snapshot it reports.
  • run402 snapshots restore-status [project_id] <snapshot_id> <restore_id> [--json] — read one restore: status (running / ready / failed), release_mode, live_release_id, error, and, when ready, the full result.
  • run402 snapshots delete [project_id] <snapshot_id> [--json] — delete a snapshot and release its CAS references.
  • run402 branches create [project_id] [--from-snapshot <snapshot_id>] [--name <label>] [--email-mode sandbox|off] [--enable-cron] [--ttl-days <1..30>] [--json] — create a contained branch project. Email defaults to sandboxed, scheduled functions default off, and TTL defaults to 7 days. A parent with no live release yields an EMPTY branch (branch_url: null, no cloned release) whose host comes up on its first activation.
  • run402 branches list [project_id] [--json] — list active branches for the parent project.
  • run402 branches renew [project_id] <branch_project_id> [--ttl-days <1..30>] [--json] — extend a branch TTL.
  • run402 branches delete [project_id] <branch_project_id> [--json] — delete a branch project and purge its resources.
  • run402 deploy [--manifest app.json] [--project <project_id>] [--check|--print-spec|--print-manifest|--plan|--require-plan <plan_id>] [--no-rehearse] [--json] [--quiet|--final-only] [--allow-warning <code> ...] [--allow-warnings] [--allow-dirty] [-v|--verbose] — the deploy itself (the unified apply primitive with assets slice support); run402 up does any missing setup and then runs this same deploy. Without --manifest / --spec / --dir / piped stdin it discovers run402.json, run402.deploy.json, app.json or an executable run402.deploy.{ts,mts,js,mjs} in the current directory and fails MANIFEST_NOT_FOUND when none is there; it accepts JSON data manifests and explicit executable typed configs through the same --manifest flag. Rehearsal against a contained branch is automatic when there is a live release to protect (--no-rehearse skips it); --json is accepted as a no-op because success output is always JSON. Against a vault_policy: required project, the deploy-lane capture applies the SAME dirty-tree rule repos capture does: a dirty work tree refuses SNAPSHOT_DIRTY_TREE before any capture, and --allow-dirty captures it as-is with the same modified_captured/untracked_captured disclosure (see the repos section above and Observability). The final result always carries a stats block; -v/--verbose also prints a one-line stderr summary.
  • run402 deploy resume <operation_id> [--project <project_id>] [--quiet|--final-only] — re-run a paused operation forward, or follow it when it is still running
  • run402 deploy status <operation_id> [--project <project_id>] — fetch one operation’s snapshot (status, release_id, urls, error, rehearsal report)
  • run402 deploy promote <release_id> [--project <project_id>] [--allow-warning <code>] [--allow-warnings] — pointer-swap (re-point live release without re-running the apply pipeline)
  • run402 deploy list [--project <project_id>] [--limit <n>] — list recent deploy operations
  • run402 deploy events <operation_id> [--project <project_id>] — fetch the recorded event stream for an operation
  • run402 deploy verify <operation_id>|--operation <operation_id> [--project <project_id>] [--wait] [--timeout <seconds>] [--json] — verify gateway/edge release coherence; exits 2 when a valid report remains not coherent
  • run402 deploy releases get <release_id> [--project <project_id>] [--site-limit <n>] — fetch release inventory
  • run402 deploy releases active [--project <project_id>] [--site-limit <n>] — fetch current-live release inventory
  • run402 deploy releases diff --from <empty|active|release_id> --to <active|release_id> [--project <project_id>] [--limit <n>] — diff release targets
  • run402 deploy resolve (<url> | --url <url> | --host <host> [--path /x]) [--project <project_id>] [--method GET] — explain what a public URL would serve against the live release; a URL cannot be combined with --host/--path

Requires active tier and a provisioned project on Run402 Cloud. Against a configured Core target, uses the active Core project and does not require Cloud tier/wallet setup. Deploys to an existing project: runs migrations, applies the authorization manifest (from a manifest.json entry in files[]), deploys functions, deploys static site, and claims subdomain when the target supports that slice. Secret values are write-only: set them with printf %s "$OPENAI_API_KEY" | run402 secrets set <project_id> OPENAI_API_KEY --stdin or --file <path> before deploy, then use value-free secrets.require in deploy manifests. deploy stops before upload/commit on confirmation-required warnings unless each warning is covered by repeatable --allow-warning <code> or the broad reviewed --allow-warnings; --require-plan absorbs the warning approval already bound into the reviewed plan and rejects warning flags. The manifest must include project_id, or use --project, RUN402_PROJECT_ID, or an app-local workspace link. Omitting intent fails with UP_PROJECT_REQUIRED; it never falls back to the active project.

Inside GitHub Actions, deploy automatically uses OIDC credentials when GITHUB_ACTIONS=true, ACTIONS_ID_TOKEN_REQUEST_URL, and ACTIONS_ID_TOKEN_REQUEST_TOKEN are present. In that mode, project intent comes from --project, RUN402_PROJECT_ID, the app-local workspace link, or the manifest; conflicting selectors are rejected.

  • run402 live --tables <a,b> [--project <project_id>] [--cursor <cursor>] [--as anon|service] — stream change hints for live tables as NDJSON (Server-Sent Events under the hood; reconnects on its own)
  • run402 live --tables <a,b> --once [--cursor <cursor>] [--wait <1..25>] — one held read: hints since the cursor, or hold up to --wait seconds for the first one
  • run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path> | --no-workflow] [--expires-at <iso>] [--route-scope <pattern> ...] [--force] — create a GitHub Actions OIDC deploy binding and write a workflow (--no-workflow: the binding only)
  • run402 ci list [--project <project_id>] — list CI bindings for a project
  • run402 ci revoke <binding_id> — revoke a binding

link github requires a local wallet because it signs the delegation. The generated workflow does not require a wallet file or service key in GitHub; it uses GitHub’s OIDC token with id-token: write. Use repeatable --route-scope only when CI should deploy route declarations; no scopes means no CI route authority.

transfer (unified project transfer — wallet + email + owned-org recipient)

Section titled “transfer (unified project transfer — wallet + email + owned-org recipient)”

One noun, three recipient kinds: wallet = two-party SIWX; email = accepted by a principal whose verified email matches, into an org it owns or a new one; both complete on the ONE accept. Owned org = --to-org <org_id> immediate same-actor org move. Same /projects/v1/:project_id/transfers; preview/list/cancel are kind-agnostic; rows carry recipient_kind.

  • run402 transfer init (--to <wallet|email> | --to-org <org_id>) [--project <project_id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>] [--retain-member developer] — owner/admin initiate. --to routes by kind. --to-org: caller must actively own source+destination org; success completes immediately with accepted result + project keys. --retain-member developer: email recipients only, only developer, rejected with BAD_FLAG on wallet/org rails; recipient must accept it at accept; omit = full severance. --billing-policy/--kysigned wallet-only; email/org reject. Codes: INVALID_RETAIN_ROLE, RETAIN_SUBJECT_REQUIRED.
  • run402 transfer preview <transfer_id> — fetch the preview document (any party; kind-agnostic). Lists custom domains, subdomains, function names, secret NAMES (never values), CI bindings that will be revoked on completion, mailbox summary, billing implications, and — on email transfers — the retain_member offer.
  • run402 transfer list [--incoming | --outgoing] [--limit N] [--offset N] — --incoming (default) shows transfers OFFERED TO you; --outgoing shows transfers you initiated. Pending rows are unioned and each entry carries recipient_kind and preview_path.
  • run402 transfer accept <transfer_id> [--org <org_id>] [--accept-retained-member] — the one completion, whatever the row’s address. Wallet-addressed: your wallet must equal to_wallet; the response has secret_names_inherited[]. Email-addressed: your verified email must match; --org <org_id> names the receiving org you own, omit it to create a new org; --accept-retained-member accepts the retained developer offer from the preview (omit = severance) and the result carries retained_member_principal_id|null. Either way the accept atomically flips ownership, revokes previous owner CI bindings, stamps secrets_rotation_advised, and returns the new owner anon_key + service_key; SDK/CLI persist keys and set the project active. The returned service_key supersedes every earlier one: the previous owner’s key answers 401 CREDENTIAL_REVOKED, and the project’s functions are re-keyed within a minute. The anon_key does not change. Rotate inherited secrets with run402 secrets set. A credential of the wrong kind for the row gets 409 WRONG_COMPLETION_FOR_TRANSFER_KIND with an accept_transfer next action naming the right one. A project with a KyGit vault hands over its source first: the first accept publishes this machine’s keystore identity, names you as the principal that receives the source, and exits TRANSFER_VAULT_HANDOVER_PENDING (next action: the sender’s run402 transfer handover <transfer_id>); run accept again once the sender has. The accept moves the vault with the project (its storage and downloads count against your org; handoff/invite keys minted before the transfer stop working) and returns vault plus a retire_previous_owner_writers next action.
  • run402 transfer handover <transfer_id> — the SENDER’s step for a project with a KyGit vault: admits the principal the recipient named (its own published signing key, as a vault writer) and wraps the vault’s current key for it. Your key must be a writer of the vault. Safe to re-run; prints {transfer_id, repo_id, writer, envelope, vault_handover}. The recipient can accept once vault_handover.state is complete.
  • run402 transfer cancel <transfer_id> [--reason <text>] — cancel a pending transfer of any kind (any authorized party). If the recipient’s key was already admitted to the vault, the response carries a retire_previous_owner_writers next action.

Pending transfer: 72h TTL; owner-side mutations return 409 PROJECT_HAS_PENDING_TRANSFER with details.transfer_id + cancel next_actions[]. Data-plane and payment routes keep serving; transfer cancel unblocked. After accept, rotate inherited secrets; secrets_rotation_advised clears after every inherited name is rewritten.

KyGit vault: moves with the project after the handover above. Nothing forces the previous owner out of it; when ready, run run402 repos access retire-previous-owner (owner + step-up), which protects only what is written afterwards. A project whose vault keeps its source in your own bucket (BYO) is not transferable (409 TRANSFER_VAULT_BYO_UNSUPPORTED). A --to-org move requires your own key to already hold the source (TRANSFER_VAULT_HANDOVER_PENDING with run402 repos access sync otherwise).

Does not transfer: tier lease (stays with original org; no proration), KMS signers, GitHub repo ownership, on-chain balances. Wallet transfers only support --billing-policy migrate; if recipient lacks active org, accept -> 409 RECIPIENT_ORGANIZATION_NOT_ACTIVE. Email/owned-org always migrate; do not pass --billing-policy.