"Every second a track plays, every hand that made it gets paid."
Provenance Pay is a real-time music royalty micropayment system that bridges open music credit metadata directly into gas-free on-chain USDC payments — triggered second-by-second as a track plays.
The music industry has a 100-year-old attribution problem. Composers, producers, session musicians, and engineers are credited in open databases like MusicBrainz, but royalty collection is opaque, slow, and riddled with middlemen. Provenance Pay solves this by:
- Reading the full contributor graph from MusicBrainz (the world's richest open music credits DB)
- Registering split percentages immutably on-chain via the
ProvenanceRegistrycontract on Arc Testnet - Firing Circle Nanopayments to every contributor wallet simultaneously, in real time, as the track plays
- Architecture
- System Components
- Data Flow
- Smart Contract
- Sidecar Agent API
- Database Schema
- Split Calculation
- Circle Stack Integration
- Project Structure
- Environment Variables
- Running Locally
- Deployment
┌─────────────────────────────────────────────────────────────────────┐
│ BROWSER (Next.js 15) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ Dashboard │ │ Provenance │ │ Library / │ │ Claim │ │
│ │ Live Ticks │ │ Graph (D3) │ │ Upload │ │ Portal │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └───┬────┘ │
│ │ │ │ │ │
└─────────┼─────────────────┼──────────────────┼──────────────┼────────┘
│ │ HTTP / SSE │ │
│ │ │ │
┌─────────▼─────────────────▼───────────────────▼──────────────▼────────┐
│ NEXT.JS API ROUTES (port 3000) │
│ │
│ /api/track/[id]/provenance /api/escrow/claim /api/config │
│ /api/library/tracks /api/wallets /api/metrics │
└─────────────────────────────────┬──────────────────────────────────────┘
│ IPC / HTTP (localhost)
│
┌─────────────────────────────────▼──────────────────────────────────────┐
│ PROVENANCE AGENT SIDECAR (Node.js, port 3001) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────┐│
│ │ mb-graph- │ │ payment- │ │ wallet- ││
│ │ builder.js │ │ dispatcher.js │ │ provisioner.js ││
│ │ │ │ │ │ ││
│ │ • Fetch MBID │ │ • EIP-3009 auth │ │ • Circle SDK ││
│ │ from MB API │ │ • Circle Gateway │ │ • Create EOA ││
│ │ • Build split │ │ • Auto-fund gas │ │ • Escrow flag ││
│ │ graph │ │ • Poll tx hash │ │ • SQLite upsert ││
│ │ • Normalize │ │ • SSE fan-out │ │ ││
│ │ contributor │ │ │ │ ││
│ │ roles │ │ │ │ ││
│ └────────┬─────────┘ └─────────┬─────────┘ └─────────┬───────────┘│
│ │ │ │ │
│ ┌────────▼───────────────────────▼────────────────────────▼───────────┐│
│ │ SQLite (data/provenance.db) ││
│ │ contributors · provenance_graphs · payment_ticks · corrections ││
│ └─────────────────────────────────────────────────────────────────────┘│
│ │ │
│ ┌────────────────────┼──────────────────────┐ │
│ │ index.js │ │ │
│ │ POST /webhook │ GET /api/payments/ │ │
│ │ • Play-gate timer │ stream (SSE) │ │
│ │ • Stop/Skip │ • tick events │ │
│ │ • Graph ready evt │ • gate_cleared │ │
│ └────────────────────┘──────────────────────┘ │
└─────────────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────────────┴────────────────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ MUSICBRAINZ │ │ CIRCLE DEVELOPER │
│ REST API │ │ CONTROLLED WALLETS │
│ │ │ (MPC, ARC-TESTNET) │
│ • /ws/2/recording │ │ │
│ /{mbid}?inc= │ │ • createTransaction() │
│ artists+rels │ │ • createWallets() │
│ • Open music │ │ • getWalletBalance() │
│ credit graph │ │ │
└───────────────────┘ └─────────┬─────────────┘
│
┌─────────▼─────────────┐
│ ARC TESTNET │
│ Chain ID: 5042002 │
│ │
│ ProvenanceRegistry │
│ 0x43c878be9d3d55e8a │
│ 5fa8e6ddd05c97df751 │
│ 3004 │
│ │
│ Malachite BFT │
│ Sub-second finality │
│ USDC as gas token │
└───────────────────────┘
The interactive web application with 8 main sections:
| Route | Description |
|---|---|
/ |
Landing page with hero, features, FAQ |
/dashboard |
Live payment dashboard with animated USDC counters and area charts |
/graph |
D3.js force-directed provenance graph, animates during active playback |
/library |
Track library — browse and trigger playback for any seeded/uploaded track |
/upload |
Upload a track and associate it with a MusicBrainz MBID |
/wallets |
View Circle Agent Wallets (escrowed) and Developer-Controlled Wallets |
/transactions |
Full payment history with Arc Testnet tx hashes |
/settings |
Configure sidecar runtime parameters (play-gate, rate, batch interval) |
/contributor/[walletAddress] |
Per-contributor earnings breakdown |
/claim |
Claim portal for unregistered contributors to withdraw escrowed USDC |
Key frontend tech:
- Next.js 15 with App Router, React 19
- D3.js v7 — force-directed graph simulation with real-time node pulsing
- Recharts — live area chart for earnings over time
- Framer Motion — micro-animations on ticks and state transitions
- Tailwind CSS v4 + custom CSS animations
- Sonner — toast notifications for payment events
- SSE (EventSource) — subscribes to
GET /api/payments/streamon the sidecar
A standalone Node.js + Express server that acts as the autonomous payment agent. It:
- Receives playback webhooks (
POST /webhook) - Builds and caches provenance graphs from MusicBrainz
- Reads/writes the
ProvenanceRegistrycontract on Arc Testnet via viem - Dispatches USDC payments via Circle Developer-Controlled Wallets API
- Fans out real-time events via Server-Sent Events
Core modules:
| File | Responsibility |
|---|---|
sidecar/index.js |
Express server, webhook handler, play-gate timer, SSE broadcaster, contract interaction |
sidecar/mb-graph-builder.js |
MusicBrainz API client, contributor graph construction, split normalization |
sidecar/payment-dispatcher.js |
Circle SDK payment dispatch, token ID resolution, auto-gas-funding, DB logging |
sidecar/wallet-provisioner.js |
Circle EOA wallet creation for unregistered contributors, escrow tagging |
sidecar/db/index.js |
SQLite schema initialization and seed data |
An EVM smart contract providing write-once, immutable royalty split storage keyed by MusicBrainz Recording ID.
Deployed at: 0x43c878be9d3d55e8a5fa8e6ddd05c97df7513004
Network: Arc Testnet (Chain ID: 5042002)
Deployment tx: 0xa8c1f7bc36a9f155fc2c0086fa118bebfd9fa46eeffdb825f975ba2da4830492
Media Player / Navidrome
│
│ POST /webhook { event:"play", mbid:"<mbid>" }
▼
Sidecar index.js
│
├── 1. buildProvenanceGraph(mbid)
│ │
│ ├── Check SQLite cache
│ │ └── [CACHE HIT] → return cached graph
│ │
│ └── [CACHE MISS] → fetch MusicBrainz API
│ GET /ws/2/recording/{mbid}?inc=artists+artist-rels+work-rels
│ │
│ ├── Extract artist-credit[] → role: "artist"
│ ├── Extract relations[] → normalize roles:
│ │ mix/* → "mixer"
│ │ produce/* → "producer"
│ │ compose/* → "composer"
│ │ vocal/* → "featured_artist"
│ │ instrument/* → "session_musician"
│ │
│ ├── computeSplits() → basis points (sum = 10000)
│ │ artist: 35%, producer: 25%, featured: 15%
│ │ composer: 10%, mixer: 8%, session: 7%
│ │ (normalized proportionally per credited roles)
│ │
│ └── Cache graph in SQLite
│
├── 2. emit('graph_ready') → SSE → Dashboard graph animates
│
├── 3. Play-Gate Timer (15 seconds)
│ │
│ └── [SKIP/STOP received] → clearTimeout → NO PAYMENT
│
└── 4. [Gate Cleared] → setInterval every 15s
│
└── dispatcher.dispatchTick(graph, 0.0001 USDC/sec)
│
├── For each split:
│ ├── resolveWallet(contributorMbid)
│ │ ├── [Known] → SQLite lookup → address
│ │ └── [Unknown] → Circle SDK createWallets()
│ │ → tag as ESCROW → SQLite
│ │
│ └── client.createTransaction({
│ walletId: PLATFORM_WALLET_ID,
│ tokenId: USDC_TOKEN_ID,
│ destinationAddress: contributor.address,
│ amounts: [(totalTick * bps/10000).toFixed(6)]
│ })
│ → Poll for txHash
│ → INSERT into payment_ticks
│
└── emit('tick', confirmations) → SSE → UI updates
Contributor visits /claim → enters wallet address
│
▼
GET /api/escrow/balance?address={addr}
│
└── SQLite: SELECT SUM(amount_usdc) WHERE contributor wallet matches
→ Returns claimable balance
│
▼
POST /api/escrow/claim { address, signature }
│
└── Verify signature
└── Circle SDK: transfer from escrow wallet → contributor address
└── Update SQLite: mark as claimed
// Write-once per MBID. Sum of bps must equal 10000 (100.00%).
function register(
bytes32 mbid, // bytes32-encoded UTF-8 MusicBrainz Recording ID
address[] calldata wallets, // contributor wallet addresses
uint16[] calldata bps, // basis points (e.g. 5000 = 50%)
string[] calldata roles // human-readable role labels
) external;
// Read splits for payment dispatch
function getSplits(bytes32 mbid) external view returns (Split[] memory);
// Check if already registered (write-once guard)
function isRegistered(bytes32 mbid) external view returns (bool);
// Propose a correction (doesn't overwrite — creates on-chain audit trail)
function proposeCorrection(
bytes32 mbid,
address[] calldata newWallets,
uint16[] calldata newBps,
string[] calldata newRoles,
string calldata reason
) external;Key design decisions:
- Write-once: Once an MBID is registered, its split cannot be mutated — preventing royalty manipulation
- Correction proposal: Disputes create an on-chain audit trail via
CorrectionProposedevents without changing the canonical split - bytes32 MBID encoding: UUIDs are stored as left-padded bytes32 for gas efficiency
- Basis points: All percentages stored as
uint16out of 10000 for precision without floats
All endpoints served by the Node.js sidecar at http://localhost:3001.
| Method | Endpoint | Body | Description |
|---|---|---|---|
POST |
/webhook |
{ event, mbid } |
Trigger play / stop / skip |
POST |
/api/simulate-play |
{ mbid } |
Manually simulate playback (demo use) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/payments/stream?trackId={mbid} |
SSE stream of payment ticks |
GET |
/api/track/{mbid}/provenance |
Full provenance graph JSON |
GET |
/api/metrics |
Aggregate payment stats |
GET |
/api/transactions |
Full payment history |
GET |
/api/contributors |
All known contributors |
GET |
/api/wallets |
All Circle wallets (with escrow flag) |
GET |
/api/library |
Track library (seeded + uploaded) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/config |
Update runtime config (play-gate, rate, batch interval) |
GET |
/api/config |
Read current sidecar configuration |
{ "type": "graph_ready", "graph": { ... } }
{ "type": "gate_cleared", "trackId": "..." }
{ "type": "tick", "confirmations": [ { "contributorMbid", "amount", "txRef", "status" } ] }
{ "type": "stopped", "trackId": "..." }SQLite database at app/data/provenance.db (auto-created on first run).
-- All known music contributors
CREATE TABLE contributors (
id TEXT PRIMARY KEY, -- UUID
mbid TEXT UNIQUE NOT NULL, -- MusicBrainz Artist ID
name TEXT NOT NULL,
wallet_address TEXT, -- Circle wallet address on Arc Testnet
wallet_id TEXT, -- Circle internal wallet ID
is_provisioned INTEGER DEFAULT 0, -- 1 if Circle wallet created
is_escrow INTEGER DEFAULT 0, -- 1 if unregistered (funds held)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Cached provenance graphs (MusicBrainz + computed splits)
CREATE TABLE provenance_graphs (
mbid TEXT PRIMARY KEY, -- MusicBrainz Recording ID
graph_json TEXT NOT NULL, -- Full graph JSON blob
arc_tx_hash TEXT, -- Arc Testnet registration tx
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Every individual payment dispatched
CREATE TABLE payment_ticks (
id TEXT PRIMARY KEY,
track_mbid TEXT NOT NULL,
contributor_mbid TEXT NOT NULL,
amount_usdc TEXT NOT NULL, -- Decimal string (6 places)
nanopay_ref TEXT, -- Circle transaction ID
arc_batch_hash TEXT, -- Arc Testnet tx hash
tick_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- On-chain correction proposals
CREATE TABLE corrections (
id TEXT PRIMARY KEY,
track_mbid TEXT NOT NULL,
correction_json TEXT NOT NULL,
arc_tx_hash TEXT,
reason TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);The sidecar computes royalty splits using a weighted role system normalized to exactly 10,000 basis points (100.00%):
| Role | Default Weight | Notes |
|---|---|---|
artist |
35% | Primary credited artist |
producer |
25% | Track producer |
featured_artist |
15% | Featuring vocalist |
composer |
10% | Songwriter / composer |
mixer |
8% | Mix engineer |
session_musician |
7% | Instrumentalist |
Normalization logic:
- Each contributor's weight is looked up by role
- All weights are summed
- Each contributor's final
bps = floor(weight / total * 10000) - Rounding remainders are allocated to the highest-weighted contributor
- Resulting splits always sum to exactly
10000
Once computed, splits are registered to the ProvenanceRegistry contract — permanently immutable for that MBID.
Provenance Pay uses all 7 layers of the Circle Agent Stack:
| Layer | How it's used |
|---|---|
| Arc Testnet | EVM settlement layer for USDC transfers and ProvenanceRegistry contract. Chain ID 5042002. Malachite BFT sub-second finality. |
| Circle Nanopayments (Gateway) | createTransaction() via Developer Controlled Wallets API — processes gas-free USDC transfers that settle on Arc Testnet in batches |
| x402 Protocol | x402 payment gating on stream endpoints (x402StreamGating config flag) |
| Agent Wallets | Auto-provisioned EOA wallets on ARC-TESTNET for unregistered contributors. Funds escrow safely. |
| Circle CLI | Used during setup to register entity secret, configure wallet sets, and set spending policies |
| Developer-Controlled Wallets (MPC) | Platform wallet (sender) and registered contributor wallets managed via @circle-fin/developer-controlled-wallets SDK |
| Circle Contracts | ProvenanceRegistry.sol deployed on Arc Testnet for tamper-proof royalty split storage |
lepton/
│
├── README.md ← You are here
├── provenance-pay-prd.md ← Full Product Requirements Document
├── .env ← Root env (shared with sidecar)
│
├── contracts/ ← Foundry smart contract project
│ ├── src/
│ │ └── ProvenanceRegistry.sol ← Main registry contract
│ ├── script/
│ │ └── Deploy.s.sol ← Foundry deployment script
│ ├── test/ ← Contract unit tests
│ ├── broadcast/ ← Deployment receipts (Arc Testnet)
│ └── foundry.toml ← Foundry config (Arc Testnet RPC)
│
├── app/ ← Next.js app + Node.js sidecar
│ ├── .env ← App-level env (same values as root)
│ ├── package.json ← Dependencies + npm scripts
│ │
│ ├── sidecar/ ← Node.js provenance agent
│ │ ├── index.js ← Express server, webhook, SSE, contract writes
│ │ ├── mb-graph-builder.js ← MusicBrainz API + split computation
│ │ ├── payment-dispatcher.js ← Circle SDK payment dispatch
│ │ ├── wallet-provisioner.js ← Circle wallet creation for unknown contributors
│ │ └── db/
│ │ └── index.js ← SQLite schema + seed data
│ │
│ └── app/ ← Next.js App Router
│ ├── layout.tsx ← Root layout, fonts, metadata
│ ├── page.tsx ← Landing page
│ ├── globals.css ← Global styles + CSS animations
│ ├── providers.tsx ← React context providers
│ │
│ ├── components/
│ │ ├── AppShell.tsx ← Sidebar navigation shell
│ │ ├── ProvenanceGraph.tsx ← D3 force-directed graph component
│ │ ├── LiveCounters.tsx ← Animated USDC ticker component
│ │ ├── landing/ ← Landing page sections
│ │ │ ├── Hero.tsx
│ │ │ ├── Features.tsx
│ │ │ ├── Ecosystem.tsx
│ │ │ ├── FAQ.tsx
│ │ │ ├── Navbar.tsx
│ │ │ └── Footer.tsx
│ │ └── ui/ ← Reusable UI primitives
│ │
│ ├── context/
│ │ └── PlaybackContext.tsx ← Global playback state
│ │
│ ├── claim/
│ │ └── page.tsx ← Public claim portal (no auth required)
│ │
│ └── (app)/ ← Authenticated app shell routes
│ ├── layout.tsx ← App layout with sidebar
│ ├── dashboard/page.tsx ← Live payment dashboard
│ ├── graph/page.tsx ← D3 provenance graph visualizer
│ ├── library/page.tsx ← Track library
│ ├── upload/page.tsx ← Track upload + MBID association
│ ├── wallets/page.tsx ← Wallet management
│ ├── transactions/page.tsx ← Payment history
│ ├── settings/page.tsx ← Sidecar runtime config
│ └── contributor/
│ └── [walletAddress]/page.tsx ← Contributor detail
Both .env files (root and app/) should have the same values:
# ── Circle API ────────────────────────────────────────────────────────────────
CIRCLE_API_KEY=TEST_API_KEY:... # Circle Developer Portal API key
CIRCLE_ENTITY_SECRET=... # 32-byte hex entity secret (registered via CLI)
CIRCLE_WALLET_SET_ID=... # Circle wallet set UUID
# ── Platform Wallet ───────────────────────────────────────────────────────────
PLATFORM_WALLET_ADDRESS=0x... # On-chain address (Arc Testnet)
PLATFORM_WALLET_ID=... # Circle internal wallet UUID (sender)
# ── Arc Testnet ───────────────────────────────────────────────────────────────
ARC_CHAIN_ID=5042002
ARC_RPC_URL=https://rpc.testnet.arc.network
# ── Smart Contract ────────────────────────────────────────────────────────────
PROVENANCE_REGISTRY_ADDRESS=0x43c878be9d3d55e8a5fa8e6ddd05c97df7513004
DEPLOYER_PRIVATE_KEY=0x... # Private key for contract writes
# ── Sidecar Agent ─────────────────────────────────────────────────────────────
SIDECAR_PORT=3001
NAVIDROME_URL=http://localhost:4533 # Optional: media server
NAVIDROME_WEBHOOK_SECRET=... # Optional: webhook HMAC secret
# ── MusicBrainz (rate-limit compliance) ───────────────────────────────────────
MUSICBRAINZ_APP_NAME=ProvenancePay
MUSICBRAINZ_APP_VERSION=1.0
MUSICBRAINZ_CONTACT=your@email.com- Node.js 20+
- npm 10+
# Root-level scripts
npm install
# Next.js app + sidecar
npm --prefix app install# From the repo root — starts Next.js (3000) + Sidecar (3001) concurrently
cd app
npm run dev:allOr run each service separately:
# Terminal 1 — Next.js frontend
cd app && npm run dev
# Terminal 2 — Node.js sidecar agent
cd app && npm run sidecar| Service | URL |
|---|---|
| Next.js Frontend | http://localhost:3000 |
| Provenance Agent Sidecar | http://localhost:3001 |
The DB is pre-seeded with 3 tracks. Navigate to Library and click Play on any track. The dashboard will show live USDC ticks after the 15-second play gate clears.
Or trigger via curl:
curl -X POST http://localhost:3001/webhook \
-H "Content-Type: application/json" \
-d '{"event":"play","mbid":"f980fc14-e29b-481d-ad3a-5ed9b4ab6340"}'cd contracts
# Build
forge build
# Deploy to Arc Testnet
forge script script/Deploy.s.sol:Deploy \
--rpc-url https://rpc.testnet.arc.network \
--broadcast \
--legacy
# Verify on Arc block explorer
forge verify-contract \
<DEPLOYED_ADDRESS> \
src/ProvenanceRegistry.sol:ProvenanceRegistry \
--rpc-url https://rpc.testnet.arc.networkAlready deployed: 0x43c878be9d3d55e8a5fa8e6ddd05c97df7513004 on Arc Testnet (Chain ID: 5042002)
cd app
npx vercel --prodSet all environment variables in Vercel dashboard (same as .env).
The sidecar is a standalone Express server. Deploy app/sidecar/ with:
node --env-file=.env sidecar/index.jsSet SIDECAR_PORT and all Circle + Arc env vars in your hosting provider.
| Property | Value |
|---|---|
| Network Name | Arc Testnet |
| Chain ID | 5042002 |
| RPC URL | https://rpc.testnet.arc.network |
| Native Gas Token | USDC |
| Consensus | Malachite BFT |
| Block Explorer | https://explorer.testnet.arc.network |
| ProvenanceRegistry | 0x43c878be9d3d55e8a5fa8e6ddd05c97df7513004 |
| Deployment Tx | 0xa8c1f7bc36a9f155fc2c0086fa118bebfd9fa46eeffdb825f975ba2da4830492 |
MIT © 2026 Nithish Jude
Built with ❤️ at Lepton Hackathon · Powered by Circle · Settled on Arc Testnet