Pay-per-use for any API or piece of content, settled in real USDC on Arc — with the payment auto-split across every contributor's chain.
Like pay.sh's x402 pay-per-call, plus instant cross-chain revenue splitting and no-KYC access to authenticated APIs — proven live on Arc Testnet with real USDC.
A reader — or an AI agent — pays a few cents to unlock a single article, photo, or song. That payment is split instantly across every contributor, each paid on their chain (Base, Arbitrum, Ethereum), settling in under 500ms on Arc in real USDC. No subscription. No signup or KYC for the reader. No gas tokens. (Solana payouts route via CCTP — on the roadmap; the live default split is EVM-only so every leg settles real, with no skipped contributor.)
Built for the Lepton Agents Hackathon (Canteen × Circle × Arc). RFB: "Monetize a single article, photo, or song, without forcing readers into a monthly commitment." Traction metric: creators earning · total creator payouts.
Most pay-per-article demos pay one creator. SplitStream's headline is the one thing that's hard: a single $0.05 unlock fans out to the writer (Base), the editor (Arbitrum), and the photographer (Ethereum) — instantly in real USDC, no gas tokens, no payroll batch. It runs natively because the settlement engine underneath was built as a multi-recipient, cross-chain payout splitter.
Three things, all live and demoable with zero keys:
- Unlock → split. Pay per piece; the price fans out to every contributor on
their own chain via Arc Gateway (instant) / CCTP. (
POST /api/v1/pieces/:id/pay) - Storefront + live traction counter. A public page anyone can browse and pay from, with a running total-creator-payouts counter — the hackathon metric, front and center.
- AI reading agent. An autonomous agent reads the catalog, decides what's worth unlocking within a budget, and pays the creators per piece — agent-to- creator commerce, no human in the loop. Also exposed over MCP so external agents can discover and pay creators.
A real creator goes to /creator, signs in with their email + a one-time
code, and is instantly assigned a custodial USDC wallet on Arc via Circle
Developer-Controlled Wallets (pre-created pool → assigned on signup). That wallet
becomes their payout address: every reader payment splits into it, they watch real
on-chain earnings climb on their dashboard, and they withdraw any time. No
wallet install, no seed phrase, no KYC — the payment floor priced these creators
out, and this is how they get back in.
- Set
CIRCLE_API_KEY+CIRCLE_ENTITY_SECRET+CIRCLE_WALLET_SET_IDfor real custodial wallets; with zero keys a creator still gets a labeled local-dev wallet so the whole flow is demoable.EMAIL_API_KEY(Resend) sends the login code; without it the code prints to the server console. - One-shot setup:
CIRCLE_API_KEY=TEST_API_KEY:… pnpm --filter @arcane/server circle:setupgenerates + registers the entity secret (saves a recovery file to~/.circle) and creates the wallet set, printing the three vars to drop into.env/ Railway. Creators then auto-provision a real Arc wallet on signup. - A Circle wallet is an Arc EVM address that drops straight into the existing contributor split — so creator payouts ride the same proven settlement engine.
- Agent-to-agent:
pnpm --filter @arcane/server a2a:demoruns a buyer agent paying a seller creator agent whose Circle wallet receives the USDC.
The full x402 loop has been run end-to-end on Arc Testnet (chain 5042002)
with real devnet USDC — an autonomous agent paid, the payment was verified
on-chain, and the API owner was paid real USDC. Reproduce it with
bun run apps/server/scripts/x402-live-loop.ts (server in LIVE_X402 mode).
| Leg | Real Arc Testnet tx |
|---|---|
| Fund the buyer agent wallet | 0xfe6d4349…f7849ebf |
| Agent pays for the call (USDC → payTo) | 0xdf1ff4e1…ea8e5042 |
| Owner paid the split (real USDC) | 0x581fa957…e9457f10d |
The agent held its own wallet and signed every payment itself — autonomous,
no human in the loop. Owner USDC balance went 0 → 0.01 on-chain.
Two run modes.
LIVE_X402settles real USDC on Arc as above (needs a funded relayer). The default mirror mode keeps a keyless, zero-setup path (single-use-nonce gated, simulated settlement) so anyone can demo the product instantly. The protocol, split math, credential injection, and live upstream API calls are real in both modes; only the settlement is simulated in mirror.
- API (live on Arc Testnet): https://splitstream-api-production.up.railway.app
— health:
/health(onchainEnabled: true,allReal: true,circleWallets: true, chain5042002); catalog:/api/v1/pieces. Runs in live Arc mode — every buy path (walletless sponsored unlock, pay-from- your-wallet, agent x402, and the reading agent) settles real USDC on Arc, and creators get live Circle custodial wallets they can withdraw from. - Storefront: https://splitstream-web.vercel.app — deployed on Vercel from
apps/webwithNEXT_PUBLIC_API_URLpointed at the API above (andNEXT_PUBLIC_SITE_URLfor absolute share/OG URLs). Shared/piece/<id>links render rich social cards, and any piece is embeddable on third-party sites via the one-line TipJar widget (/widget.js). - Source: https://github.com/0x-pankaj/splitstream
Every localhost:8787 example below also works against the live API — just swap
the host. For example:
curl -X POST https://splitstream-api-production.up.railway.app/api/v1/pieces/piece-arc-frontier-001/pay \
-H 'content-type: application/json' -d '{"payer":"demo"}'pnpm install
# Backend on :8787 (mirror mode — simulated settlement, always works)
pnpm --filter @arcane/server dev
# Storefront on :3000
pnpm --filter @arcane/web devOpen http://localhost:3000:
- The live traction counter (total creators paid, unlocks, chains).
- Unlock for $0.05 on a piece → watch the cross-chain fan-out reveal (each creator, their chain, tx hash, latency).
- "Let the agent read & pay" → the AI agent autonomously unlocks and pays.
Shareable single-piece link: /piece/<id>. The old B2B treasury console (this
project's origin) still lives at /dashboard.
Any creator can monetize a piece on their own site (blog, portfolio, Substack) with a single tag — readers unlock & pay every contributor without leaving the host page, walletless:
<script src="https://YOUR-SITE/widget.js" data-piece="piece-arc-frontier-001"></script>/widget.js replaces itself with a compact, auto-resizing iframe (/embed/<id>)
that runs the relayer-sponsored unlock and fans the split out on Arc. Optional
attributes: data-width, data-base. Every piece page shows a one-click
copy-paste snippet for its own widget.
# Unlock a piece — splits across contributors on their chains
curl -X POST localhost:8787/api/v1/pieces/piece-arc-frontier-001/pay \
-H 'content-type: application/json' -d '{"payer":"demo"}'
# Let the reading agent read & pay creators autonomously
curl -X POST localhost:8787/trpc/agent.read -H 'content-type: application/json' \
-d '{"interests":["stablecoin","arc"],"maxUnlocks":5,"budgetUSDC":"0.50"}'A piece can be a paid API service (kind: "api"): you register your app's
endpoint with a per-call price and a revenue split; an AI agent discovers it,
pays the sub-cent price on Arc, and the platform proxies one call to your
endpoint and returns the response. No API keys to issue, no billing setup —
stablecoin pay-per-call.
# SELLER: register your API (price + upstream endpoint + who gets paid)
curl -X POST localhost:8787/api/v1/pieces -H 'content-type: application/json' \
-H 'x-api-key: arc_test_sk_demo_0001' \
-d '{"title":"My App: FX Rates","kind":"api","priceUSDC":"0.01",
"endpoint":"https://api.frankfurter.app/latest?from=USD&to=EUR","httpMethod":"GET",
"contributors":[{"role":"api owner","address":"0x4444…","targetChain":"base","splitBps":10000}]}'
# AGENT (or anyone): pay $0.01 and get the live upstream result back
curl -X POST localhost:8787/api/v1/pieces/piece-fx-api-001/call \
-H 'content-type: application/json' -d '{"payer":"agent-bob"}'
# → { "unlock": {…paid+split…}, "upstream": { "ok": true, "status": 200, "body": { "rates": { "EUR": 0.86 } } } }Sellers can do this from the UI too — /publish registers either a content
piece or a paid API. Agents discover and pay via the MCP tools list_pieces +
call_api. The payment still splits across chains, so an API with multiple
owners is paid out automatically.
Most real APIs need a key. The seller stores that credential once with SplitStream; the platform injects it on the proxied call. The paying agent gets the API's response but never sees the key — so an AI agent buys access to an authenticated API with no account and no KYC of its own. SplitStream is the account; the agent just pays.
# SELLER registers an authenticated API (bearer | custom header | query param)
curl -X POST localhost:8787/api/v1/pieces -H 'content-type: application/json' \
-H 'x-api-key: arc_test_sk_demo_0001' \
-d '{"title":"Premium Data API","kind":"api","priceUSDC":"0.02",
"endpoint":"https://api.yourapp.com/v1/data","httpMethod":"GET",
"auth":{"type":"bearer","secret":"sk_live_…"},
"contributors":[{"role":"owner","address":"0x…","targetChain":"base","splitBps":10000}]}'The secret is write-only: it's never returned by pieces.list, pieces.get,
or the call response — only authenticated: true and authType are disclosed.
(Auth types: bearer → Authorization: Bearer <secret>, header → a custom
header, query → a URL parameter.)
Production note: secrets are stored in the engine's store (in-memory / SQLite) for the hackathon. A real deployment should encrypt them at rest in a secrets vault with rotation — the injection point (
callPaidService) stays the same.
POST /api/v1/pieces/:id/call speaks the x402 "402 Payment Required"
protocol, so any x402-native agent pays SplitStream the standard way — no custom
client. SplitStream is the facilitator: it issues the challenge, verifies the
payment, settles the cross-chain split to the API's owners, injects the upstream
credential, and proxies the call.
1. POST /pieces/:id/call → 402 Payment Required
{ x402Version, accepts: [{ scheme:"exact", network:"arc-testnet",
maxAmountRequired, payTo, asset:<USDC on Arc>, nonce, … }] }
2. (agent pays the USDC amount on Arc)
3. POST /pieces/:id/call + X-PAYMENT: <base64 proof carrying the nonce>
→ 200 OK
X-PAYMENT-RESPONSE: <base64 { success, transaction, network, payer }>
body: { paid:true, unlock:{…split…}, upstream:{ ok, status, body } }
The issued nonce is single-use — replaying a proof is rejected (402), so a paid call can't be reused. Try the whole handshake (server up on :8787):
pnpm --filter @arcane/server x402:call # → 402 → pay → 200 + result → replay blockedThe one-click web button (pieces.callApi) is a bundled convenience path over
the same engine; the x402 HTTP endpoint is the interoperable, agent-facing one.
There are two payer models, and the split payout to contributors always comes
from the platform relayer (0x8984…7154c, RELAYER_PRIVATE_KEY) — only the
price-in differs:
| Buyer | Who pays the price in | Sponsored by us? |
|---|---|---|
| Human, default "Unlock for $X" (walletless) | platform relayer (via a relayer-funded demo agent) | ✅ yes |
Agent via our MCP tools / in-app reading agent / pieces.callApi |
same relayer-funded demo agent | ✅ yes |
| Human, "pay from your own wallet" button | the reader's own wallet (real USDC on Arc) | ❌ self-pay |
External agent hitting raw POST /pieces/:id/call with X-PAYMENT |
the agent's own wallet — it submits a real on-chain payment proof | ❌ self-pay (x402) |
In short: a CLI/MCP agent that uses our tools is sponsored (we cover it from
the relayer); a true x402 agent pays itself via the /call challenge. Because
sponsored buys spend the relayer's USDC, keep it topped up at
faucet.circle.com (Arc Testnet) — the
RELAYER_ALERT_WEBHOOK pings you when it's low. (The relayer is also the payTo
where every price lands before the split fans out.)
With LIVE_X402=true + a funded relayer, the flow settles real USDC on Arc
Testnet (chain 5042002):
- Money in —
verifyArcUsdcPayment(services/x402Settle.ts) reads the Arc tx receipt and confirms a real USDCTransferto the platform payTo for ≥ the price before anything is served; the tx hash is redeemed single-use. (Verified live against Arc RPC — a nonexistent tx is correctly rejected.) - Money out —
payContributorsOnArchas the relayertransfereach owner theircomputeSplitshare in real USDC on Arc; the response returns real Arc tx hashes you can open on the explorer.
Mirror mode (no LIVE_X402) keeps the nonce-gated simulated path so the demo is
always keyless. Full funded walkthrough: LIVE_X402_RUNBOOK.md.
A piece payment is a bulk payout — the contributors are the recipients, their
splitBps are the amounts. So payForPiece builds a BulkPayoutInput and calls
the proven processBulkPayout, getting cross-chain routing, compliance, the
audit log, and Arc settlement for free.
| Instant path | Whale path | |
|---|---|---|
| Range | < $5,000 (per-piece is always tiny → here) | ≥ $5,000 |
| Rail | Solver mesh → Circle Gateway (unified balance) | Native CCTP V2 |
| Latency | < 500 ms | ~1–15 min |
| Settlement | reimbursed on Arc via executeIntent |
canonical burn / mint |
computeSplit distributes the price by basis points and assigns the rounding
dust to the largest share, so contributor shares always sum to the price exactly.
apps/server/src/services/readingAgent.ts decides which pieces to unlock and
pays creators within a budget and unlock ceiling (enforced in code; the model
only chooses). Two modes:
- heuristic (default) — deterministic interest-keyword scoring. Always runs.
- llm — when
OPENROUTER_API_KEYis set, an LLM via OpenRouter (modeldeepseek/deepseek-v4-pro, overrideOPENROUTER_MODEL) ranks the catalog and explains each choice. Falls back to the heuristic on any error.
MCP tools for external agents:
claude mcp add splitstream -- bun run apps/server/src/mcp/stdio.tslist_pieces, pay_for_piece (+ the inherited treasury tools). Any agent can
discover a piece and pay its creators, bounded by optional on-chain spend caps.
SplitStream rides the parent engine's already-deployed, already-proven rails:
| Contract | Address |
|---|---|
| ArcaneComplianceGuard | 0xf9E0117e2506182690e009B9dB78456DE270368f |
| ArcaneTreasuryVault | 0x72dC5bFeb7f12c36ACac9A8FE7986dB656e7fAF5 |
Real cross-chain settlement, proven Arc → Base Sepolia via CCTP V2 + Circle's Forwarding Service (no destination gas, no pre-funded liquidity):
- burn on Arc:
0x2a7c97cb… - mint on Base:
0x1105e368…
Capture a live split (one command):
# mirror (keyless) — proves the flow:
pnpm --filter @arcane/server prove:split
# live Arc settlement:
cp apps/server/.env.live apps/server/.env # funded relayer + vault
pnpm --filter @arcane/server prove:splitSee PHASE4_LIVE_PROOF.md for the funded prerequisites.
apps/server/.env (gitignored) is the demo default: mirror mode +
OPENROUTER_API_KEY (so the agent runs in llm mode). Live Arc creds live in
apps/server/.env.live. Never commit keys.
pnpm --filter @arcane/server test # Vitest — 85 tests (split, agent, creator,
# wallets, x402, recovery, snapshot, MCP)
pnpm typecheck # tsc across the workspace
cd contracts && forge test # Foundry — 28 testssplitstream/
├── packages/shared/ Arc chain def + verified addresses, Zod schemas, the
│ 6/18 decimal-duality utils, routing/fees, splits.ts
├── contracts/ Foundry — vault + compliance guard (live on Arc), 28 tests
└── apps/
├── server/ Bun + Hono + tRPC — split engine, reading agent, Circle
│ custodial wallets, hybrid router (Gateway/CCTP), MCP, scripts
└── web/ Next.js storefront (/), /piece/[id] (+ OG cards), /creator,
/publish, /dashboard, embeddable /embed/[id] + /widget.js
USDC on Arc is one balance with two interfaces: 18 decimals native (gas) and
6 decimals ERC-20. All value math uses 6dp base units (bigint); conversions
go through @arcane/shared/decimals. Never mix the two precisions.
Note: internal package names are still @arcane/* (the proven engine's
namespace). Renaming to @splitstream/* is cosmetic and deferred.