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
PaperRoute prices exposure with an open auction, not a flat rate. 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 = winning_bid × exposed_megapixels × (1 minute / 60) / 2
winning_bid is the gross price (in cents per exposed-megapixel-hour) the
sponsor whose ad was actually showing was bidding at that minute. The / 2
is your 50% share — see below.
where:
| Constant | Value | Meaning |
|---|---|---|
MIN_GROSS_BID_CENTS |
1 | Smallest gross bid an advertiser can set, cents per exposed-megapixel-hour |
MAX_GROSS_BID_CENTS |
100 | Largest gross bid an advertiser can set — bounds the platform's worst-case liability from a single click or impression and catches fat-fingered bids |
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, priced at the serving ad's bid |
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, showing an ad bidding 4¢/mpx-hour, earns 4 × 3.7 / 2 = 7.4 cents…
but wallpaper is rarely fully exposed. A typical 30%-exposed desktop at that
bid earns about 2.2 cents/hour — PaperRoute is beer money, not rent money,
and we'd rather you know that up front. The rate moves with what sponsors
are actually paying; see "Checking the current rate" below for the live
number.
How the auction works
Every ad an advertiser runs carries a bid: cents per exposed-megapixel-hour, set when the ad is created and editable any time from the advertiser dashboard. Bid changes take effect immediately — no re-moderation, no waiting period — and are never retroactive: past minutes keep the bid that was in effect when they were served. This history is append-only, so we can always reconstruct exactly what any past minute paid.
When your device asks for an ad, eligible funded advertisers enter a lottery: one ticket per advertiser, weighted by their highest current bid, so screen time is shared in proportion to what each sponsor pays. A sponsor bidding 10¢ gets twice the rotation of one bidding 5¢, and nobody can rent the whole fleet: winning more share means bidding higher and paying that higher rate on every extra hour, so the cost of dominating grows superlinearly. Only ads tied at an advertiser's own top bid can serve (running copies of an ad, or low-bid decoys next to one high bid, buys nothing). Eligible means active, approved, within its schedule window, and backed by a wallet that isn't at $0. When no advertiser is bidding, an unpaid house ad fills the space instead — house ads pay nothing and earn nothing, they exist purely so the space isn't blank.
Checking the current rate
GET /api/v1/rate is public and returns the live picture: the current top
bid among ads eligible to serve right now (halved — what the market
leader's minutes pay you), whether any sponsor is bidding at all, the sum
of per-advertiser top bids (the lottery's total ticket pool — a sponsor's
bid divided by this is their share of the rotation), and a trailing
24-hour average rebuilt from yesterday's actual served minutes and their
bids. The trailing average is a
simple average over the minutes that were served — it does not apply
the DAILY_ACTIVE_MINUTES_CAP, so on days with unusually long sessions it
can read slightly higher than what any single device actually earned. It's
a market snapshot, not a per-device payout figure.
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 and the creative itself is at least half visible on that monitor. A monitor whose ad is buried behind windows contributes nothing, no matter how much other wallpaper is exposed there — you can't earn by hiding the ad. The app relocates a covered ad into open space within about a minute, so honest desktops recover quickly. 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 a click bonus once per ad per device per UTC day, priced from the serving ad's gross bid:
click credit cents = round(50 × grossBidCents / 120)
That's CLICK_MULTIPLIER impression-minutes at the bid rate, halved to
your share. At the pre-auction legacy bid of 4¢ this is 2¢, matching the old
flat rate. At the minimum bid of 1¢ this rounds down to 0¢ — a click on the
cheapest possible ad earns nothing extra, which is intentional: the click
bonus scales with what the sponsor is actually paying. Anonymous QR scans
redirect but credit nothing.
The 50/50 split
Ledger amounts are your half of the ad revenue: the auction sets the gross price, and you're credited half of whatever the winning bid pays for your exposure. House ads (below) are the exception — PaperRoute funds those out of pocket at its own expense while real sponsor inventory is built.
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 2× the publisher's click credit (
ADVERTISER_GROSS_MULTIPLIER), once per ad per device per UTC day — so an ad bidding 4¢ debits 4¢ per click and credits the publisher 2¢, the legacy rate. - 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 — counting only monitors whose composited creative was itself at least half visible (a hidden ad zeroes that monitor's contribution) |
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. |
ad_bid_history |
Your ad's auction bid over time: ad_id, gross_bid_cents (cents per exposed-megapixel-hour, minimum 1), and effective_at. Append-only — creating or editing a bid inserts a new row, never updates or deletes one. The current bid is the row with the latest effective_at for that ad; the bid in effect at any past minute is the latest row at or before that minute. House ads have no rows here — they are never priced and never bid. |
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.