Get paid for waiting. A privacy-first, transparent ad marketplace for AI coding tools — built as a "better Kickbacks.ai" reference product.
- 60/40 split, locked. 60% of net ad revenue goes to the developer, enforced by a public
formula in payment code (
dev_share = floor(gross × 60 / 100)). Not "estimated", not "up to". - Privacy by design. Telemetry carries ad-event metrics only — no code, no prompts, no AI responses, ever. There is no opt-in tier that changes this.
- Signed everything. Impression events are Ed25519-signed per device; client update manifests are Ed25519-signed by the server. No unsigned event is credited, no unsigned update is applied.
- Transparent. Every campaign, CPM, impression count, and dev payout is on a public ledger.
See docs/ARCHITECTURE.md for design decisions, docs/FRAUD.md for the fraud-detection
strategy, docs/COMPLIANCE.md for money rails and tax/compliance, and
docs/PRODUCTION.md for the deployment guide.
server/ Express + better-sqlite3 API (auction, ledger, payments, signed updates)
web/ Vite + React dashboard (home, advertise, transparency, earnings, install)
clients/
opencode/ opencode plugin (ad fetching + signed impressions)
claude-code/ Claude Code CLI status-line + hooks integration
vscode/ VS Code extension (status-bar surface + update verification)
shared/ account setup script (registers email + device Ed25519 key)
start.sh Runs API (:3001) and web (:5173) together
npm install
./start.shThen open the web dashboard at http://localhost:5173 (API at http://localhost:3001).
Demo data is seeded on first boot: campaigns across all four surfaces plus a demo account.
First configure an account (shared by all surfaces):
node clients/shared/setup.mjs- Opencode: copy
clients/opencode/plugin.tsto~/.config/opencode/plugins/, reload opencode. - Claude Code CLI:
bash clients/claude-code/install.sh— patchesstatusLine+SessionStart/Stophooks in~/.claude/settings.json(backup written first, restore command printed). Self-update:node clients/claude-code/waitshare.mjs update. - VS Code: build with
npm run buildinclients/vscode, package withvsce, install. The status bar renders the shared sponsored line and theWaitShare: Verify update signaturecommand demonstrates signed-update verification.
All routes are under /api/v1.
| Route | Purpose |
|---|---|
GET /ads/next?surface=&deviceId= |
Issue an ad serve (default 30 min TTL) |
POST /ads/impressions |
Report a signed impression; credit dev at locked split |
POST /auth/register |
Create/sign-in a developer account |
POST /auth/device |
Register a device Ed25519 public key |
GET /auth/google/login · GET /auth/google/callback |
Google OAuth sign-in (redirects to dashboard) |
POST /webhooks/stripe |
Stripe webhooks (campaign activation, onboarding, payout clearing) |
GET /dev/me · GET /dev/earnings · GET /dev/payouts |
Developer dashboard data + payout history |
POST /dev/onboarding · POST /dev/payout |
Stripe Connect onboarding and payout request |
POST /advertiser/campaigns |
Create campaign + Stripe Checkout session |
GET /auction/state |
Live market: surface CPMs, impressions/hr |
GET /ledger |
Public transparency ledger |
GET /updates/latest · GET /updates/key |
Signed update manifest + server public key |
GET/PUT /updates/artifacts/:name |
Download / upload update artifacts (PUT is admin-only) |
POST /updates/ |
Publish a signed manifest (admin token required) |
GET /split |
The locked revenue split contract |
services/payments.ts defines a PaymentProvider interface. A working stub is active by default
(STRIPE_MODE=stub), so checkout, onboarding, and transfers simulate cleanly. To go live:
- Set
STRIPE_SECRET_KEY(secret key) andSTRIPE_MODE=live. - Set
STRIPE_WEBHOOK_SECRET(webhook signing secret) and addPOST /api/v1/webhooks/stripeto your Stripe account's webhook endpoints. - Webhooks handled:
checkout.session.completedactivates a paid campaign,account.updatedflips dev onboarding status,transfer.createdmarks a payout cleared. The route is mounted beforeexpress.json()so the raw body is available for signature verification.
The live provider uses Stripe Checkout for advertiser payments and Stripe Connect Express for dev payouts (payouts are transfers to the dev's connected account; WaitShare never holds funds).
POST /dev/payoutmoves the available balance into aheldpayout (available balance zeroed).- After the clearing window (
PAYOUT_HOLD_MS, default 72h) a sweeper creates the transfer (pendingin live mode,clearedin stub mode) and thetransfer.createdwebhook marks itcleared. RESERVE_PCT(default 10%) of each dev share is withheld inreserve_millsat credit time and released back to the available balance afterRESERVE_RELEASE_MS(default 30 days).
Google OAuth is available for developers: the dashboard's "Continue with Google" button redirects to
/api/v1/auth/google/login, and the callback exchanges the code, upserts the dev by Google sub/email,
and redirects back to the dashboard with a session token in the URL fragment. Set GOOGLE_CLIENT_ID and
GOOGLE_CLIENT_SECRET to enable; email sign-in remains as a demo fallback.
Impression integrity beyond signatures:
- Challenge nonce. Every serve carries a random nonce; the impression must echo it (signed) or the serve is voided — defeats blind replay of captured events.
- Focus/occlusion. Impressions report
focusPct(share of time the surface was focused/visible); the VSCode surface measures it from window-focus events. Gate withMIN_FOCUS_PCTwhen ready. - Env-tunable caps.
CAP_HOURLY,CAP_DAILY,MAX_PENDING_SERVES(serve hoarding),MIN_GAP_MS(inter-impression spacing),SERVE_TTL_MS,MIN_IMPRESSION_SECONDS,MIN_VIEWABLE_PCT. Payout safety:PAYOUT_HOLD_MS(clearing window),RESERVE_PCT+RESERVE_RELEASE_MS(clawback reserve). - Fleet-wide (Tier 2). IPs are masked to the /24 (IPv6 /48), salted (
FRAUD_SALTor a persistent per-instance salt), and SHA-256 hashed; only the hashes are stored. Detection:- Farm — a masked network shared by ≥
TIER2_FARM_DEVS(5) distinct developers withinTIER2_WINDOW_MS(24h) is withheld from serving. - VPN rotation — a developer appearing on ≥
TIER2_VPN_NETWORKS(3) distinct networks within the window is withheld. - IP binding — each serve is bound to the network that issued it; an impression from a different masked network is rejected (403) and the serve voided.
- Rejections land in the
fraud_eventsaudit table and incrementdevs.fraud_flags. - ASN/DC reputation —
server/src/services/asn.tsclassifies IPs against a multi-provider range dataset (server/assets/asn.json, ~68.5k ranges from AWS, Google, Microsoft, Oracle, DigitalOcean; built byserver/scripts/build-asn.mjs); withTIER2_DC_ENFORCEon (default), datacenter/cloud networks are withheld from serving (tier2-dcevents logged).
- Farm — a masked network shared by ≥
- Risk scoring + trust tiers (Tier 3). Every impression gets a 0–100 risk score through a
pluggable
RiskModel(server/src/services/risk-model.ts): a deterministic heuristic model or a trained logistic model loaded fromserver/assets/risk-model.json(TIER3_MODEL_PATHoverrides; malformed/missing → heuristic fallback). Features are activity-shape statistics only (regularity, duration/viewability uniformity, rate, network rotation, flags, account youth) — no content factors. Score ≥TIER3_HIGH_RISK(75) rejects, ≥TIER3_REVIEW(55) flags for review. Trust tiers (0new →1established →2trusted) gate serve caps, and tier-0 accounts get reduced hourly/daily caps plus aTIER0_PAYOUT_CAP_CENTSpayout gate. Admins clear/review/suspend via/api/v1/admin/review. - Advertiser chargeback defense (Tier 4). Live checkout runs Stripe Radar on every charge;
charge.dispute.createdstops delivery and logs achargebackrow infraud_events;charge.dispute.closedreactivates the campaign if the dispute iswon(and audits the resolution) or leaves itdisputedif lost;charge.refundedflips the campaign torefunded.
Client updates are integrity-protected end-to-end:
-
Publish an artifact and manifest (admin only —
ADMIN_TOKENrequired):WAITSHARE_ADMIN_TOKEN=<token> node server/scripts/publish-update.mjs claude-code-cli 0.2.0 ./artifact.mjs
The script uploads the artifact, computes its sha256, and stores an Ed25519-signed manifest. The signature covers exactly
{ platform, version, url, sha256 }(stable key order) and can be verified against the public key atGET /updates/key. -
Clients check
GET /updates/latest?platform=&version=; the shared updater (clients/shared/updater.mjs) verifies the manifest signature, downloads the artifact, and verifies its sha256 before applying atomically (write-temp + rename). -
Wired surfaces:
waitshare.mjs update(Claude Code CLI, self-replaces), the VS CodeWaitShare: Update extension (verified)command (downloads + installs the.vsix), and the opencode plugin logs a verified-update notice on load. Any manifest or artifact that fails verification is refused.
- Configuration — all knobs documented in
.env.example; the server validates its configuration at startup (hard errors forSTRIPE_MODE=livewithout a key, warnings for production defaults like demo seeding or localhost base URLs).X-Forwarded-Foris trusted only whenTRUST_PROXY=1(default off), so a directly exposed API cannot spoof the header to rotate IP rate limits or network hashing. - Payments / OAuth —
STRIPE_MODE=stubexercises the full marketplace without keys; setSTRIPE_MODE=livewith realSTRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRETandGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETto go live. - CI —
.github/workflows/ci.ymltypechecks, builds, and runs a self-contained smoke test (npm test -w server) that spawns the server on a temp data dir and drives auth → device key → campaign → serve → signed impression → payout → updates → tier-2/3 fraud signals → admin review end to end; a second job runs the same suite against a Postgres 15 service container. - Relational DB — the server runs natively on SQLite (default, dev) or Postgres: set
DATABASE_URLand the driver loadsserver/migrations/001_init.sql(idempotent DDL + indexes) at startup.server/scripts/export-postgres.mjsmigrates an existing SQLite DB to portable SQL. - See
docs/PRODUCTION.mdfor the full deployment guide.
- Device keys are generated client-side; the server stores only the public key.
- Impression payloads must be signed with the registered device key and echo the serve's challenge nonce; the server rejects unsigned, wrongly-signed, or nonce-mismatched events.
- IP addresses are masked to the /24 (IPv6 /48), salted, and SHA-256 hashed; only the hashes are persisted and used transiently for fleet detection — raw IPs are never stored.
- Serve records expire after the serve TTL (default 30 minutes, matching the single-impression
duration cap), can be used once, and cap at
MAX_PENDING_SERVESper device.
Shipped: ASN/DC reputation (multi-provider dataset — AWS, Google, Microsoft, Oracle,
DigitalOcean — with enforcement), pluggable risk scoring (heuristic + trained logistic model
behind one interface), graduated trust tiers with a human review queue, advertiser chargeback/
Radar defense (dispute/refund webhooks, including dispute-won reactivation), a native Postgres
runtime (async data-access layer + idempotent schema), verified live Stripe webhook handling
(signature check + every campaign and payout transition covered by test/webhook.mts), an
admin review-queue UI (/admin with label filters, pagination, and per-dev fraud timelines),
per-dev fraud review labels (bucket counts per signal in fraud_labels), an ops surface
(metrics endpoint, request logging, per-IP rate limiting, backup/restore scripts), a Docker
deployment (single container serving the built dashboard + API, with a Postgres service),
CI validation of the Docker build and VS Code extension packaging, an auto-earn loop in the
VS Code client, and automated Open VSX publishing on version tags. Since v0.4: automated
releases (release.yml builds the zip, opens a release-branch PR, and publishes a GitHub
Release on v* tags), a persisted admin-action log (admin_actions) feeding the risk
trainer, an export script (npm run export:labels -w server) that turns review-queue
decisions into labeled feature CSVs, admin smoke coverage for filter/pagination/timeline,
and auto-earn parity across the opencode plugin and the Claude Code CLI.
Remaining (all behind existing interfaces, no contract changes):
- Replace the synthetic logistic weights in
server/assets/risk-model.jsonwith a model trained on labeled review-queue data. The trainer (npm run train:risk -w server) accepts real labels via--data labeled.csv(7 feature columns +label) and falls back to synthetic samples. Labels can be exported from the review queue withnpm run export:labels -w server(--out labels.csv --per-dev 300 --max-rows 20000; Postgres viaDATABASE_URL, SQLite viaDATA_DIR). Real labels arrive once the review queue has production traffic (live Stripe spend).