sponsormyterminal
v0.1.4
Published
ad402: sponsor cards in your AI coding agent (OpenCode), paid per card in USDC on Solana. Devnet pilot.
Downloads
775
Readme
ad402
License: Business Source License 1.1; bundled third-party code retains the licenses in THIRD-PARTY-NOTICES.txt.
Gate 1 of the Solana Devnet pilot is complete: an OpenCode 1.18.32 terminal card driven by a local ad and model fixture. Gate 2 has local Worker and real Coinbase CDP Devnet payout evidence. Gates 3 and 4 were accepted by the owner. Gate 4's single owner-approved CDP proof payout finalized on staging; the Worker was returned to disabled mode. The owner confirmed the keyboard sponsor link. The canonical PRD and gate evidence live in the ignored planning/ directory on the build machine.
The fixture assigns an ad to a request. Assignment does not verify delivery to a terminal, a complete frame, or a human view. The terminal measurements here are controlled Gate 1 observations and never billing signals.
Run locally
Requires macOS and Node.js 24 or newer. The CLI bundles [email protected] exactly. Install dependencies with npm ci, then run the fixture and CLI in separate terminals:
npm run fixture
node src/cli.mjs launch --api-url http://127.0.0.1:8788The published package includes a precompiled OpenTUI plugin. npm pack rebuilds it with prepack; after editing the plugin locally, run npm run build:plugin. Babel and the JSX compiler stay in development dependencies.
On first launch, enter an on-curve Solana address or press Enter for a disposable Devnet test wallet. Gate 1 rejects selected known program and sysvar IDs; full account-type checks require the later server gate. The wallet is a plaintext local test file with 0600 permissions; never use it for real funds. The CLI puts only the payout address, fixture origin, client version, and ads setting in a temporary 0600 tui.json. It does not edit OpenCode's config. OpenCode's model and provider settings remain the user's own settings. For a no-cost model during testing, use the isolated fixture test harness below.
The first ordinary prompt in each OpenCode session has no ad. Later prompts each cause at most one request when the session turns busy. The plugin skips that request when a blocking UI is open or the terminal is below 80×24; a lost response, short turn, or later resize can still leave an assignment without a visible card. A never-checked address first gets checking, so its next prompt can receive an ad. Shell input and built-in slash input were excluded in the Gate 1 TUI run. Model-backed custom slash commands are filtered by the plugin's draft classifier but still need a successful end-to-end host run. A visible card stays above the native input during the response. The optional HTTPS link opens with a mouse click or with Ctrl+X, then P (the default OpenCode leader key followed by the plugin's P binding). Ctrl+P opens OpenCode's command palette, where Open sponsor link is also available; it does not open the link immediately. Link opening does not affect rewards. Ad failures never block OpenCode.
Other Gate 1 commands:
node src/cli.mjs wallet address
node src/cli.mjs wallet set <Solana-address>
node src/cli.mjs ads off
node src/cli.mjs ads on
node src/cli.mjs statuswallet set and ads on|off take effect at the next launch. status reads finalized Devnet balances and rewards from the selected API; the CLI defaults to the reviewed staging origin, and --api-url selects a loopback fixture. wallet send <address> <amount> is available only to the local wallet: it checks test-USDC and SOL balances, shows the transfer for confirmation, then checks the quote and signs. The local wallet pays its own Devnet SOL network fee. The signed attempt is stored before sending, and a lost reply retries the same bytes.
Verify Gate 1
The probes create and remove disposable HOME/XDG directories. They bind fixture services only to 127.0.0.1 and use an OpenAI-compatible local model through OPENCODE_CONFIG_CONTENT. They never use a provider credential.
npm run typecheck
npm test
python3 tests/tui_probe.py
python3 tests/cli_probe.py
node tests/analyze_tui.mjsplanning/evidence/GATE1.md summarizes the outputs, measurements, limitations, and the owner's manual checklist. planning/ is ignored by Git.
npm test also runs an offline native Worker test in Miniflare for alarm scheduling, unused-address expiry, concurrent ATA setup, and alert delivery. It uses disposable keys and intercepts all outgoing RPC and alert requests with local mocks. Miniflare and the CLI fixtures require loopback sockets.
Gate 2 local Devnet setup
This work is local only. The staging Worker configuration keeps both switches off. Use disposable Devnet keys and the isolated setup script; never pass credentials or private RPC URLs as command arguments or exported environment variables.
python3 scripts/gate2_local_secrets.py
npm run dev:gate2The first command privately prompts for a CDP Ed25519 API key ID and secret, then a Helius Devnet RPC URL. It stores file-bound secrets in an ignored 0600 file under planning/gate2-local/home/, creates an ignored .dev.vars symlink, and prints only the unfunded ATA-rent and payout addresses. The second command runs workerd on 127.0.0.1 with isolated HOME, XDG and local storage.
In another terminal, start with the read-only checks and create one disposable test campaign:
python3 scripts/gate2_probe.py facilitator
python3 scripts/gate2_probe.py campaignThe owner must explicitly fund the printed ATA-rent address with Devnet SOL before gate2_probe.py create-ata <campaign-id>, and fund the printed campaign address with Devnet test USDC before gate2_probe.py activate <campaign-id>. gate2_probe.py serve requests an ad for the isolated payout address; its first request may return checking. gate2_probe.py state, status and attempt <reference> report local state without exposing keys. planning/evidence/GATE2.md records the real settlement, recovery and fault-injection runs, with limits for each result. PayAI completed the first real Devnet payouts after CDP connectivity failed. The owner then selected CDP for continued testing. Both local and staging configurations now select CDP, with no runtime fallback. Three CDP payouts and a deliberately invalid memo have since been tested in local workerd against Devnet; the evidence notes their limits.
For the current disposable test campaign, node scripts/gate2_sponsor.mjs init creates a 0600 sponsor keypair inside the isolated test home, and status reads its finalized Devnet balances. Circle's public faucet sends 20 test USDC to that sponsor, so the guarded send action transfers only the PRD's 0.10 test USDC to the campaign ATA. The fund-gas action transfers exactly 0.01 Devnet SOL from the approved disposable rent key to this sponsor, only after separate owner approval. Both actions save signed transaction bytes in ignored 0600 outbox files before submission and only resubmit identical bytes. They are bound to this test's printed public addresses and require new guards and owner funding approval for a fresh test identity.
Additional local recovery campaigns use the B preset: a maximum of ten ads and an initial 0.04 test-USDC deposit each. Their exact public targets and amounts are held in the ignored 0600 planning/gate2-local/home/funding-plan.json. After the owner approves that complete plan, node scripts/gate2_sponsor.mjs send-plan <campaign-id> validates the plan against the local campaign and transfers exactly 40,000 atomic units, with a separate signed outbox per campaign. Each deposit must be verified finalized before activation.
Gate 2 evidence includes a historical admission error: three valid CDP payouts and one refused wrong-memo ad were served while released debits were incorrectly excluded from the campaign budget. The PRD budget rule is restored; those serves do not prove budget-safe admission. Fresh Gate 3 campaigns are separate from those historical campaigns. The local test helper routes exist only with LOCAL_GATE2=true on loopback. The reviewed staging origin is compiled into the CLI and plugin. A disabled staging Worker is deployed; npm run bundle:staging remains a guarded local dry run and cannot upload a Worker.
Gate 3 local work
The operator panel lives at /operator on the local Worker. For a disposable test login, run python3 scripts/gate3_local_secrets.py operator-test before starting npm run dev:gate2. This creates a private key file in the isolated test HOME and binds only its SHA-256 hash to workerd. For an owner-chosen key, use python3 scripts/gate3_local_secrets.py operator and its hidden prompts instead. The panel creates a draft, previews the card at narrow and normal widths, publishes the immutable creative, shows funding senders, activates after a reviewed finalized deposit, pauses and closes campaigns, and exports JSON or CSV statements.
scripts/gate3_probe.py operates only on the local Worker. Its campaign A|B action creates a fresh campaign with the isolated sponsor wallet as refund destination. platform-fee-ata and campaign-ata <id> create USDC accounts with the previously funded disposable ATA-rent key. funding <id> shows chain deposits and senders; activate <id> <funding-signature> records the operator's reviewed transaction. serve --recipient <label>, status, attempt <ref>, close <id>, refund <id>, statement <id> --save, and tripwire-scan <id> support the local proof. The statement is saved only in ignored planning/evidence/.
The exact A/B deposits for this run are in an ignored 0600 funding plan. Only after the owner approves its two public destinations and amounts may node scripts/gate2_sponsor.mjs send-gate3 <campaign-id> transfer 0.10 or 0.04 Devnet test USDC. Each transfer is saved to its own private outbox before submission. Campaign creation alone sends no sponsor USDC. Creating an ATA spends disposable Devnet SOL from the ATA-rent wallet. The owner confirmed one local Telegram delivery to a private chat and chose to keep the current bot token. Staging alert delivery was later enabled and tested once in Gate 4.
Gate 4 local preparation
The reviewed Devnet API origin is https://devnet.ad402.fun, hosted as a Workers Custom Domain in the Trion account ([email protected]). The CLI and plugin also accept loopback HTTP for fixtures and reject other default origins. The disabled configuration keeps ads, payouts and identity off. Bootstrap and live uploads are separate owner actions documented in the Devnet runbook. The apex remains reserved for mainnet.
Build the local site, immutable tarball and disabled Worker bundle without credentials or network upload:
npm run build:site -- --contact [email protected]
npm run bundle:stagingThe first command publishes the tarball's full SHA-256 into the local landing page. The sponsor page uses the existing company inbox directly. In disabled staging, the operator publishes a campaign, opens its USDC account with the ATA-rent wallet through the protected panel, reviews the sponsor's finalized deposit, and activates it. Those two on-chain funding steps require separate owner approval. The proof-only configuration is generated later, after a reviewed campaign and owner wallet exist; it enables one 0.01 test-USDC payment with ads still off, then the disabled configuration is restored. No local test harness or campaign signing script is included in the participant tarball.
node scripts/staging_upload.mjs inspect checks the reviewed disabled config, built Worker and immutable site assets without reading credentials or contacting Cloudflare. Every published tarball is retained under site/downloads/; inspection requires the archived packages to be committed and unchanged. Private Devnet files live under ignored planning/devnet-local/ with mode 0600, prepared by python3 scripts/devnet_local_secrets.py. The guarded create-disabled command requires an empty remote Worker/namespace baseline and empty local state; it creates Coordinator and Identity once, then prints their IDs for owner pinning. Ordinary uploads preserve the pinned Coordinator and accept only remote migration tags v1/v2. upload-live requires a completed disabled deployment, a separately reviewed live config, all local provider secrets, Devnet-only payouts, mainnet-only history reads and exact owner-approved pilot caps. /operator verifies both Access JWT and the existing operator key/session. See the owner runbook for approvals, verification and recovery.
Gate 5 Devnet pilot preparation
The owner selected server assignment as the purchased unit and open participation with limits. The proposed deployed policy is a 0.010000 test-USDC default reward, 0.012000 default sponsor debit, 0.050000 assigned rewards per UTC day, and 0.100000 over this Worker's lifetime. The already paid 0.010000 Gate 4 proof counts toward the lifetime cap, leaving at most nine more assignments at the default reward. Campaign terms are stored when the operator creates a draft; its reward and debit may be lower than the deployed caps, and its optional daily ad cap may be left unset. Existing campaign terms remain at their historical values.
The owner also selected 60 seconds between ads and 10 ads per payout address per UTC day, while retaining the tested per-IP and global new-address controls. At most two recipient USDC accounts can be subsidized in total. Traffic limits slow abuse; the campaign's finalized balance and coordinator caps bound spending. wrangler.jsonc keeps both switches off, and the staging uploader still refuses an ads-on deployment. No participant ads are enabled by building or uploading a disabled candidate. Gate 6 requires separate custody and independently enforced spending limits before real funds; the Devnet Worker must not be reused as a Mainnet treasury.
Address validation has separate work limits. The defaults allow 50 pending first-time address checks, 10 new checks per IP per UTC day, and 100 new checks globally per UTC day. Configure them with PILOT_ADDRESS_CHECKS_PENDING_CAP, PILOT_ADDRESS_CHECKS_PER_IP_DAY, and PILOT_ADDRESS_CHECKS_GLOBAL_DAY; older configurations retain these defaults. Values must be positive safe integers, and the global daily limit must cover the per-IP limit. Rechecking an existing participant does not consume a first-time pending slot. Checks do not spend first-assignment or reward allowances.
Exhausting a check limit queues an alert, deduplicated once per limit type per hour, and schedules enabled alert delivery. Anonymous traffic can still exhaust a shared daily allowance and exclude other newcomers; these limits bound work without identifying legitimate participants. Unused address records expire after 24 hours, including when traffic stops. Records with an assignment, obligation, or recipient ATA job are retained, and obligation evidence checks use a destination index.
Failed address and ATA checks keep their retry deadlines so other due recipients can progress. Campaign balance monitoring rotates independently of finalized slots and writes its cursor only when the selected campaign changes. Monitoring is scheduled before activation is committed; a scheduling failure returns monitoring_unavailable and leaves the campaign in setup for retry. Managed campaign and platform-fee ATA setup share one in-flight preparation per account and reuse persisted signed bytes after a lost response. Alert delivery permits one in-flight send; a lost external acknowledgment can still cause a later retry to repeat a message.
Owner terminal check
Run npm run manual:gate1 in your own interactive macOS terminal. This command starts both local fixtures, gives OpenCode an isolated empty project and HOME/XDG, and creates an unfunded disposable Devnet wallet. It prechecks the test address and gives each model reply 12 seconds, so the second ordinary prompt can show the card. It never reads your normal OpenCode settings or uses an external model. The test wallet and home are removed when the command exits.
Follow the on-screen steps for a complete card, mouse and Ctrl+X then P link opening, prompt focus, and a narrow terminal. After OpenCode exits, answer the observation questions. The script saves only your answers and request counts in ignored planning/evidence/gate1-manual.json; this is an owner report, not display telemetry or payment proof. The sponsor link points to https://example.org/ and opens only when you choose it.
