Appearance
What we use, and why
Every outside service, library and piece of infrastructure, with one line on what it does for Salvo. Status uses the home page states, with "testnet" and "test key" written out. Hosting rows say Running, and the probe row says Measured. Env vars are listed by name only.
Which of these cost money and do nothing yet
Accounts that can cost money: Railway, Stripe (per payment, and only on a live key), Resend, ParcelsApp, Lit, and any proxy or paid RPC. ParcelsApp, Lit and a proxy do no work in production today, because what each serves is not running yet.
Money coming in
| Service | What it is for | Status | Env |
|---|---|---|---|
| Stripe | Takes card payments for Cinemark tickets through Stripe Checkout, and refunds them. For gift cards at the register: saves the shopper's card once, holds the price when the barcode opens, then charges or cancels the hold. Gift card resale needs Stripe's approval before a live key; see Where the money goes | 🟢 Cinemark on a test key. 🔵 Gift card holds built, never run against Stripe. A live key is refused | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
| x402, with the x402.org facilitator | Lets an AI agent pay for an order in USDC over HTTP 402. The facilitator checks the buyer's signed transfer and submits it on chain, so our receiving address needs no funding | 🔵 Built, never run. Not configured in production | X402_PAY_TO, X402_NETWORK, X402_FACILITATOR_URL |
MPP on Tempo (mppx) | A second machine-payment protocol. Agents pay in a stablecoin on the Tempo chain | 🟢 Testnet only: two settlements on moderato | TEMPO_*, MPP_SECRET_KEY |
Crypto refunds (viem) | Sends the token back to a buyer from our wallet, after checking it holds enough of the token. Gas and fees are not checked. Never for a Salvo Pay order: those refuse before any key is read | 🔵 Built | X402_PRIVATE_KEY, TEMPO_PRIVATE_KEY |
Salvo Pay accounts (Tempo passkey accounts, viem/tempo) | The shopper's and seller's own account on Tempo's test network. A passkey (Face ID) is the root key; a scoped access key on the phone signs without a prompt, only into the escrow, up to a weekly limit the chain enforces. Testnet faucet for test dollars. No sponsor is configured, so the phone pays its own fee | 🔵 Built, tested offline. Off unless ESCROW_RAIL=testnet | ESCROW_RAIL, TEMPO_SPONSOR_URL (optional) |
See Payment rails.
Money at rest
| Service | What it is for | Status | Env |
|---|---|---|---|
| Morpho | Lends idle escrow into a vault on Base to earn yield for the reserve. Payouts never wait on it. Only scripts can reach it | 🔵 Built, no transaction sent | MORPHO_* |
| Multi-chain escrow (Tempo mainnet and Moderato) | Would hold a buyer's money in a contract until release or refund. The escrow pool, payout policy and chain registry have tests, but no contract they can read is deployed, so every escrow read fails closed to unfunded. The gift card escrow (SalvoEscrowV1, next row) is separate | 🔵 Built, no contract deployed | CHAINS_ENABLED, CHAIN_* |
Gift card escrow on Tempo, second version (SalvoEscrowV2) | Holds a dollars shopper's payment at the till: the value used of each card, and a deposit at face on what is left on the last. A card sells in parts; what is left stays listed for the next shopper first. The tap is one batch; Lit's release runs beside the second host's agreement, and the purchase is booked once both agree. No owner or admin. The keeper follows it beside V1 | 🔵 Built. Deployed on Tempo's test network on 2026-10-13, at 0xa30d1631bf644fa608dacc02b6d2aebbc1cd200b, pinned and bound; its live smoke passed. Open to shoppers (ESCROW_V2_LIVE): new sales go here, and V1 takes nothing new. The till's dollars rail on it is tested offline only. Not reviewed | ESCROW_RAIL, ESCROW_KEEPER_KEY, ESCROW_SELLER_ALLOWLIST |
Gift card escrow on Tempo (SalvoEscrowV1) | Frozen for new sales since V2 opened: what it sold still releases, pays out and refunds. Held a Salvo Pay shopper's payment. Paying and opening the card are one transaction; the seller is paid 24 hours later by anyone's call; only the seller can refund. No owner or admin. Read from two pinned public hosts that must agree. Our keeper, in the 15-minute job, records sales and, with its key set, makes the payout call | 🔵 Built, with live testnet scripts (escrow:*). Deployed on Tempo's test network on 2026-10-10, at 0x75c9aabe8254559f865e14d6e5fa70c738869bef. Its creation check and live smoke passed. Not reviewed: on the testnet a listing is taken as the operator's own card | ESCROW_RAIL, ESCROW_KEEPER_KEY, ESCROW_SELLER_ALLOWLIST |
See Morpho.
Keeping card numbers safe
| Service | What it is for | Status | Env |
|---|---|---|---|
| Local envelope vault | Encrypts each card number and PIN under its own key, wrapped by a master key. This is what runs today. Every listing is recorded as the operator's, including any card a stranger types into the public sell screen (an open decision) | 🔵 Built | VAULT_MASTER_KEY |
| Lit Protocol (Chipotle, Lit V3), keys derived in a TEE | Each card is encrypted to a key that only one exact piece of code holds, inside Lit's enclave. Meant for other people's cards once the unlock rule reads the escrow contract. Today's rule is our own signature, so it is custodial and refused for sellers | 🔵 Rebuilt. Live smoke passed on Lit on 2026-10-10 | None in production. The smoke reads the usage key from --key-file; setup uses the account key from the shell |
Lit escrow template (salvo-vault-escrow/1) | The unlock rule that reads the escrow. It signs a ticket before any money moves, and releases a card only for a final purchase, encrypted to the buyer's phone key. Our routes relay it and see ciphertext only | 🔵 Built, frozen, and pinned to the testnet escrow. Its signer and sealing key are pinned from a live bind on Lit, and the live smoke passed on 2026-10-10. Not counted non-custodial: on the testnet a listing is taken as the operator's own card | LIT_VAULT_USAGE_KEY at runtime, with the rail on |
See Custody.
Stores
| Service | What it is for | Status | Env |
|---|---|---|---|
| Best Buy and Nike balance pages | Reads a card's balance from the store's public, no-login page, so a sale can settle on fact | 🔵 Built, never read a real card. Nike's result field is a guess | none |
| Best Buy and Nike checkouts | Fills a card into the store's own checkout, for online redemption | 🔵 Built. Best Buy's steps come from saved pages; Nike's checkout was never captured. Live use switched off | BESTBUY_ALLOW_LIVE_REDEEM, NIKE_ALLOW_LIVE_REDEEM, BESTBUY_CLIENT_* |
| Cinemark driver | Buys a ticket for the buyer from the operator's own Cinemark account, paid with Movie Club credits | 🔵 Built, never run for real. Both testnet orders used a stand-in instead | FULFILLER, CINEMARK_* |
| Toast, Square and Clover probes | Read-only checks of whether a venue takes gift cards. Toast and Clover answer per venue; Square's probe can only say unknown | 🟢 Measured: Toast across 127 venues | none |
| Barcodes | Draws the store's barcode on the buyer's phone. Code 128, ITF and Code 39 are written in-house. Target's PDF417 is drawn on our server with bwip-js and sent with the page, so the phone loads nothing extra | 🔵 Built, never scanned at a register | none |
| Apple Wallet pass | Adds a pass to Apple Wallet. It holds a short-lived signed reference, not the card; the store's barcode only appears on the reveal page | 🔵 Built, unsigned: no certificate | WALLET_* |
| Cards drawn on the phone (Salvo Pay) | A bought card's barcode, drawn on the shopper's phone with bwip-js's browser build, from the card the phone decrypted: at the till in scan order, and on Account for cards bought on the first escrow. Our server never sees it. Passes built on the phone, for Apple Wallet, are a later phase | 🔵 On-screen code built, never scanned. Phone-built passes not built | none |
Delivery
| Service | What it is for | Status | Env |
|---|---|---|---|
| ParcelsApp | Confirms a parcel was delivered, which releases the first stage of a cash advance payout. It replaced EasyPost, which kept tracking behind a manual review we could not clear. Nothing in production uses it yet: no live call has been made, and the cash advance has no storage | 🔵 Built, never called live | PARCELSAPP_API_KEY, DELIVERY_CONFIRMER |
See Cash advance.
Messages
| Service | What it is for | Status | Env |
|---|---|---|---|
| Resend | Emails buyers when an order is fulfilled or refunded. With no key, the email is logged instead | 🔵 Built | RESEND_API_KEY, EMAIL_FROM |
| ntfy, self-hosted on Railway | Pushes every order state change to the operator's phone. Self-hosted because pushes from the Railway worker to the public ntfy.sh timed out, for a reason never found | Running. Checked end to end on a smoke order | NTFY_SERVER, NTFY_TOPIC |
Hosting and data
| Service | What it is for | Status | Env |
|---|---|---|---|
| Railway | Runs four services. The web app, a worker that holds the Cinemark session, and a reconcile job every 15 minutes share one Docker image; ntfy runs from its own. A push to main redeploys web, worker and cron, and web skips a push that touches none of its watched paths | Running | set by Railway |
| PostgreSQL, through Prisma | Stores orders, their event log, gift card listings, and webhook de-duplication. Card numbers are stored only as ciphertext | Running | DATABASE_URL, TEST_DATABASE_URL |
See Operations.
Building and testing
| Tool | What it is for |
|---|---|
| Next.js and React | The app |
| VitePress | Builds these docs into the app, on the same domain |
| Playwright | Drives Chromium for the Cinemark driver, the balance pages, the checkouts and the UI audit. It ships with the app, not only with the tests |
| Vitest | The test suite, run against a separate schema on the Railway database |
| An HTTP proxy: a residential service, or tinyproxy on a VPS as the runbook sets up | Only for the session-portability test, which has never been fired. PORTABILITY_PROXY_* |
| viem, zod, tsx | Chain calls; checking env vars the first time they are used; running scripts |
What we do not use
- No paymaster. There is no paymaster, bundler or smart account. Every wallet is a plain private key. The two nearest things: the x402 facilitator submits the buyer's USDC transfer, and Tempo fee sponsorship, whose testnet relay URL is stored in the code but never used. Some comments say the Tempo key co-signs sponsored fees; the payment call passes no fee payer, so buyers pay their own Tempo fee from their stablecoin. Salvo Pay is the one place a wallet is not a plain key: its accounts are passkeys with a scoped access key, Tempo's own account type, and they also pay their own fee.
- No analytics, error tracking or telemetry.
- No SMS.
- No sign-in provider. Buyers and sellers have no accounts. The operator page is protected by a shared secret.