Skip to content

0x-pankaj/splitstream

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SplitStream

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.

Why it's different

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:

  1. 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)
  2. 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.
  3. 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.

Creators sign up and earn — no MetaMask, no seed phrase

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_ID for 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:setup generates + 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:demo runs a buyer agent paying a seller creator agent whose Circle wallet receives the USDC.

Proven live on Arc Testnet — real USDC, nothing mocked

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_X402 settles 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.

Live deployment

  • API (live on Arc Testnet): https://splitstream-api-production.up.railway.app — health: /health (onchainEnabled: true, allReal: true, circleWallets: true, chain 5042002); 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/web with NEXT_PUBLIC_API_URL pointed at the API above (and NEXT_PUBLIC_SITE_URL for 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"}'

Quick start (the demo path — no keys needed)

pnpm install

# Backend on :8787 (mirror mode — simulated settlement, always works)
pnpm --filter @arcane/server dev

# Storefront on :3000
pnpm --filter @arcane/web dev

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

Embed anywhere — the TipJar widget

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.

Try the API directly

# 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"}'

Register your own API for agents to pay per call (x402 / pay.sh-style)

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.

Authenticated upstreams — access for AI without KYC

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: bearerAuthorization: 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.

The real x402 flow (challenge → pay → prove → serve)

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 blocked

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

Who pays? (sponsored vs self-pay)

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

Real settlement on Arc (LIVE_X402)

With LIVE_X402=true + a funded relayer, the flow settles real USDC on Arc Testnet (chain 5042002):

  • Money inverifyArcUsdcPayment (services/x402Settle.ts) reads the Arc tx receipt and confirms a real USDC Transfer to 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 outpayContributorsOnArc has the relayer transfer each owner their computeSplit share 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.

The split engine (reuses a proven Arc settlement spine)

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.

The reading agent (agentic layer)

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_KEY is set, an LLM via OpenRouter (model deepseek/deepseek-v4-pro, override OPENROUTER_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.ts

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

Live proof on Arc Testnet (chain 5042002)

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):

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:split

See PHASE4_LIVE_PROOF.md for the funded prerequisites.

Configuration

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.

Tests

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 tests

Monorepo layout

splitstream/
├── 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

The Arc precision duality (important)

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.

Releases

Packages

Contributors

Languages