Auth, billing, sign-in, doctor, logs
This slice serves customer people working within their own projects and organizations. Human sign-in, project administration and staff administration are distinct authorities. staff commands require separate platform credentials; they are not a repair shortcut for a customer permission denial.
Manage project user authentication: passwordless email links/codes, trusted invites, passwords, passkeys, and auth settings.
run402 auth magic-link --email <addr> [--delivery link|code|both] [--redirect <url>] [--intent signin|invite|claim|recovery] [--state <value>] [--locale <tag>] [--project <project_id>]— request passwordless email auth. Link is the wire default; redirect is required for link/both.--localepicks the email language (en,heright to left,ru; anything else English). JSON saysaccepted:true, preservesmessage/warnings, and returns opaquechallenge_idfor code/both without claiming delivery.run402 auth verify --token <token> [--project <project_id>]— exchange a magic-link tokenrun402 auth verify --challenge-id <challenge_id> --code <six-digits> [--project <project_id>]— exchange an email code. Token and challenge/code modes are mutually exclusive; the code is secret and is never retried or persisted by the CLI.run402 auth create-user --email <addr> [--admin <true|false>] [--invite] [--redirect <url>] [--project <project_id>]— service-key create/update auth userrun402 auth invite-user --email <addr> --redirect <url> [--admin <true|false>] [--project <project_id>]— create/update user and send trusted inviterun402 auth users get --email <addr> [--project <project_id>]— look up a project user (a member or a test user) by email and print{ id, email, is_admin, is_test, email_verified_at, created_at }; read-only, service key. Use the id to seed a role table keyed by user id. No match failsAUTH_USER_NOT_FOUND, whosenext_actionsname how to create the userrun402 auth set-password --token <bearer> --new <password> [--current <password>]— change, reset, or set passwordrun402 auth settings [--allow-password-set <true|false>] [--preferred <method|null>] [--public-signup <policy>] [--require-admin-passkey <true|false>] [--allowed-email-domains <csv|none>] [--test-mode <true|false|null>] [--project <project_id>]— update auth settings (requires service_key).--test-modeallows or forbids test sessions (run402 dev --as,r.auth.createTestSession);nullreturns to the tier default, on for prototype and off for hobby and team.--allowed-email-domainsis a comma-separated list restricting hosted Google sign-in to those domains (enforced at token issuance);noneclears it; empty = unrestricted.run402 auth passkey-register-options --token <bearer> --app-origin <origin> [--project <project_id>]— create WebAuthn registration optionsrun402 auth passkey-register-verify --token <bearer> --challenge <challenge_id> --response <json> [--label <text>] [--project <project_id>]— verify and store passkey registrationrun402 auth passkey-login-options --app-origin <origin> [--email <addr>] [--project <project_id>]— create WebAuthn login optionsrun402 auth passkey-login-verify --challenge <challenge_id> --response <json> [--project <project_id>]— verify passkey login and return session tokensrun402 auth passkeys --token <bearer> [--project <project_id>]— list authenticated user’s passkeysrun402 auth delete-passkey --token <bearer> --id <passkey_id> [--project <project_id>]— delete one passkeyrun402 auth providers [--project <project_id>]— list available auth providersrun402 auth scaffold-roles [--table <name>] [--user-col <col>] [--role-col <col>] [--roles <csv>] [--cache-ttl <secs>]— offline generator: emits a role-table migration +requireRolegate snippet + first-owner bootstrap (JSON out; no project/network). Pipe throughjq(e.g.| jq -r .migration).
Machine email-code flow: request --delivery both → retain challenge_id → read the code from email → verify the handle/code pair. The link in the same email remains a one-tap fallback. Credentials expire in 15 minutes and are single-use. challenge_id is public; never put the code, link token, cookie, or returned session tokens in logs/files. A request is accepted, not proven delivered.
billing
Section titled “billing”Email orgs + Stripe checkouts; pay by card or scale email beyond tier caps.
run402 billing create-email <email>— create an email-based organization (Stripe-only, no wallet). Sends a verification email. Idempotent.run402 billing link-wallet <org_id> <wallet>— link a wallet to an email organization for hybrid Stripe + x402 access. Response includes apool_implicationsblock (organizationtier,projects_in_pool_count,org_api_calls_current,org_storage_bytes_current,tier_limits,over_limit) — inspectover_limitbefore linking a wallet whose usage might push the merged pool past the tier cap.run402 billing checkout <org-id | wallet | email> --product tier --tier <prototype|hobby|team>— start/renew/upgrade a tier lease via Stripe (hobby $5 / team $20; prototype is free on testnet — userun402 tier set prototype, not a Stripe charge). Returns a checkout URL.run402 billing checkout <org-id | wallet | email> --product email-pack— buy a $5 email pack (10,000 emails, never expire). Returns a Stripe checkout URL.run402 billing checkout <org-id | wallet | email> --product balance-topup --amount <usd_micros>— add allowance to the organization. Returns a Stripe checkout URL.run402 billing topup <org-id | wallet | email> --sats <n> [--wait] [--qr <file.png>] [--timeout <seconds>] [--idempotency-key <k>]— top up the allowance over Lightning (lightning-cash-topup): mints a bolt11 invoice fornsats (100–1,000,000) through the platform’s hosted wallet provider and prints it (lightning:<bolt11>, pay from any Lightning wallet).--qr <file.png>also writes the invoice as a scannable PNG (uppercaseLIGHTNING:URI) for posting into a chat or showing on a screen. The dollar value is quoted at mint and credited as quoted, exactly once, when the payment lands (also up to an hour after the invoice expires —paid_late).--waitblocks untilpaid,paid_late, orexpired(exit 2 on expired/timeout; a late payment still credits) and prints the receipt. No Stripe, no node; the gateway answersLIGHTNING_TOPUP_NOT_CONFIGURED(503) when no provider is wired — usebilling checkoutthen.run402 billing auto-recharge <org_id> <on|off> [--threshold <n>]— auto-repurchase $5 packs when the allowance drops below threshold. Requires saved Stripe payment method.run402 billing metering [<org_id>] [--enable --cap <usd> | --disable] [--standing-order <on|off>] [--threshold <usd>] [--recharge <usd>]— metered usage under one monthly cap (PATCH /orgs/v1/:org_id/billing/metering); with no flags it prints the metering block (enabled,cap_usd_micros,suspended_reason,month_spent_usd_micros,included,headroom,prices,standing_order). Dollar flags are whole or decimal US dollars (--cap 50is 50,000,000 micro-dollars);--capis required the first time (at least $1),--threshold/--rechargeat least $5 (defaults $10 / $20). Needs a live hobby or team lease (403 METERING_REQUIRES_LEASE, next actionrenew_tier) and the billing role or higher; a grant key is refused.--standing-order onneeds a saved card (403 STANDING_ORDER_REQUIRES_CARD, next actionsave_card).run402 billing card setup [<org_id>] [--return-url <url>]— a one-time link to save a card for the standing order (POST /orgs/v1/:org_id/billing/payment-methods/setup). Prints{ org_id, setup_url, expires_at }; a person openssetup_urlin a browser, and the card is stored by the payment processor, never by Run402 or the CLI.--return-urlis a run402.com or localhost URL.run402 billing balance <org-id | wallet | email>—allowance_usd_micros+ email_credits_remaining + tier + lease + auto_recharge state (response includesorg_id). A wallet/email is resolved to its organization viaGET /orgs/v1/lookup?wallet=|?email=; an organization id (UUID) readsGET /orgs/v1/:org_id/billingdirectly.run402 billing history <org-id | wallet | email> [--limit <n>]— ledger history. Keyed by organization id: a wallet/email is resolved to itsorg_idfirst, thenGET /orgs/v1/:org_id/billing/history.
Auth: balance/history require SIWX from linked wallet or admin; wallet lookup requires matching SIWX (email lookup staff-only). link-wallet requires body wallet SIWX/admin; checkout/auto-recharge require linked wallet SIWX/admin. CLI signs from the local wallet, so that wallet must belong to the queried/linked/billed org. Non-member/guessed org id -> 403 no existence leak. create-email unauthenticated.
Email packs only activate when the tier daily limit is exhausted AND the project has a ProjectDomain with verified custom email sending (spam protection for Run402-managed mail reputation).
Metered usage. An organization on a paid lease (hobby or team) can turn on metered usage under ONE monthly cap: storage and vault bytes above the lease’s included pools cost a published price per GB-month (prorated by the UTC day), API calls above the included amount a price per million (run402 projects quote, the metered block), charged daily from the allowance and never past the cap in a UTC month. The same cap bounds what the card standing order may charge: when the allowance falls below the threshold the card refills it by the recharge amount, under the cap. When the allowance or the cap cannot cover a day, metering suspends: nothing is deleted, everything keeps serving, the flat pool applies again and new writes past it are refused with fund_allowance / raise_metering_cap / save_card next actions. Under metering the function and secret count ceilings apply per project instead of pooling. run402 tier status carries the block as metering, and on a metered organization pool_usage.*_limit is the effective headroom while pool_usage.*_included carries the tier’s amounts. Four owner events: metering_cap_nearing (80% of the cap, once a month), metering_suspended (suspended_reason: allowance | cap | lease), metering_resumed, standing_order_failed (three declines disable the order). Ledger kinds: metered_usage (debit, metadata: { dimension, quantity, unit, usage_date }) and standing_order_charge (credit).
contracts
Section titled “contracts”Signers — an Ethereum wallet Run402 keeps for your project, for sending money and signing smart-contract transactions. Its key is created in and never leaves the platform’s key custody. Pricing: $0.04/day rental + $0.000005 per call. Signer creation requires $1.20 in allowance (30 days of rent). Non-custodial — see https://run402.com/humans/terms.html#non-custodial-kms-wallets.
run402 contracts provision-signer --chain <base-mainnet|base-sepolia> [--recovery-address 0x...]— provision a signer ($0.04/day wallet rent, requires $1.20 in allowance at creation). Prompts for confirmation if the project already has ≥1 active signer.run402 contracts get-signer <signer_id>— get signer metadata + live native balance + USD value (free)run402 contracts list-signers— list all signers owned by the project, including deleted ones (free)run402 contracts set-recovery <signer_id> [--address 0x... | --clear]— set/clear the optional recovery address used for auto-drain on day-90 deletion (free)run402 contracts transfers create <signer_id> --to 0x... --amount <base units> [--asset usdc] [--chain <base-mainnet|base-sepolia>] [--idempotency-key <k>]— send a token from the signer with no gas on it: Run402 signs the EIP-3009 authorization with the signer’s key and its facilitator settles it and pays the gas (one signing fee, no gas).--idempotency-keyworks as oncall; a transfer that failed before settlement re-executes under its key, one that died in settlement never does;status: "unknown"is finished by the reconciler, poll it.run402 contracts transfers get <signer_id> --transfer <transfer_id>— one transfer (status, tx hash, nonce, failure phase)run402 contracts transfers list <signer_id>— the signer’s transfers, newest firstrun402 contracts set-alert <signer_id> --threshold-wei <n>— set the low-balance alert threshold (free; alerts billed via existing email infra)run402 contracts call <project_id> <signer_id> --to 0x... --abi <json> --fn <name> --args <json> [--value-wei <n>] [--idempotency-key <k>]— submit a contract write call (gas at-cost + $0.000005 signing fee). Returns{ call_id, tx_hash, status }.run402 contracts deploy <project_id> <signer_id> --bytecode 0x... [--chain <base-mainnet|base-sepolia>] [--value-wei <n>] [--idempotency-key <k>]— deploy a contract from the signer (gas at-cost + $0.000005 signing fee).--bytecodeis the full creation calldata (creation bytecode + ABI-encoded constructor args, concatenated client-side via viem/ethers; ≤ 128 KB). Returns{ call_id, tx_hash, status, contract_address }— the deterministic CREATE address is known synchronously. run402 does NOT compile Solidity.run402 contracts read --chain <chain> --to 0x... --abi <json> --fn <name> --args <json>— read-only contract call (free, no signing, no billing)run402 contracts status <call_id>— get call status, gas used, gas cost USD-micros, receipt, error (free)run402 contracts drain <signer_id> --to 0x... --confirm— drain native balance to a destination address (gas at-cost + $0.000005 signing fee). Works on suspended signers — the safety valve.run402 contracts delete <signer_id> --confirm— schedule the signer’s key for deletion (7-day window). Refused if balance ≥ dust — drain first.
stripe
Section titled “stripe”Stripe payments for your app, on the organization’s OWN Stripe account: the organization that owns the project connects its account to Run402’s platform once, your app takes card and SEPA Direct Debit payments (one-off or recurring) through Checkout Sessions on it, and the money events arrive on the project’s events feed. Run402 charges nothing on the account (zero platform fee), never holds the money, and your app never holds a Stripe key. Every command resolves the project from --project or the active project; auth is control-plane auth (wallet, sign-in session, or grant key with project:write; project:read for the reads) or the project’s service key over HTTP.
run402 stripe account connect [--country <iso>] [--business-type company|individual|non_profit] [--business-name <name>] [--support-email <email>] [--statement-descriptor <text>] [--email <email>] [--project <project_id>]— connect the organization’s account, or return the one it already has (idempotent per organization). Prefill is optional; Stripe collects the rest during hosted onboarding. While onboarding is incomplete the output carries a one-timeonboarding.urlfor the account holder, printed exactly once and never cached: hand it to the person, then runstatus.run402 stripe account status [--project <project_id>]— the account read fresh from Stripe:status(onboarding | active | restricted | disconnected),charges_enabled,payouts_enabled,requirements.currently_due[],capabilities(card_payments,sepa_debit_payments),next_actions[]. Never prints a link.run402 stripe account onboarding-link [--return-url <url>] [--refresh-url <url>] [--project <project_id>]— mint another one-time Stripe-hosted onboarding (or update) link; URLs must be HTTPS on one of the project’s own hosts or the console (422 STRIPE_RETURN_URL_NOT_ALLOWEDnamesdetails.allowed_hosts[]).run402 stripe account disconnect --confirm [--project <project_id>]— detach the account from Run402. The Stripe account itself is unaffected (stripe_account_unaffected: true); existing subscriptions keep billing on it, but Run402 stops receiving their events.--confirmrequired.run402 stripe checkout create --mode subscription|payment (--price <price_id> | --price-data <json>) --success-url <url> --cancel-url <url> [--quantity <n>] [--customer <cus_id> | --customer-email <email>] [--client-reference-id <id>] [--metadata <json>] [--trial-days <n>] [--locale <code>] [--idempotency-key <key>] [--project <project_id>]— create a Checkout Session on the organization’s account and print{ checkout_session_id, url, deduplicated, next_actions }; send the payer tourl.--price-datais Stripe’s shape, e.g.'{"currency":"eur","unit_amount":12000,"product_name":"Annual dues","recurring":{"interval":"year"}}'(recurringrequired in modesubscription, refused in modepayment; SEPA Direct Debit needseur).--client-reference-id(member:42) is echoed on every event about the session and its subscription.--idempotency-key: the same key with the same body returns the original session (deduplicated: true); a different body is409 IDEMPOTENCY_KEY_REUSED.run402 stripe checkout get <checkout_session_id> [--project <project_id>]— the session’s outcome from Stripe:status(open | complete | expired),payment_status(paid | unpaid | no_payment_required),customer_id,subscription_id,payment_intent_id,client_reference_id, yourmetadata.run402 stripe portal create --customer <cus_id> --return-url <url> [--project <project_id>]— open the customer portal for an existing customer; send them to the returnedurlto manage their subscription and payment method.
Account-state refusals are 403 with next_actions: STRIPE_ACCOUNT_REQUIRED (connect_stripe_account), STRIPE_ACCOUNT_ONBOARDING_INCOMPLETE (details.requirements, complete_onboarding), STRIPE_ACCOUNT_RESTRICTED (details.disabled_reason). A Stripe refusal the caller can act on is 502 STRIPE_UPSTREAM_ERROR (details.code, details.param); Stripe unreachable is 503 with Retry-After; the platform not configured is 503 STRIPE_CONNECT_UNAVAILABLE. The money events (stripe_checkout_completed, stripe_invoice_paid, stripe_invoice_payment_failed, stripe_subscription_updated, stripe_subscription_deleted) and the account events (stripe_account_updated, stripe_account_disconnected) ride the project’s events feed (run402 events list); route them to a function with a notification rule (channel: webhook) or poll the feed.
message
Section titled “message”run402 feedback send "<text>" [--project <project_id>] [--handle <handle>] [--return-address <how to reach you>]—--project+message: "promote: yes"relays a deploy’s promotion consent (see “Finish a deploy: hand your human two links” above), and apromote:message without--projectis refusedVALIDATION_FAILED;--handleis the human’s X/Twitter handle (≤64 chars), delivered as-is;--return-address(an email, a handle, a URL; at most 256 characters) tells the Run402 team how to contact you back, since there is no inbox to read. The output carriesnext_actionswithregister_contactwhen Run402 holds no verified email for your organization (run402 agent contact --name <name> --email <email>).
run402 agent contact --name <name> [--email <email>] [--webhook <url>]run402 agent statusrun402 agent verify-emailrun402 agent passkey enroll
contact returns email_verification_status, passkey_binding_status, assurance_level. New/changed emails start reply challenge and remain email_pending until owner replies. passkey enroll requires email_verified, emails enrollment link, never prints token.
notifications
Section titled “notifications”Owner notifications (audit log, preferences) plus the self-serve Telegram push channel and its routing rules.
run402 deliveries list [--type <event_type>] [--since <iso>] [--limit N] [--after <cursor>]— paginated delivery-attempt audit log.run402 deliveries get <notification_id>— one audit row.run402 contacts preferences/preferences set <key>=<value> ...— read/update cadence, per-class toggles,webhook_url, locale, timezone. Cross-wallet effects needemail_verified;webhook_urlchanges needoperator_passkey.run402 contacts test [--source app|platform] [--type <event_type>] [--severity <level>]— fires a real synthetic event through the FULL pipeline (email/webhook AND Telegram routing rules), rate-limited to 1/min. Omit the flags for the default sample event (severitynotice), or set them to exercise a specific rule’s filters. The response’stelegram.destinations[]reports one delivered/failed outcome per matched Telegram binding — empty when no rule matches (not an error) — andtelegram.skipped_rules[]names each rule that did not send and why:below_min_severity,deferred_to_window(withnext_window_closes_at), orescalation_not_exercised(a test never pages the ladder).
Telegram channel (notification-channel-routing-telegram):
run402 contacts connect telegram [--label <name>]— creates a PENDING binding, printsconnect_url(private chat) andconnect_group_url(group chat) — single-use, 15-minute deep links — then POLLScontacts listuntil the binding flips toactiveor the code expires, printing progress to stderr. Requiresoperator_passkeyassurance AND a VERIFIED owner email (bindings are addressed to it). Until the platform’s dedicated bot is provisioned on this gateway, this returns503 TELEGRAM_CHANNEL_NOT_CONFIGUREDwith anext_actionsentry — printed verbatim.run402 contacts list— every channel (email, webhook, and every live Telegram binding) for the authenticated wallet.run402 contacts rm <binding_id>— requires passkey assurance (operator_passkey); a missing/already-revoked/foreign binding id all return the same not-found error (no existence oracle).
Routing rules — one rule routes to exactly ONE Telegram binding; every match dimension you set (--project, --source, --type, --class) is ANDed, and an OMITTED dimension is a wildcard. --type/--class accept comma-separated lists (matches ANY listed value). No rules = no Telegram traffic — Telegram is opt-in per event, per rule; the mandatory email floor (security/recovery/billing_critical/destructive_lifecycle/verification classes) is completely untouched by rules and can never be silenced by one.
run402 subscriptions add --binding <binding_id> [--project <project_id>] [--source app|platform] [--type a,b] [--class a,b] [--min-severity info|notice|warn|critical] [--group-by a,b] [--window <seconds>]— requires passkey assurance (operator_passkey); an unusable/foreigntelegram_binding_idreturns the same 404 as a nonexistent one.--min-severityfilters on the severity the emitter declared. Without it, a rule that names no--typecarriesnoticeand above (routine traffic stays in the feed) and a rule that names types carries everything of those types.--window <seconds>(60-86400) sends ONE message per closed wall-clock window instead of one per event: the total, a line per--group-byvalue with its count (ten, then a summary), three samples, and a feed pointer.--group-byneeds--window; a rule keeps at most 200 group values per window and counts the rest under(other). The feed still holds every event.
run402 subscriptions add --escalate-to-level <n> --project <project_id> [--min-severity critical] [...filters]— instead of a chat, a matching event raises on the organization’s escalation ladder (mandatory pages, one-tap ack, deadman climb); a repeat while it is open joins it (attached_count) instead of paging again. Needs owner authority on the project’s organization (403 OWNER_REQUIRED). Bounds: 20 raises per rule per UTC day; refusals are counted on the rule (escalation_refused_count).- There is no count or rate condition: the rule filters on severity and reduces with a window, and a field the rule shape does not have is refused (
400 INVALID_RULE_FIELD). run402 subscriptions list— the person’s routing rules.run402 subscriptions rm <rule_id>— requires passkey assurance (operator_passkey).
Full rule-model explanation: run402 contacts --help / run402 subscriptions --help.
buzz notifications — route project events into a Buzz channel
Section titled “buzz notifications — route project events into a Buzz channel”Push selected project events into a Buzz community channel as signed NIP-29 messages. A route is an explicit, owner-declared destination: one ACTIVE community installation, 1–50 named projects (future or transferred-away projects never join automatically), reviewed event filters, one channel. The workflow is configure → authorize → test → live, and every command here has zero spend impact.
run402 buzz notifications scope <buzzper_id> --org | --listed [--project <project_id> ...]— switch the route between the whole organization (project_scope: org: every project it owns, present and future, read fresh per event so a transferred project drops out; no list to maintain) and an explicit list. The stored list survives a switch to--org;--listedon a route with nothing stored needs--project.configure --all-projectscreates a route org-wide from the start.run402 buzz notifications projects <buzzper_id> --add <project_id> | --remove <project_id>— widen or narrow a listed route’s explicit project scope (a PATCH at the route’s current revision). A listed route names its projects: a project an agent provisions later is not routed, and its deploys, crashes, and pages do not reach the channel, until it is added — or the route is switched to--org. Only events occurring from then on are delivered; nothing is replayed.run402 buzz notifications on-call <buzzper_id> --agent <hex-pubkey> [--name <display>] | --clear— set or clear the Buzz agent this route pages. A crash (error_fingerprints_observed) orplatform_incidenton a routed project is posted by the project bot as@<name> please investigate: …with apmention of that agent: the@nametext is what Buzz renders as a mention chip, the tag is what wakes a managed Buzz agent.--namesupplies the name; without it the gateway reads the agent’s Buzz profile (best effort — a silent relay leaves the page tag-only). The failing release’s deployer is addressed instead when it holds a public Buzz identity link.--clearempties it (the route pages nobody). The command reads the route’s current revision and PATCHes at it, then prints the one pubkey the agent must allow: a page is signed by the installation identity (not the project bot, which keeps deploy credits and receipts), and a managed Buzz agent answers only its owner by default, so add that pubkey to the agent’s respond-to allowlist (or set respond-to toanyone) and restart it. One entry covers every routed project.run402 buzz notifications configure --org <uuid> --installation <buzzci_id> --name <route_name> --channel <nip29-channel-id> --project <project_id> [--project <project_id> ...] [--event-type <t> ...] [--event-class <c> ...] [--include-org-events] [--on-call <hex-pubkey> [--on-call-name <display>]]— create the route.--on-callnames the agent the route pages from the start,--on-call-namethe@nameit is addressed by (seeon-callabove).--include-org-eventsalso delivers the organization’s own events into the channel — theplatform_payment_receivedreceipt after a Lightning top-up or tier purchase — on top of the named projects’ events; without it a route is strictly project-scoped. The response’sauthorizationblock is eitherauthorized(live now) orpending_buzz_authorizationwith the ONE exact, non-secret step, printed loudly to stderr: a Buzz community owner or admin adds thenotification_pubkeyas a relay member (the approving human’s own Buzz key lives in Buzz Desktop → Settings → Profile → Identity). Only three reviewed event types are routable (deploy_activated,error_fingerprints_observed,platform_incident); the classessecurity/billing_critical/destructive_lifecycle/verification/recoverymay NEVER be routed. Omitting a filter routes everything registered; an explicit empty filter is rejected — never a wildcard, never a silent match-nothing.run402 buzz notifications status [--org <uuid> | <buzzper_id>]— the org’s routes, or one route’s honesthealth(derived from route + credential state, never queue emptiness) with per-status delivery counts and therevisionan update must echo.run402 buzz notifications test <buzzper_id> [--wait]— re-checks the Buzz-side membership (activating apending_authorizationroute when the member-add landed — the test doubles as the authorization poll) and queues ONE signed test message (202, queued-not-delivered).--waitpolls until it settles; on timeout it prints the still-queued delivery and exits 2 — the publisher tick runs ~every 60s, so silence is cadence, not failure.run402 buzz notifications deliveries <buzzper_id> [--limit <n>] [--cursor <c>] [--delivery <buzzped_id>]— keyset newest-first history, dead letters included, the signed envelope never. Retries back off 1m/5m/30m/2h/12h to 8 attempts or 48h, thendead_letter; retried publishes are byte-identical, so the relay converges on one Nostr event id.run402 buzz notifications pause|resume <buzzper_id>— pause stops matching NEW events (nothing is retroactively delivered on resume); resume re-arms and resets the hard-failure counter (ten consecutive hard failures auto-pause withpause_reason: delivery_failuresand fire the mandatorybuzz_route_auto_pausedowner notification).run402 buzz notifications rotate <buzzper_id>— stages the NEXT signing generation; the current key keeps signing until the next pubkey’s own Buzz-side membership verifies. No command in this group ever accepts or prints a signing secret —notification_pubkey+signing_generationare the only credential material on the wire.run402 buzz notifications revoke <buzzper_id>— cancel queued deliveries and keep sanitized history readable; the installation’s notification credential is destroyed only when this was its LAST live route.
Routes deliver NEW events only (the outbox floor is captured at creation), and Buzz is never a deadman channel: mandatory owner notifications keep their human paths (email, Telegram) regardless of route state, and a Buzz delivery acknowledges nothing. Auth: org owner + fresh step-up (a SIWX wallet is inherently fresh); every mutation is idempotent (--idempotency-key, auto-generated when omitted).
webhook-secret
Section titled “webhook-secret”run402 webhook-secret rotate— new HMAC signing secret for the notification webhook, returned EXACTLY once (store it immediately). Previous secret remains valid for 24h (dual-secret grace window). Requires passkey assurance (operator_passkey; arun402 loginsession satisfies it).
sign-in (login, logout, whoami, approve)
Section titled “sign-in (login, logout, whoami, approve)”A person signs in; an agent’s wallet is a separate identity (run402 status shows the wallet’s own organization state). There is one sign-in session, bound to the person’s principal and graded by how it was minted: loopback (run402 login, full and step-up-able), device (run402 login --device, read-only), browser (the console). A device-grade session reads everything a member may read; every write answers 403 SESSION_READ_ONLY naming run402 login.
Wallet-less writes (write approval): wallet agents use SIWX. A person without a wallet uses the sign-in session; provisioning (org.project.create), deploying (project.deploy), and secret writes (project.secret.write) also need a passkey-signed write approval for one (action, target), sent as X-Run402-Write-Approval. Missing or mismatched approval → WRITE_APPROVAL_REQUIRED / WRITE_APPROVAL_SCOPE_MISMATCH / WRITE_APPROVAL_BINDING_MISMATCH (403) or WRITE_APPROVAL_SESSION_INVALID (401); the SDK raises WriteApprovalRequiredError (isWriteApprovalRequired()) with the exact run402 approve … command and an approve_write next action. Approval is never ambient for MCP/CI/non-TTY; only an interactive TTY auto-opens the browser and retries once. A write approval never satisfies step-up.
run402 login [--no-open]— loopback-PKCE (RFC 8252) sign-in. Starts a127.0.0.1listener, opens the browser (TTY) for the passkey ceremony, exchanges the code atPOST /agent/v1/control-plane/cli/token, and caches theloopback-grade session at{base}/control-plane-session.json(0600; the token is never printed). Signing in again is the step-up forSTEP_UP_REQUIRED. Stdout{ logged_in, grade, read_only, principal_id, amr, expires_at, expires_in_seconds, memberships? }—memberships[]includes invites auto-redeemed at sign-in when the lookup succeeds. Browser sign-in flows are SDK/console-side (r.session.*).run402 login --device [--no-open]— RFC 8628 device authorization for a terminal with no browser:POST /agent/v1/control-plane/cli/deviceprints a URL and user code to stderr; approve it in the console (which needs a sign-in within the last 5 minutes, elseRECENT_SIGN_IN_REQUIRED); the CLI polls…/cli/device/tokenand caches adevice-grade (read-only) session. Errors:LOGIN_DENIED,LOGIN_EXPIRED,LOGIN_TIMEOUT.run402 whoami—GET /agent/v1/whoami: the principal, memberships, andsession({ grade, amr, amr_times }, ornullwhen the active wallet reads), pluswrite_approvals[]cached on this machine (action,org_id,project_id,expires_at; never the token). With a live cached session it reads as that session; a session the gateway no longer honours is cleared and answersSESSION_INVALID.run402 logout— revokes the session (POST /agent/v1/control-plane/session/revoke), then deletes it and every cached write approval. Idempotent; the local cache is cleared even when the revoke fails. Stdout{ revoked, cleared }.run402 approve --action <capability> (--org <org_id> | --project <project_id>) [--no-open]— mint a write approval for one(action, target):org.project.create+--org,project.deploy+--project,project.secret.write+--project. Requiresrun402 login(a device-grade session cannot approve:SESSION_READ_ONLY). OpensPOST /agent/v1/control-plane/write-approval/challenges, the passkey confirm page redirects to the loopback, and…/write-approval/cli/tokenmints the token, cached at{base}/write-approvals.json(0600) bound to(api_origin, sign-in session, action, target); 30 minutes idle, 4 hours at most. Approvals for different targets coexist;loginandlogoutclear them.projects provisionanddeployrun it for you on an interactive TTY. Errors:LOGIN_REQUIRED,APPROVE_FAILED,APPROVE_BAD_CONFIRM_URL.run402 orgs list— your orgs joined with the account overview (GET /agent/v1/me/overview: tier, lifecycle, lease, quotas, allowance, wallets, advisories); see the orgs slice.
The session is principal-scoped and cached once at the base config dir, independent of --wallet. No MCP login by design: an MCP host holds no browser, and a person’s session must not become the agent’s ambient authority; MCP whoami reports the grade.
service
Section titled “service”Public service-level status. No wallet, no auth, no keystore required — works on a fresh install. Reports on the Run402 service (uptime, capabilities, operator); for your organization state use run402 status.
run402 service status— public availability report (24h/7d/30d uptime per capability, operator, deployment, schemarun402-status-v1)run402 service health— liveness check with per-dependency results (postgres, postgrest, s3, cloudfront) and service version
SSR origin-cache inspection + invalidation. Capability ssr-isr-cache (paired with @run402/astro v1.0+). Cache is scoped per-project; cross-project hosts return R402_CACHE_INVALIDATION_HOST_FORBIDDEN.
run402 cache inspect <url> [--locale <code>] [--release-id <release_id>]— read the cache row state for a URL. Stdout is JSON:{ status: "HIT" | "MISS", host, path, locale, releaseId, cachedAt, expiresAt, writtenUnderGeneration, contentSha256, headers }.statusis NEVERBYPASS— inspect does not issue a request, so it cannot evaluate runtime bypass conditions. Defaults to active release + default locale; pass--locale/--release-idto inspect non-default rows. (The legacy--jsonflag is removed — JSON is the default.)run402 cache invalidate <url>— invalidate a single absolute URL. Stdout:{ deleted, host, path, generation }.run402 cache invalidate --prefix <p> --host <h>— invalidate all rows under a path prefix on a specific host.run402 cache invalidate --all --host <h>— entire-host purge (catastrophic content changes; nav restructure; layout-wide updates).
Every invalidate returns { deleted, generation, host, path? }. generation is the post-increment per-(project, host) counter — it gates in-flight MISS renders from overwriting after invalidation completed.
doctor
Section titled “doctor”Health + config diagnostics. Agent-DX entrypoint — agents run this first to verify environment before attempting anything else.
run402 doctor [--verbose] [--refresh] [--no-scan] [--dir <app>] [--manifest <path>] [--scan-dir <D>] [--project <project_id>] [--only <check> ...]— checks: config dir presence, installed CLI update state (cli_update:latest_known,checked_at,comparison, andinstall_confidence(confidence in detecting the installation method, explicitly labeled byinstall_confidence_basis: "installation_method_detection"; independent of whether registry versions match); confidence concerns install detection, not version truth), local wallet (wallet) + rail, keystore wallet count, API base reachability, active tier + lifecycle state, notification health snapshot (binding state + per-attempt verification failure detail), recovery posture (per vault-owning org: human-owner login + member source-key backup evidence), vault, source scan (auth-aware-ssr). thecli_updatecache has a real 24h TTL (kychee-com/run402#561): a plaindoctorcall reuses a cache within that window only for the same installed version, and refreshes a MISSING or EXPIRED cache or one recorded before an installation change with exactly one bounded live check automatically —--refreshsimply forces that same check even when the cache is still fresh. A failed live check (offline) falls back to the last known-good value rather than reporting a bare unknown, and it is labeled as an estimate, not presented as current:cli_update.value.cachealways carriesfresh,freshness_basis: "observation_age",age_ms,refresh_attempted,refresh_failed, andlast_attempt_at, and a stale fallback’shintnames the estimate’s age and the failure reason. An installed version ahead of the observation has statusunknown, with no downgrade advice;--refreshcan retry the registry. A within-TTL observation is not proof that an ahead-of-observation installation is current. Stdout is{ ok, blocking[], warnings[], checks[] }.okanswers one question — can this agent ship from here — and istrueexactly whenblocking[]is empty; advisory findings never affect it or the exit code (0 whenok, 1 otherwise), so an unbound passkey or a degraded recovery posture on another org is reported inwarnings[]whileokstaystrue. Every check carriesseverity: "blocking" | "advisory" | "info"(blocking: config dir or wallet missing/error, API unreachable, tier inactive/frozen/past_due/dormant/missing/error, error-severity source-scan findings; advisory:account_health,recovery_posture,vault,runtime_staleness,retiring_routes,cli_updategaps; info: ok/skipped/unknown).warnings[]is[{ check, code?, message, hint? }], one entry per gap;blocking[]is[{ check, status, message, hint? }];checks[]is[{ name, status, severity, value?, hint?, message? }]. Agents branch onokand readwarnings[]for non-blocking gaps. Thetiercheck’sstatusis a fixed vocabulary (ok | inactive | frozen | past_due | dormant | purged | missing | unknown | error, never a tier name) with the tier name and raw lifecycle invalue.tier/value.lifecycle; a wallet whose own organization holds no tier but can reach projects on another organization reportsmissingas advisory (TIER_MISSING_ON_OWN_ORG), not blocking.--onlyand--refreshproduce the same shape. When stale,cli_update.value.next_actions[]includes anupgrade_clientaction withcommand,argv, install context, confidence,cwd, and mutation flags. When owner email verification ispending, the doctor surfaces the per-reason hint fromemail_verification.last_challenge.hintalong withattempt_count/remaining_attemptsso the person sees what to fix. When the gateway reportsreachability.reachable: falseonGET /agent/v1/me/status(recovery-event-reachability — the org resolves to zero verified notification recipients across both the wallet-contact chain and org-membership verified emails), the notification-health check warns that mandatory recovery/security notifications currently reach nobody, including how many were already skipped in the last 90 days, with therun402 agent contact --email ...remedy. Therecovery_posturecheck (vault-recovery-custody; rides the same status read) renders one entry per vault-owning org:control_plane_configured: falsewarns “no human owner with a working control-plane login” with therun402 orgs invite create <org_id> --email … --role ownerremedy;source_backup_configured: falsewarns “no human member holds a working source-access key” pointing at console.run402.com/account source enrollment;custody_legacy_present: truewarns that a member key is still single-credential legacy custody (one passkey, no recovery code). These are EVIDENCE levels — “configured” is what the platform verified, never proof an off-platform passkey or saved code still exists — and the same events arrive asorg_recovery_posture_degraded/org_recovery_posture_recoveredfeed events. A gateway without the block reportsskipped, and an account with no vault-owning org reportsokwith an emptyorgslist. The source scan selects only the application at--dir/cwd or the explicit--manifest; without a selected manifest it reports unscoped/skipped. It scans the selected app’ssrcand explicit referenced source files, excludes sibling apps and nested repositories, and shares its gate with SDKupanddeploy. It flags hallucinated SDK auth names (R402_AUTH_UNKNOWN_EXPORT), state-changing GET handlers (R402_AUTH_STATE_CHANGING_GET),auth.*calls inexport const prerender = truepages (R402_AUTH_PRERENDERED), and direct mutation ofinternal.sessions.authz_version(R402_AUTH_AUTHZ_VERSION_PROHIBITED), and warns whenadminDb()(the service key, bypassing RLS) appears in a file that reads the current user (SERVICE_KEY_IN_USER_REQUEST; usedb(req), or silence with// run402-allow-admin-db: <reason>). Theretiring_routescheck (same status read,retiring_routes[]) lists each of your projects’ calls to a route being retired in the last 7 days, with the fix:run402 functions rebuild --all --project <project_id>for deployed functions,npm i -g run402@latestfor an old CLI or SDK, or the successor URL for your own code; a gateway without the block reportsskipped.--no-scanskips the scan (config-only checks).--scan-dir <D>is an advanced arbitrary scan and reports advisory findings without claiming application deploy refusal.--project <project_id>(kychee-com/run402#566) targets the vault check at that project, outranking the repo-standing pin/remote/env/active-project default — scoped to the vault check only, since every other check is wallet/machine-wide. Any flag doctor does not recognize is rejected (UNKNOWN_FLAG/BAD_USAGE), never silently ignored.run402 deployruns the scan as pre-flight and refuses to deploy on anyerror-severity finding (bypass withRUN402_DEPLOY_SKIP_SCAN=1); with--dir <build>it scans the Astro app that build belongs to, never the build output. Patterns match code only: names inside comments are not findings, and allow-comments are read from your source. (The legacy--jsonflag is removed — JSON is the default.)
--only <check> (repeatable, kychee-com/run402#566 remaining half). Runs ONLY the named check(s) and suppresses every other one — INCLUDING the source-tree scan, which on a monorepo can produce thousands of findings that bury a single check’s diagnosis (the original dogfood complaint). A skipped check’s work never runs at all: run402 doctor --only vault costs one vault read, not the full config/tier/account/scan sweep. Composes with --project, which still scopes only the vault check. The valid check names are config_dir, cli_update, wallet, projects, api_reachable, tier, account_health, runtime_staleness, retiring_routes, recovery_posture, vault, source_scan (run402 doctor --help lists them); an unknown name is BAD_USAGE listing the valid set. --only is not used together with --buzz — buzz mode is its own separate, always-complete check set, and the combination is rejected rather than silently ignoring one of the two flags.
run402 doctor --only vault # just the vault diagnosis — no source scan, no monorepo noiserun402 doctor --only vault --project prj_1a2b3crun402 doctor --only config_dir --only wallet --no-scanAstro dev wrapper. Loads .env.local, verifies RUN402_PROJECT_ID + RUN402_SERVICE_KEY, then spawns astro dev with the env inherited.
run402 dev [--port <n>] [--host <h>] [--project <project_id>] [--as <label>]
SDK calls (db, auth.user, cache.invalidate, assets.put) hit the LIVE Run402 project at https://api.run402.com — no local database, storage, or key-custody setup required. This is shape-parity with production. Offline emulator mode is deferred to v1.5.
Without --as, auth.user() is null in astro dev, so a members-only site renders signed out. --as <label> previews it signed in: before starting Astro it mints a 2-hour test session (r.auth.createTestSession) for the test user <label>@<project_id>.test.invalid and passes it to the dev server in RUN402_DEV_TEST_SESSION; the @run402/astro adapter then runs every request under astro dev as that user, so auth.user() (with is_test: true), auth.requireUser(), auth.role({ from }) and db() (under the user’s row-level security) behave as on the deployed site. Needs RUN402_ANON_KEY in .env.local (print the keys with run402 credentials project-keys export --project <project_id> --reveal) and the project’s test mode (on by default for prototype; otherwise TEST_MODE_DISABLED names run402 auth settings --test-mode true). The label is a test-user label, never an email or user id: no real member can be impersonated. The token stays in the dev server (the browser never holds it or the service key), astro build never includes the middleware, and when the session expires requests are served signed out until you restart run402 dev --as <label> (same label, same user). A dev server Astro is already running for the project keeps the environment it started with, so run402 dev stops it (astro dev stop) before starting a new one and says so; under an AI agent Astro runs astro dev in the background, so stop it with npx astro dev stop when you are done. The banner prints the test user’s id beside its email with a role-row hint; give it a role in your own role table with run402 projects sql like any member (any user’s id: run402 auth users get --email <addr>).
init astro
Section titled “init astro”Project scaffolder. Subroute of init (alongside init rail setup).
run402 init astro [<dir>] [--force]— creates a deployable Astro project withpackage.json(dev/deploy scripts),astro.config.mjs(one-line@run402/astropreset),src/pages/{index,[slug]}.astro(the latter demonstrates the full DB-backed dynamic page pattern with cache directive),src/layouts/Layout.astro(title + og + canonical),src/pages/api/save-page.ts(admin save endpoint withdb().upsert() → cache.invalidate()),.env.example,.gitignore. Refuses non-empty dirs without--force. Stdout is JSON{ dir, files_created, created, next_steps }; progress lines go to stderr. (The legacy--jsonflag is removed — JSON is the default.)
Fetch function logs by request id. Top-level shortcut — for fine-grained per-function control, run402 functions logs <project> <name> still works.
run402 logs --request-id <request_id> [<function> | --function <name>] [--project <project_id>] [--tail <n>] [--since <ts>] [--app|--platform|--all]— fetch the log lines correlated to one request across every function in the project. Reads with the project’s cached service key, or with your wallet / session / grant key (project.read) when none is cached, so an org member or teammate agent can follow an error fingerprint’s logs drill-down without a key. The id is thex-run402-request-idheader on every function response (req_…);fnrun_…/fnatt_…durable-run ids work too. No function name is needed: the SDK fans the read out to each deployed function and merges the matches oldest-first, each entry tagged with itsfunctionandorigin. By default only the function’s own output is shown (--app); Lambda’s runtime lines (INIT_START,START/END/REPORT RequestId, billed duration) areorigin: "platform"and hidden —hidden.platformcounts them and ahintappears when hiding them left nothing,--platformshows only them,--allshows the raw stream.--function <name>(or a leading positional) narrows the scan;--functionalone is that function’s recent tail.--tail(default 100, max 1000) bounds the read per function before the filter. Unknown flags are rejected. Stdout is JSON{ ok, request_id, project_id, scanned, entries, errors?, origin, hidden?, hint? };okisfalseonly when a function’s read failed (named inerrors[]with itscode). (The legacy--jsonflag is removed — JSON is the default.)
When an SSR response returns a 5xx, the response headers include x-run402-error-code: R402_SSR_RUNTIME_ERROR and x-run402-request-id: req_.... Copy the request id and run run402 logs --request-id req_... to fetch the full stack trace. Scans every function in the project in parallel and aggregates entries timestamp-ascending unless --function narrows the scope.
npm installation script warnings
Section titled “npm installation script warnings”Neither the CLI nor the SDK ships a TypeScript loader. On Node 22.18 or later a TypeScript deploy config that uses only erasable types loads through Node’s own type stripping. A config that needs a transpiler (an enum, an extensionless or .js-spelled import of another .ts file) loads through tsx installed in the config’s own project (npm install --save-dev tsx); without it the command fails with EXECUTABLE_CONFIG_TS_LOADER_UNAVAILABLE and that install as its next action. tsx depends on esbuild, and some npm configurations then warn that esbuild is not covered by allowScripts: its postinstall only verifies its platform binary, which it finds through its optional platform package anyway, so a skipped postinstall does not stop configs from loading. Inspect the install exit code rather than the warning, and keep the user’s script policy intact; do not enable scripts globally. Optional fsevents failures are separate from this warning.