How PaperRoute earnings are computed
This document is the public, authoritative description of the payout math.
The constants here are the same constants the code runs
(apps/web/src/lib/earnings-constants.ts); if they ever disagree, that's a
bug — tell us.
The formula
Every minute your device is actively used (not idle, not locked), part of your wallpaper is actually visible, and a sponsor or house creative is actually composited on it, you earn:
cents = RATE × exposed_megapixels × (1 minute / 60)
where:
| Constant | Value | Meaning |
|---|---|---|
RATE_CENTS_PER_ACTIVE_EXPOSED_MPX_HOUR |
2 | Cents (your 50% share) per active-exposed-megapixel-hour |
DAILY_ACTIVE_MINUTES_CAP |
480 | Earning stops after 8 active hours per device per UTC day |
EXPOSED_MPX_CAP |
16.5888 | Exposed area is clamped to two 4K monitors (2 × 3840 × 2160 px) |
CLICK_MULTIPLIER |
50 | A sponsor click is worth 50 impression-minutes |
CLICK_CREDIT_CENTS |
2 | Flat click credit: round(50 × 2 / 60) — once per ad per device per day |
PAYOUT_MINIMUM_CENTS |
1000 | $10.00 minimum balance to request a payout |
MAX_DEVICES_PER_ACCOUNT |
5 | Devices per account |
Worked example: a 2560×1440 monitor (3.7 mpx) fully exposed for an active
hour earns 2 × 3.7 = 7.4 cents… but wallpaper is rarely fully exposed.
A typical 30%-exposed desktop earns about 2.2 cents/hour — PaperRoute is
beer money, not rent money, and we'd rather you know that up front.
Rounding
Per-minute amounts are accumulated as exact fractions and rounded once per device per day, half-down (1.5 → 1). No per-bucket rounding, no cumulative rounding drift.
What "active" and "exposed" mean
- Active: keyboard/mouse input within the last 120 seconds, session not locked. Idle and locked minutes earn nothing.
- Ad displayed: minutes only accrue while a creative (sponsor or operator-funded house ad) is composited into the wallpaper. Running the app with no ad to show — for example when there's no inventory — earns nothing and doesn't consume the daily cap.
- Exposed: the part of the desktop wallpaper not covered by windows, measured as window rectangles only. Fully see-through overlays (screen dimmers, click-through HUDs) don't count as covering — the wallpaper behind them is visible. We never read window titles, process names, or screen contents — see the data dictionary.
Clicks
Opening the current sponsor from the tray menu (or following an ad's short
link from your paired device) credits CLICK_CREDIT_CENTS once per ad per
device per UTC day. Anonymous QR scans redirect but credit nothing.
The 50/50 split
Ledger amounts are your half of the ad revenue. In the MVP, the day-one inventory is operator-funded house and affiliate ads — PaperRoute itself is the sponsor, paying out of pocket at the rate above while real sponsor inventory is built. The rate may change; changes are announced and never applied retroactively.
The advertiser side
Sponsors prepay into a wallet (Stripe Checkout) and ads serve only while that wallet is positive — nothing runs on credit:
- For every cent a publisher is credited from a sponsor's ad, the sponsor's
wallet is debited 2× (
ADVERTISER_GROSS_MULTIPLIER = 2). That's the 50/50 split from the money-in side: the publisher keeps their half, and the platform keeps the other half — Stripe's processing fees come out of the platform's half, never the publisher's. - A click debits
CLICK_DEBIT_CENTS= 4 (2× the publisher'sCLICK_CREDIT_CENTS), once per ad per device per UTC day. - The wallet is an append-only ledger, same as publisher earnings: the balance is always a SUM of deposits, spend, refunds, and adjustments. Nothing is ever overwritten.
- At $0 the sponsor's ads pause automatically and resume on the next deposit. An ad that reaches its total budget stays paused until the sponsor changes it, even after a top-up.
- An ad may also set an optional daily spend cap. Once a UTC day's attributed spend (impressions + clicks) reaches the cap, no further debits are attributed to the sponsor for the rest of that day; billing resumes at 00:00 UTC. The ad itself may keep displaying past the cap — publishers are still credited for that exposure, and the platform absorbs the difference, the same way it does for house ads.
- Unspent budget is refundable on request. Refunds are reviewed by an operator and returned to the original payment method — only the un-spent remainder is ever refundable.
- House ads (PaperRoute's own inventory) debit no one; publisher credits for them are operator-funded as described above.
The dwell report
Each ad's detail page reframes raw spend as genuinely-visible desktop time. An exposed-screen-hour is one megapixel of the creative kept visible on an active desktop for one hour (exposed-megapixel-minutes ÷ 60) — the same exposed-area × time that drives the split above, so cost per exposed-screen-hour is the true unit price of attention. The report also shows the average exposed megapixels across serving devices and cost per click, over a rolling 30-day window (the telemetry retention horizon). Every figure sits next to a platform-wide inventory average for the same window. That benchmark is aggregate scalars only — the average across all sponsor ads — never another advertiser's identity or itemised numbers.
Payouts
- Publisher payouts run on Stripe Connect Express: you connect a payout account once from the dashboard, and Stripe handles identity and banking — PaperRoute never sees or stores those details.
- You can request a payout once your available balance is at least
PAYOUT_MINIMUM_CENTS($10.00). - Requests move through a reviewed queue: requested → approved → paid (or rejected). The amount is held — excluded from your available balance — from the moment you request, so the same cent can never be requested twice.
- On "paid", a Stripe Transfer moves the money to your connected account. The transfer is idempotent per request: a retry after a failure re-sends the same transfer instead of paying twice.
Caps exist to keep the system honest
The daily cap, area clamp, and device limit are enforced server-side so that an inflated or dishonest client cannot mint money. Anomalous devices are flagged and their earnings held for review rather than silently paid.
Data dictionary — everything PaperRoute collects
This is the complete list of fields the desktop client ever transmits. It
matches the zod contracts in packages/shared/src/index.ts field-for-field;
if the code and this page ever disagree, that's a bug — tell us.
What is never collected, by design: window titles, process names, application identities, screenshots, screen contents, keystrokes, URLs, clipboard, or anything readable on your screen. The client computes geometry locally and transmits only the aggregates below.
Telemetry (POST /api/v1/telemetry, one batch per minute)
Contract: TelemetryBatch
| Field | Type | Purpose |
|---|---|---|
deviceId |
string | Which paired device is reporting |
buckets[].bucketStart |
integer (unix seconds, minute-aligned) | Which minute the measurement covers |
buckets[].exposedMpx |
number (megapixels) | How much wallpaper was visible that minute, averaged, summed across monitors |
buckets[].active |
boolean | Whether you were present (any input in the last 120s, session unlocked) |
buckets[].adId |
string | null | Which creative was displayed; null = none composited (those minutes never earn) |
Request headers: x-device-id, x-timestamp (unix seconds), x-nonce
(random, replay protection), x-signature (HMAC). None carry personal data.
Pairing (POST /api/v1/pair, once per device)
Contract: PairRequest
| Field | Type | Purpose |
|---|---|---|
code |
string | The one-time code you generated in the dashboard |
deviceName |
string | A label you see in your dashboard (defaults to the Windows computer name — rename it if that bothers you) |
monitors[].widthPx, monitors[].heightPx |
integers | Monitor resolutions — the ceiling for plausible exposure (anti-fraud) and the area clamp |
Sponsor interactions
GET /api/v1/ads/next— signed headers only, no body.POST /api/v1/click—{ adId }: which sponsor you opened from the tray.
Account data (website, via Clerk)
Your authentication identity (e-mail) lives with Clerk, our sign-in provider. PaperRoute's own database stores your Clerk ID, role, devices, telemetry buckets, the earnings ledger, and payout requests.
Receipts (website)
The dashboard can render any earning day — or a completed payout — as a downloadable thermal-till PNG receipt. A receipt is a read-only projection of your own ledger and telemetry: exposure time, active-exposed megapixel-hours, distinct sponsor count, the 50/50 split, and your running balance. Like every other surface it carries geometry-derived aggregates only — never window titles, process names, or screen pixels. The tear-off barcode is a deterministic hash of that day's ledger entry ids, not a scannable code.
Advertiser accounts (website)
Opening an advertiser account stores, in PaperRoute's own database:
| Table | What it holds |
|---|---|
advertisers |
Your Clerk-linked user id and a Stripe customer id. Card numbers and billing details live with Stripe — PaperRoute never sees or stores them. |
advertiser_ledger_entries |
The append-only wallet ledger: amount, kind (deposit / spend_impression / spend_click / refund / adjustment), the Stripe object id for deposits and refunds, and attribution metadata (which ad, which UTC day). No personal data. |
advertiser_refund_requests |
Amount requested, status, an optional note, and timestamps. |
ads |
The creative image you uploaded, its destination URL, name, serving status, total budget, optional per-UTC-day spend cap (daily_cap_cents), optional schedule, and (if rejected) the moderation reason. When the cap is set, the earnings cron stops attributing spend to the ad once a UTC day's spend reaches it, resuming at the next UTC day. |
stripe_events |
Processed Stripe webhook event ids, their event type, and when they were processed — pure idempotency bookkeeping. |
The ad detail dwell report (exposed-screen-hours, cost per screen-hour, CPC, average exposed area) stores nothing new — it is derived on demand from your own ad's telemetry buckets and wallet ledger. Its platform benchmark is an aggregate average across all sponsor ads; no other advertiser is named.
Payout accounts (website)
Setting up payouts stores a payout_accounts row: your Stripe Connect
account id, two flags (details submitted, payouts enabled), and the
timestamp of the last Stripe account event applied (webhook-ordering
bookkeeping). Identity,
KYC, and banking details are collected by Stripe Express onboarding and live
entirely with Stripe. Payout requests store amount, status, an optional
note, and the Stripe transfer id once paid.
Retention
Telemetry buckets exist to compute and audit earnings: retained while your
account is active, deletable with your account. Replay nonces expire within
the hour. The earnings ledger — publisher and advertiser alike — is
append-only and kept for the life of the account (it is the money record).
Backups of your original wallpaper never leave your machine — they live in
%APPDATA%\PaperRoute.