BitTorrent distributes bytes better than any CDN and has never been a product: nobody is paid to seed, and nothing in it is private. Every Profullstack media property answers that with a central HTTP proxy and a pass system bolted on, and pays for every byte it serves. OpenSwarm is an add-on to BitTorrent, carried as BEP 10 extension messages, that fixes both. The swarm carries AES-256-CTR ciphertext whose integrity is pinned by two SHA-256 merkle roots in a signed manifest, so a tracker or DHT node learns an infohash and a size. A leecher buys a pass over x402 in USDC (the same exchange x402-gateway runs for crawl passes, settled by CoinPay), a seeder serves inside a bounded credit window, and the leecher signs a cumulative voucher for every verified batch. Whoever seeds gets paid. Vanilla clients remain valid members and browsers remain first-class peers. Adding a file mints a key pair for it, derived from one publisher seed by default so there is one thing to back up: the public half is the file's identity and its BEP 46 key, the private half signs the manifest and authorises grants and payout changes, and a separate content key encrypts the bytes and is sealed to paying peers. The family: a core (records, keys, hashing, transports, discovery, events), ipfile, ippay, ipdb (a signed hash-chained catalogue with heads on the DHT), ipaudio, ipvideo, iplive (paid relays with backpressure) and ipname (Moshpit pins and DNS TXT). Plus the c0mpute.com integration with seven workload types and thirteen use cases, a proposed ip CLI, conformance profiles, a security model and an FAQ. Registered on the site the way OpenCreds is: nav entry, docs registry, sitemap, a /openswarm landing page, PRD 0005, and the MCP prd_next_id expectation moved to 0006. Specs only. No code, no schemas, no reference implementation. Claude-Session: https://claude.ai/code/session_01YafYxayh7Gqe5MWNNQMev2 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
17 KiB
iplive: Live Streams over Paid Peers
Version: 0.1 (draft)
Status: draft. Extends the OpenSwarm core (spec.md). Reuses
ipfile's encryption, grants and credit window and ippay's passes and
vouchers. Adds one BEP 10 extension, iplive. Nothing below is
implemented.
1. Scope
iplive moves a live stream from one origin to many viewers through peers
that are paid to relay it:
- A channel record: identity, renditions, latency mode, price, relay policy.
- A head record: signed every segment, naming the segments that exist and the key epoch in force.
- The
ipliveextension: announcing, requesting and fanning out encrypted segments with backpressure. - Roles: origin, relay, viewer, and how a relay is paid.
- Recording: how a finished stream becomes an
ipvideotitle.
Non-goals
Ingest protocols (RTMP and SRT are the encoder's business; c0mpute's DIP 0019 covers them). Sub-second latency; the floor is one part duration. Chat. Ads.
2. Terminology
| Term | Meaning |
|---|---|
| channel | A named live source with a key pair derived from the publisher seed. |
| channel id | 20 bytes: SHA-256 of `"openswarm:iplive:v1" |
| origin | The peer that encodes, encrypts, signs heads and holds the content key. |
| relay | A peer that forwards segments it has verified and is paid per byte. |
| viewer | A peer that plays. It MAY also relay. |
| segment | One CMAF fragment, 2000 ms by default, encrypted as one unit. |
| part | In low-latency mode, a sub-segment chunk (500 ms default) announced before the segment completes. |
| key epoch | The span of segments encrypted under one content key. |
| live edge | The newest segment the origin has announced. |
3. Data model
3.1 Channel
The channel key derives like a feed:
channelSeed = HKDF-SHA256(ikm = publisher seed, salt = utf8("live:" + name), info = "openswarm:live:v1")
{
"openswarm": "0.1",
"type": "iplive.channel",
"channel": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
"box": "x25519:4c4055604fe6bd8781cab6835093ad896f68f676b472d5bd6811e52f28cb18c3",
"publisher": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
"name": "courtside",
"title": "Courtside, live",
"latency": "low",
"segmentMs": 2000,
"partMs": 500,
"epochSegments": 900,
"dvrSegments": 1800,
"renditions": [
{ "id": "720p", "codecs": "avc1.64001f,mp4a.40.2", "width": 1280, "height": 720, "frameRate": 30, "bitrateKbps": 2800 },
{ "id": "360p", "codecs": "avc1.64001e,mp4a.40.2", "width": 640, "height": 360, "frameRate": 30, "bitrateKbps": 800 }
],
"swarm": {
"trackers": ["wss://tracker.openwebtorrent.com", "udp://tracker.opentrackr.org:1337/announce"],
"private": false
},
"relays": { "allow": "any", "maxDownstream": 8, "minRelays": 2 },
"policy": { "vanilla": "deny", "creditBytes": 8388608, "voucherBytes": 2097152 },
"pricing": {
"currency": "USD",
"perGib": "0.020000",
"keyUsd": "1.000000",
"split": { "publisherBps": 6000, "seederBps": 3500, "hubBps": 500 },
"keeperBps": 0,
"hubs": ["https://coinpayportal.com/api/openswarm"],
"payTo": { "network": "eip155:8453", "address": "0xCC3b072391AE7A8d10cF00DdC5F61DB2cA5541E5" }
},
"keys": { "modes": ["peer"], "keepers": [] },
"createdAt": "2026-09-05T20:00:00.000Z",
"sigs": [
{
"alg": "ed25519",
"key": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
"sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
},
{
"alg": "ed25519",
"key": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
"sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
}
]
}
| Field | Rule |
|---|---|
channel, box |
Channel signing and box keys. Signed by both channel and publisher, as an ipfile manifest is. |
latency |
normal: segments announced when complete. low: parts announced as they are encoded. |
segmentMs, partMs |
2000 and 500 by default. 4000 is allowed and matches c0mpute DIP 0019. |
epochSegments |
Segments per content key. 900 at 2 s is 30 minutes. |
dvrSegments |
Segments the origin and relays keep for late joiners and scrubbing. |
relays.allow |
any, or a list of identities allowed to pull from the origin. |
relays.maxDownstream |
Peers one relay will feed at once. |
relays.minRelays |
Relays the origin wants before it stops feeding viewers directly. |
policy, pricing, keys |
As ipfile §3.1 and §7, with the same fields and meanings. seederBps pays relays. |
The record lives in ipdb under channel:<channel key>.
3.2 Head
Signed by the channel key every time a segment (or, in low-latency mode, a part) is announced:
{
"openswarm": "0.1",
"type": "iplive.head",
"channel": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
"seq": 4187,
"prev": "sha256:ba8cd1a97b865a05cdc5840c63b086dd463624a831874c1ba0d7c4ce6f806f7a",
"epoch": 4,
"keyId": "sha256:41d10f45e705e0526c9eeedf62b32dda3daaf552dd9b6a7b09e01776b05813bb",
"live": true,
"segments": [
{
"seq": 4187,
"t": 8374000,
"d": 2000,
"renditions": [
{ "id": "720p", "root": "sha256:d6c3f8285b7871d6a400cba14408288a9acde679f12e1e7dc276f29ca7c493ff", "size": 702113, "parts": [175528, 175530, 175527, 175528] },
{ "id": "360p", "root": "sha256:4b74eb43677e4d03af5fb0856333f9aa21d9a5a3bbf944b13aa7eef379c7a342", "size": 200418, "parts": [50104, 50105, 50104, 50105] }
]
}
],
"init": [
{ "id": "720p", "root": "sha256:c9f17d4eaf2b28122b46d111cef6697d2c3f708a19d4628bce5c0304481b355b", "size": 1524 },
{ "id": "360p", "root": "sha256:00bc9c01ca00938016e276fe5b5748cfe13fd02bc53cc651535682c8dd5c83be", "size": 1498 }
],
"relays": ["ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94"],
"createdAt": "2026-09-05T22:19:34.000Z",
"sigs": [
{
"alg": "ed25519",
"key": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
"sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
}
]
}
| Field | Rule |
|---|---|
seq |
Segment number of the live edge. Starts at 1 when the stream starts. |
prev |
Id of the previous head, so a viewer can detect a skipped announcement. |
epoch, keyId |
Which content key encrypts segments from this seq on. A key change is announced one full segment before it applies. |
segments |
The newest segment, or the newest few. A head is small: relays fill history from have. |
segments[].renditions[].root |
BEP 52 pieces root of the segment ciphertext, with the segment as one piece. That is what a relay verifies before forwarding. |
segments[].renditions[].parts |
Byte lengths of each part, in order, for low-latency requests. |
init |
Roots and sizes of each rendition's init segment, which is not encrypted. |
relays |
Identities the origin currently feeds; a viewer prefers these. |
live |
false in the final head, which marks the end of the stream. |
Heads are not stored in ipdb. The final head is, under
channel:<key>:end:<seq>, alongside the recording (§8).
3.3 Segment encryption
As ipfile §4.1, with the segment as a single piece and the IV derived with
info = "openswarm:iplive:iv:v1:" + decimal(seq). The content key changes
each epoch; a grant carries keyId and a viewer requests a new grant when
the head announces a new keyId. A pass expiring mid-stream stops at the
next epoch, which is why epochs are short.
4. Wire protocol
4.1 Finding peers
Peers announce the channel id to the DHT (announce_peer) and to the
channel's trackers exactly as they would an infohash. Vanilla clients that
see it find a swarm whose ut_metadata returns nothing; they leave.
Browsers find WebRTC peers through wss trackers as with any WebTorrent
swarm.
4.2 Handshake
The BitTorrent handshake uses the channel id as the infohash. The BEP 10
handshake advertises iplive. Both sides send hello with the ipfile
§5.1 fields (identity, box, nonce, roles, hubs, credit, interval, sig)
where roles is any of origin, relay, viewer, and the signature
covers the channel id in place of infohashV2.
4.3 Messages
t |
Payload | Meaning |
|---|---|---|
hello |
as ipfile |
Identity and terms. |
channel |
{ "r": bytes } |
The channel record. Sent by origin and relays after hello. |
head |
{ "r": bytes } |
A head record. Forwarded to every downstream peer within 100 ms of receipt. |
have |
{ "from": int, "to": int, "r": [string] } |
Segment range and rendition ids the sender holds. |
window |
{ "buffer": int, "live": int, "down": int, "max": int } |
Segments buffered, live edge, downstream count, maxDownstream. Sent after hello and whenever it changes. |
want |
{ "seq": int, "r": string, "part": int or absent } |
Request one segment, or one part. |
seg |
{ "seq": int, "r": string, "part": int or absent, "o": int, "d": bytes } |
A 16 KiB chunk of the requested unit at offset o. |
init |
{ "r": string, "d": bytes } |
An init segment. |
pass, credit, voucher, key_req, grant, deny, bye |
as ipfile |
Payment and keys, unchanged. |
seg chunks are 16 KiB so that a WebRTC data channel and an MTP/1 record
both carry one without fragmentation.
4.4 Session
viewer relay
| hello |
|---------------------------------------->|
|<------------------------------- hello |
|<----------------------------- channel |
|<------------------------------ window | buffer=30 live=4187 down=3 max=8
| pass |
|---------------------------------------->|
|<-------------------------------- head | seq 4187
|<-------------------------------- have | 4158..4187
| want 720p init |
|---------------------------------------->|
|<-------------------------------- init |
| want 4185 720p |
|---------------------------------------->|
|<--------------------------------- seg | x 43 chunks
| want 4186 720p ... |
| key_req |
|---------------------------------------->| relay is not a keeper:
|<-------------------------------- deny | not-keeper; ask origin or keys.url
| voucher |
|---------------------------------------->|
Rules:
- A viewer starts
segmentMs * 2behind the live edge innormalmode and one part behind inlowmode. - A peer MUST NOT
wanta seq belowlive - bufferof the peer it asks. If it has fallen further behind it jumps tolive - 1and discards. Stalls are resolved by dropping, never by holding a relay's buffer. - A relay forwards a segment only after verifying its ciphertext against
the
rootin a head signed by the channel key. A relay never needs the content key. - A relay serving
down >= maxanswers newhellos withdeny rateand the currenthead, whoserelayslist points elsewhere. - Payment is
ipfile§5.3 rules 2 to 6, with a relay as payee. The default window is larger (8 MiB) because a segment is bigger than a piece and a choke at the live edge is a stall. - Keys come from the origin or a keeper listed in the channel, never from a plain relay. A relay MAY be a keeper if the publisher delegates to it.
4.5 Low latency
In low mode the origin announces a head as soon as the first part of a
segment is encoded, with parts filled in as they complete (lengths for
finished parts, 0 for pending). A viewer requests parts by index and
appends them to MSE as they arrive. A part is encrypted as part of the
segment's CTR stream: part k begins at counter offset(k) / 16, so a
relay can forward parts without waiting for the segment and a viewer can
decrypt them in order. The segment root is only known when the segment
completes; a relay in low mode forwards parts on the strength of the
signed head naming the part lengths and verifies the root when the last
part lands, disconnecting any upstream whose completed segment fails.
5. Roles and payment
| Role | Holds key | Pays | Is paid |
|---|---|---|---|
| Origin | Yes | Nothing | Publisher share of every voucher, keyUsd per grant |
| Relay | No | Vouchers to its upstream for bytes it pulled | Vouchers from downstream for bytes it served |
| Viewer | Yes, after a grant | Vouchers to whoever served it | Vouchers if it also relays |
A relay's margin is seederBps of what it serves minus what it pays
upstream. With perGib = 0.020000, a relay feeding eight viewers from one
upstream pull earns 8 * 0.35 * 0.02 = 0.056 and pays 0.02 per GiB
pulled, before the hub's share. That is the incentive to relay, and a
c0mpute node bids on iplive.relay jobs for the guaranteed floor
(c0mpute.md §3).
The origin is a peer that pays nobody; relays.allow and minRelays are
how it limits its own upload. A publisher who wants more reach than its
relays provide buys pins on c0mpute.
6. Publishing procedure
ip live create courtside --latency low writes the channel record and
puts it in ipdb. ip live start courtside --input srt://:9000 then:
- Derives the channel key. Generates epoch 1's content key.
- Runs the encoder (ffmpeg) producing CMAF segments per rendition, and
for
lowmode, parts. - For each completed unit: encrypts, computes the root, signs a head,
announces to the DHT and trackers, sends
headto every connected peer. - Answers
key_reqwith grants for pass holders. - Rotates the content key every
epochSegments, announcing the newkeyIdone segment early. - On stop: signs a final head with
live: false, and if--record, assembles the recording (§8).
7. Viewer procedure
ip live watch ed25519:<channel key> or a browser page:
- Resolve the channel record from
ipdbor a peer. - Buy or reuse a pass scoped to the channel key with
grant: true. - Find peers; prefer identities in the latest head's
relays; connect to two. - Get the init segment, the head, a grant. Start at the live edge minus the offset in §4.4 rule 1.
- Pull each segment from the connected peer with the smaller
live - seqlag; on adeny rateor a stall over one segment duration, replace the peer. - Send vouchers on schedule to each peer that served bytes.
8. Recording
With --record, the origin keeps every segment's plaintext, concatenates
each rendition into one CMAF file, and publishes an ipvideo.title whose
segment index is exactly the head history. Because segments were 2 or 4
seconds and keyframe-aligned, the recording is a valid ipvideo ladder
with no re-encode. The title's ipdb entry references the channel in
meta.channel and the final head's id.
9. Security and privacy
- Relays see ciphertext and heads. They learn segment sizes and timing, never content.
- Head forgery needs the channel key. A relay forwarding an unsigned or wrongly signed head is dropped by every downstream peer.
- Freeloading is bounded per
ipfile§11 with the larger window. - Denial by relay count: an origin with
relays.allow: anycan be surrounded by relays that pull and never serve. It pays them nothing (it pays nobody) and they earn nothing; the cost is its upload.maxDownstreamon the origin is the cap. - Late key: a viewer whose pass expires keeps the current epoch's key and loses the next. Epoch length is the publisher's tolerance.
10. Events
| Type | Emitted by |
|---|---|
com.logicsrc.openswarm.iplive.channel.created.v1 |
publisher |
com.logicsrc.openswarm.iplive.stream.started.v1 |
origin |
com.logicsrc.openswarm.iplive.stream.ended.v1 |
origin, with the final head id and recording title id |
com.logicsrc.openswarm.iplive.relay.joined.v1 |
origin |
11. Implementations
| Piece | Exists | Where | What is new |
|---|---|---|---|
| RTMP/SRT ingest to HLS segments, segment swarming in the DHT, relay nodes rebuilding playlists, LL-HLS opt-in | Designed | c0mpute dips/0019-live-stream-plugin.md |
Encrypted segments, signed heads, paid relays: the same design with ippay attached |
| Live TV playback in the browser (hls.js, mpegts.js) | Yes | media-streamer hls-player-modal.tsx, tipoffwatch and genrewatch players |
MSE append of decrypted CMAF parts from peers instead of an HLS URL |
| Pay-per-game passes with live session caps | Yes (central) | media-streamer IPTV resale (iptv_share_sessions) |
Concurrency is no longer the owner's provider line; it is the origin's maxDownstream |
Channel and head records, iplive messages, relay economics, recording to ipvideo |
No | All of it |
12. Conformance
An origin signs a head per unit, rotates keys per epoch, and announces a
key change one segment early. A relay verifies roots before forwarding,
honours window rules, and never serves below live - buffer. A viewer
drops rather than stalls, vouchers every peer that served it, and requests
a grant on a keyId change.
13. Version history
| Version | Date | Change |
|---|---|---|
| 0.1 | 2026-09-05 | Initial draft. |