Inspiration
Streaming is the last big consumption medium still sold like an all-you-can-eat buffet. You pay $11.99/month flat, and your money gets thrown into a giant pool that's split by share of total platform streams — so a song you loved and a song you skipped pay out the same way, and most of your fee funds whoever is trending rather than who you actually listened to.
That's broken in two directions:
- Listeners overpay. The average listener plays ~3,278 songs/year ≈ 9,800 minutes. At a fair per-minute rate that's about $8/month — less than the flat fee. Light listeners subsidize heavy ones.
- Artists are paid by a pool, not by you. Per-stream payout is an opaque, ever-shrinking slice of a shared pot. There's no clean line from "I listened to this track for 4 minutes" to "this artist earned for those 4 minutes."
Usage-based billing already won cloud, APIs, and AI tokens. We wanted to apply that same metered-billing pattern to music: you pay for the minutes you actually listen, and artists get paid for the minutes you actually played.
What it does
TollRoad meters playback the way a utility meters electricity. Every minute streamed is a metered billing event attributed to one listener and one rightsholder.
- Listeners top up a prepaid balance, stream anything, and watch a live per-minute taximeter tick up as they play. Stop listening, stop paying. A hard stop-at-zero gate means you can never stream past your balance.
- Artists set their own per-track rate — anywhere from free to $1.00/min at 0.1¢ precision — and earn on real consumption, settled into an auditable royalty ledger and paid out via Stripe Connect.
- Labels / catalogs get the same engine as infrastructure — "Stripe for music royalties."
- AI agents are first-class clients. An MCP server turns the catalog into a programmatic Vibe DJ that discovers, pays (via an x402-style protocol), and streams on your behalf.
On top of the metering core we shipped a full product: wallet + Stripe top-ups, likes/playlists/library (liking a track costs one real minute — a money-backed signal), Superfan Bonds (every paid minute becomes a ranked, un-fakeable relationship with an artist), a Vibe Pad mood-tagging mini-game that pays out free minutes and produces an AI-tagging training set, vector-based discovery, and shareable artist/profile pages.
How we built it
The heart of the project is a polyglot CQRS architecture that uses two AWS databases, each for the grain it's best at:
- Amazon DynamoDB — the command / hot path. Holds the listener's authoritative real-time balance via a conditional
UpdateItemthat debits only whilebalance ≥ cost(so you physically can't stream past zero), plus the METER/TOPUP event firehose. Every metered minute is a singleTransactWriteItems— conditional debit + guarded event write — chosen for single-digit-ms conditional writes and the scale to handle ~16–23K writes/sec at a million concurrent streams. - Amazon Aurora DSQL — the query side / system of record. Serverless, scale-to-zero, Postgres-compatible. Holds the catalog, accounts, library, and the append-only
royalty_ledger(one immutable credit row per metered minute), plus precomputed per-artist/day summaries. Superfan bonds and leaderboards are derived in-SQL — no extra store. Kept out of the hot loop so it can scale to zero between projection runs. - AWS Lambda — the projector. The sole writer of the DSQL read models. It consumes DynamoDB Streams (at-least-once, duplicates expected) and builds the ledger idempotently (
UNIQUEkey +ON CONFLICT DO NOTHING) with OCC40001retries. - S3 + CloudFront + KMS for delivery: audio is SSE-KMS encrypted, served via Origin Access Control behind short-TTL (150s) signed URLs the API issues only after the meter authorizes the minute.
- Frontend on Vercel, built with v0 — the listener player with its live meter and the artist dashboard.
- Stripe for top-ups and Stripe Connect (Express) for artist payouts, fed by the DSQL ledger.
Streaming is gated by an x402-style protocol — request → 402 Payment Required → pay → retry — but crypto-free: settlement is the prepaid wallet, not a chain. That makes the whole thing API-first and agent-friendly out of the box.
Challenges we ran into
- Designing around Aurora DSQL's grain. DSQL isn't vanilla Postgres: no foreign keys, no triggers, no PL/pgSQL, optimistic concurrency (no row locks), a 128 MiB/query limit, and per-transaction caps. A mutable "running balance" row would have thrown
40001under load — so we made the ledger append-only and compute balances asSUM/summaries, which is both OCC-friendly and the correct accounting pattern. BI runs off precomputed summaries, never a heavy scan. - A real CQRS race we had to fix by construction. Originally the synchronous charge wrote the DSQL ledger itself, front-running the projector — so the projector always hit
ON CONFLICT DO NOTHINGand the rollup summaries never updated. The fix: the command path touches only DynamoDB, and the projector owns DSQL. No race, no dropped summary. - Sub-cent pricing. To represent a 0.5¢/min track, everything is stored in millicents (cents × 1000); Stripe stays in whole cents at the boundary and the remainder lives in the artist balance.
- Server-authoritative metering. Playback depends on the meter, not the reverse: no recent paid minute → HTTP 402, and the signed URL expires within 150s. You can't listen free, and you can't wash-stream to inflate earnings.
What we learned
- Polyglot persistence pays off when each database is matched to its access pattern — DynamoDB for high-velocity conditional writes, Aurora DSQL for relational reads and an auditable system of record — joined by a stream-driven projector.
- CQRS forces a clean ownership model: one writer per store removes whole classes of race conditions.
- Designing into a database's constraints (append-only ledgers, derived-in-SQL leaderboards, scale-to-zero) produces a cleaner system than fighting them.
What's next
A server-authoritative signed-heartbeat meter (the server issues a signed token chain and computes elapsed time itself) to fully close client-side forgery, deeper label/catalog onboarding, and expanding the agent/MCP ecosystem around the public /v1 API.
Built With
- amazon-api-gateway
- amazon-aurora-dsql
- amazon-bedrock-(titan-embeddings-v2)
- amazon-cloudfront
- amazon-dynamodb
- amazon-web-services
- aws-cdk
- aws-kms
- aws-lambda
- dynamodb-streams
- model-context-protocol-(mcp)
- next.js
- node.js
- postgresql
- stripe
- stripe-connect
- typescript
- v0
- vercel
- x402
Log in or sign up for Devpost to join the conversation.