paperroute_transparency

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 (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.