Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family (#143)
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run

* Add pay2seed to the OpenSwarm family: consent at upload and a paid seed market

OpenSwarm pays a seeder per verified piece served, and nothing pays anyone
to stay. An archive, a backup, a dataset waiting for its buyer or a
podcast's back catalogue earns nothing the month nobody downloads it, so
it dies the way every swarm always has. And nothing in BitTorrent says who
put a swarm there or whether they were allowed to, which is why a seeder
is presumed to be doing something wrong.

pay2seed is the member document for both halves. An attestation, signed
at upload with a fixed basis (own, licensed, open-license, public-domain,
personal) and a notice endpoint, is what a hub requires before it will
list anything; public claims get a claim window and a standing, and a
notice voids them. An offer escrows a budget at an ippay hub for a swarm,
public or private, to be held by M seeders for N days at a price per
GiB-month, bought over x402 exactly as a pass is. Seeders take leases,
prove each period by storage challenge or by a probe over the ordinary
wire, and are paid through the payee they already have. Public feeds ride
on ipdb; ipfile.pin on c0mpute is the same offer on the auction.

Also: the family table, stack diagram and registry rows; the ip seed
command group; PRD 0006; the protocol row on /openswarm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKAohrRkqLKVQL2cGCAkR5

* Split pay2seed into client and server halves, and add pay2stream and paid2stream

The rule is now in the names. pay2* is the client protocol: the side that
pays, over HTTPS, and plays. paid2* is the server protocol: the BitTorrent
side that earns. One hub implements both halves of a pair; a requester or
viewer implements only pay2*; a seeder, relay or gateway only paid2*.

pay2seed keeps consent, offers, the requester's market and notices.
paid2seed takes leases, storage challenges and probes over the wire,
GiB-month accrual and receipts, the seeder client, and the ipfile.pin
mapping.

pay2stream and paid2stream do the same for a live channel over iplive.
A broadcaster attests the channel (with the two rules that separate a
licensed rebroadcast from a stolen feed), buys relays by the hour, and
publishes listings; viewers buy tickets. Relays take leases and are
proven present by a verifier that pulls segments as a peer; a gateway is
a relay that also serves standard HLS, clear or sealed, with the M3U and
XMLTV pair every IPTV app asks for, so VLC, TiviMate, Kodi and a
television play a paid swarm with nothing installed. Ace Stream showed
BitTorrent can carry live TV to millions; this is that with consent,
payment and an open spec.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKAohrRkqLKVQL2cGCAkR5

* pay2seed: encrypted by default, access as the product, a README in every swarm

The client encrypts by default and the hub never does: what the hub
manages is who may decrypt. A team is a named set of member keys with a
scope over the owner's swarms; the hub, as keeper, issues grants to
members when the owner is offline, with invitations that expire, roles,
an audit trail, and re-encryption on removal so the next version is
closed to whoever left. A few seats are free; above that the hub charges
per seat and per organisation, settled through the same pay plugins as
everything else. Seeding is priced at disk; access is where a hub earns,
and both sides earn: seeders rent disk, requesters sell access.

Public is not a fallback. Encryption off is an explicit act, and a
public swarm is attested, listed, kept and rendered exactly as a private
one is; the only difference is who can read it.

Every swarm on the market carries a README.md at its root, no
exceptions, and the attestation carries its Markdown and the hash of
the copy inside the swarm, so the hub renders it as the swarm's page
without a key. Relative links resolve into the swarm and are gated the
way the files are.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKAohrRkqLKVQL2cGCAkR5

* pay2seed: 1 percent, ads on the free tier, and agents as sellers

Three things the specs did not say. The reference hub takes 1 percent of
any payment that crosses it, charged to whoever is paying and never
deducted from a seeder or a relay, so a quoted price is what the
publisher gets and a promised floor is what the seeder is paid.

Public swarms are free to fetch and free to list, and an advertisement on
the swarm README page is what pays for that. The ad is on the hub page
and nowhere else: never inside a swarm, never injected into a file, a
segment or a playlist, and never in the catalogue or the market API. A
requester who wants no ad buys a seat instead. A free-to-watch channel
works the same way.

And a requester is a key, not a person. An agent can attest what it made,
price access, sell tickets, take payment through its own payee and spend
what it earns keeping its own work online. The consent rules do not
soften because a machine signed them, and the reference hub asks an
agent public attestation to name a responsible operator key so somebody
is reachable when a notice arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKAohrRkqLKVQL2cGCAkR5

* Fix CI: number the PRD requirements and advance the next-id assertion

Two checks the new PRD tripped, both by existing rather than by being
wrong.

The collection validator wants requirements as numbered R# entries and
0006 used a plain ordered list, so it reported OP-L-NO-REQUIREMENTS.
Rewritten as R1 to R7 with priorities, one capability per entry, and the
implementation tracking moved to a paragraph under them where it is not
pretending to be a requirement.

The MCP standards test asserts what the next free PRD id is, and its own
comment says that advances with every PRD added. Adding 0006 makes it
0007.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKAohrRkqLKVQL2cGCAkR5

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-05 19:12:45 -07:00 committed by GitHub
parent 4fed2681ec
commit be99e683bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1751 additions and 3 deletions

View file

@ -17,7 +17,11 @@ const PROTOCOLS: Array<[name: string, line: string]> = [
["ipaudio", "Audio releases and tracks on ipfile swarms: renditions, seek tables, gapless, royalties"], ["ipaudio", "Audio releases and tracks on ipfile swarms: renditions, seek tables, gapless, royalties"],
["ipvideo", "Video on demand: CMAF renditions, segment index, subtitles, thumbnails, an HLS bridge"], ["ipvideo", "Video on demand: CMAF renditions, segment index, subtitles, thumbnails, an HLS bridge"],
["iplive", "Live streams: segment fan-out over paid relays with backpressure"], ["iplive", "Live streams: segment fan-out over paid relays with backpressure"],
["ipname", "How a Moshpit name or a domain resolves to a publisher key and a catalogue"] ["ipname", "How a Moshpit name or a domain resolves to a publisher key and a catalogue"],
["pay2seed", "Client protocol for paid seeding: consent at upload, seed offers with escrowed budgets, the requester's market"],
["paid2seed", "Server protocol for paid seeding: leases, storage challenges and probes over the wire, GiB-month settlement, the seeder client"],
["pay2stream", "Client protocol for paid live streams: consent for channels, relay offers by the hour, tickets, watching on any HLS player"],
["paid2stream", "Server protocol for paid live streams: relay leases, presence proofs, gateways serving standard HLS with an M3U and EPG"]
]; ];
const ROLES: Array<[role: string, work: string, paid: string]> = [ const ROLES: Array<[role: string, work: string, paid: string]> = [

View file

@ -34,6 +34,10 @@ a different product. The member protocols keep their `ip` names.
| `ipvideo` | Video on demand on `ipfile` swarms: CMAF renditions, segment index, subtitles, thumbnails | [`ipvideo.md`](./openswarm/ipvideo.md) | | `ipvideo` | Video on demand on `ipfile` swarms: CMAF renditions, segment index, subtitles, thumbnails | [`ipvideo.md`](./openswarm/ipvideo.md) |
| `iplive` | Live streams: segment fan-out over peers, paid relays, backpressure | [`iplive.md`](./openswarm/iplive.md) | | `iplive` | Live streams: segment fan-out over peers, paid relays, backpressure | [`iplive.md`](./openswarm/iplive.md) |
| `ipname` | How a Moshpit name resolves to a publisher key and a catalogue | [`ipname.md`](./openswarm/ipname.md) | | `ipname` | How a Moshpit name resolves to a publisher key and a catalogue | [`ipname.md`](./openswarm/ipname.md) |
| `pay2seed` | Client protocol for paid seeding: consent at upload, seed offers with escrowed budgets, the requester's market | [`pay2seed.md`](./openswarm/pay2seed.md) |
| `paid2seed` | Server protocol for paid seeding: leases, storage challenges and probes over the wire, GiB-month settlement, the seeder client | [`paid2seed.md`](./openswarm/paid2seed.md) |
| `pay2stream` | Client protocol for paid live streams: consent for channels, relay offers, tickets and listings, watching as a peer or on any HLS player | [`pay2stream.md`](./openswarm/pay2stream.md) |
| `paid2stream` | Server protocol for paid live streams: relay leases per hour, presence proofs, gateways serving standard HLS, M3U and EPG | [`paid2stream.md`](./openswarm/paid2stream.md) |
Supporting documents: Supporting documents:
@ -50,6 +54,10 @@ Supporting documents:
| ipaudio ipvideo iplive ipdb (catalogue) | | ipaudio ipvideo iplive ipdb (catalogue) |
| releases titles channels feeds, entries | | releases titles channels feeds, entries |
+---------------------------------------------------------------+ +---------------------------------------------------------------+
| pay2seed / paid2seed | pay2stream / paid2stream |
| consent, offers, market | consent, relay offers, tickets |
| leases, challenges, payout | leases, presence, HLS gateways |
+------------------------------+--------------------------------+
| ipfile: manifest, per-file key pair, encrypted pieces, | | ipfile: manifest, per-file key pair, encrypted pieces, |
| key grants, credit window, vouchers per served piece | | key grants, credit window, vouchers per served piece |
+-------------------------------+-------------------------------+ +-------------------------------+-------------------------------+
@ -132,6 +140,20 @@ serves more than the window unpaid.
Its head is a BEP 44 mutable item under the feed key, so any DHT node can find Its head is a BEP 44 mutable item under the feed key, so any DHT node can find
the latest catalogue of any publisher with one `get`. the latest catalogue of any publisher with one `get`.
**One seed market, one stream market.** A `pay2seed` offer is money
escrowed at a hub for a swarm to be kept for a period, public or private,
and it cannot be listed without a signed attestation of who put the data
there and on what basis. On the `paid2seed` side, seeders take leases,
prove they hold and serve the pieces every period, and are paid per
GiB-month. `pay2stream` and `paid2stream` do the same for a live channel:
a broadcaster attests it and buys relays by the hour, relays and gateways
prove they are online and serving, and a gateway turns the swarm into
standard HLS so any television plays it. The naming is the rule: `pay2*`
is the client protocol, the side that pays over HTTPS; `paid2*` is the
server protocol, the BitTorrent side that earns. It is how a torrent
client becomes a legitimate file sharer: consent on the way in, proof on
the way out, and a payout for staying.
## What it does not define ## What it does not define
A media player. Transcoding settings beyond what a manifest must declare. A A media player. Transcoding settings beyond what a manifest must declare. A
@ -146,6 +168,7 @@ key and the spec says so.
| Browser WebTorrent player, hybrid Node seeder, wss trackers | `profullstack/media-streamer` (bittorrented.com) | Speaks the vanilla wire this family extends; needs the `ipfile` extension to pay | | Browser WebTorrent player, hybrid Node seeder, wss trackers | `profullstack/media-streamer` (bittorrented.com) | Speaks the vanilla wire this family extends; needs the `ipfile` extension to pay |
| DHT crawl (bitmagnet) and `/dht` browse | `profullstack/media-streamer`, `dht-infohash-crawler` | Observes `ipfile` swarms as opaque infohashes; `ipdb` is how it would learn what they are | | DHT crawl (bitmagnet) and `/dht` browse | `profullstack/media-streamer`, `dht-infohash-crawler` | Observes `ipfile` swarms as opaque infohashes; `ipdb` is how it would learn what they are |
| Pay-per-pass grants, HLS manifest sealing | `media-streamer` IPTV and seedbox rails | The central-proxy version of what `ippay` moves into the swarm | | Pay-per-pass grants, HLS manifest sealing | `media-streamer` IPTV and seedbox rails | The central-proxy version of what `ippay` moves into the swarm |
| Headless seeder daemon with an add API and per-torrent seed time | `torlink` (`torlnk serve`) | The `pay2seed` seeder client, once it polls a market and answers challenges |
| x402 v2 offer, `X-PAYMENT` proof, verify and settle | `profullstack/x402-gateway`, CoinPay | `ippay` pass purchase reuses it unchanged | | x402 v2 offer, `X-PAYMENT` proof, verify and settle | `profullstack/x402-gateway`, CoinPay | `ippay` pass purchase reuses it unchanged |
| CloudEvents 1.0 + Standard Webhooks signing | `profullstack/autoblog` | Every OpenSwarm event uses the same envelope and headers | | CloudEvents 1.0 + Standard Webhooks signing | `profullstack/autoblog` | Every OpenSwarm event uses the same envelope and headers |
| MTP/1 post-quantum transport, name pins | `profullstack/moshpit-transport`, `moshpit-proxy` | Optional tunnel for native peer links; `ipname` pin kind | | MTP/1 post-quantum transport, name pins | `profullstack/moshpit-transport`, `moshpit-proxy` | Optional tunnel for native peer links; `ipname` pin kind |

View file

@ -130,6 +130,44 @@ ip node hello <peer address> # diagnostic: handshake and p
A c0mpute worker embeds the same library; `c0mpute worker start --openswarm` A c0mpute worker embeds the same library; `c0mpute worker start --openswarm`
is the daemon form and `ip` is the operator's tool. is the daemon form and `ip` is the operator's tool.
### 2.10 Seed and stream markets
```
ip seed attest (<file key> | <infohash> | <magnet>) --basis own|licensed|open-license|public-domain|personal
[--license <spdx>] [--notice <url|mailto>] [--description <text>] [--public | --private]
ip seed offer (<file key> | <infohash> | --feed <feed key>) --hub <url> --days <n> --seeders <min>[,<max>]
[--price <usd per GiB-month>] [--proof-hours <n>] [--tracker <url>]...
ip seed offers [--hub <url>] [--public | --private] [--basis <b>] [--min-price <usd>] [--max-size <bytes>]
ip seed take <offer id> --hub <url> # take a lease, fetch, seed until it ends
ip seed leases [--hub <url>] [--status proven|fetching|lapsed|ended]
ip seed status <offer id | lease id>
ip seed void <offer id> # requester: void and refund the unearned budget
ip seed notice <attestation id> --kind rights|illegal|personal-data|other --statement <text>
ip team create <name> --hub <url> [--scope <file key>|--publisher <key>]...
ip team invite <team> --to <email|ipname|key> [--role admin|member|readonly] [--expires <days>]
ip team accept <invite id>
ip team remove <team> <key> [--no-rotate]
ip team grant <team> --file <file key> # a member fetching their sealed grant
ip team audit <team> [--since <time>]
```
```
ip stream offer <channel key> --hub <url> --from <time> --to <time> --relays <min>[,<max>]
[--price <usd per relay-hour>] [--gateway-bonus <bps>] [--region <r>]...
ip stream ticket <channel key> --hub <url> --hours <n> [--gateway <url>] # prints the HLS, M3U and EPG URLs
ip stream listing <channel key> --title <text> --from <time> --to <time> [--price <usd>]
ip stream relay <offer id> --hub <url> [--gateway --base <url> --mode clear|sealed]
ip stream relays [--hub <url>]
```
`ip seed attest` is the consent step and is what `ip file add --public`,
`ip file pin` and `ip live create` call first; `ip seed offer` on a private file key includes a
pass for the seeders when the manifest charges per GiB. `ip seed take` is
what a torlink daemon does on its own from `ip seed offers`; here it is the
manual form, and it sets the swarm's seed time to the lease's end so the
client's reaper cannot drop it early. Everything returns the record ids
(`pay2seed` §7.2) and, for `offers`, the projected `earnedUsd` per lease.
## 3. Output examples ## 3. Output examples
``` ```

View file

@ -261,7 +261,7 @@ Published at `GET <base>/.well-known/openswarm-hub.json`:
"eip155:137": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "eip155:137": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" "eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}, },
"minHubBps": 300, "minHubBps": 100,
"payout": { "minUsd": "1.000000", "schedule": "daily" }, "payout": { "minUsd": "1.000000", "schedule": "daily" },
"passDays": 30, "passDays": 30,
"createdAt": "2026-09-05T00:00:00.000Z", "createdAt": "2026-09-05T00:00:00.000Z",
@ -280,6 +280,14 @@ lists the hub's `base`, from TLS to that base, and optionally from an MTP/1
pin. `minHubBps` is the least `split.hubBps` a manifest may set for this hub pin. `minHubBps` is the least `split.hubBps` a manifest may set for this hub
to sell passes for it. to sell passes for it.
The reference hub takes **1 percent** (`minHubBps: 100`), on every payment
that crosses it: a pass, a key grant, a seed offer, a relay offer, a
ticket. It is charged to whoever is paying and never deducted from a
seeder's or a relay's earnings, so what a publisher quotes is what a
publisher gets and what a seeder is promised is what a seeder is paid. A
hub setting a higher floor says so in this record, and a manifest naming
it can refuse.
### 5.2 API ### 5.2 API
All bodies are JSON. Records are verified on receipt. All bodies are JSON. Records are verified on receipt.

325
docs/openswarm/paid2seed.md Normal file
View file

@ -0,0 +1,325 @@
# paid2seed: The Server Protocol for Paid Seeding
Status: 0.1 draft. Member of the [OpenSwarm](../openswarm.md) family.
Depends on the [core](./spec.md), [`ipfile`](./ipfile.md) and
[`ippay`](./ippay.md). Its client half is [`pay2seed`](./pay2seed.md).
## 1. Scope
`paid2seed` is the side that gets paid. It is spoken by a seeder, a
BitTorrent client with disk and uptime to sell, to a hub over HTTPS and to
the swarm over the ordinary wire. It defines:
- **Leases.** The hub's binding of one seeder to one `pay2seed.offer` for
one period at one price.
- **Proof of seeding.** Storage challenges answered from held pieces, and
probes in which a verifier fetches pieces over BEP 3 or the `ipfile`
extension. Both work on a vanilla torrent, which is what makes seeding
a public swarm payable.
- **Settlement.** GiB-month accrual per proven period, receipts, and
payout through the `ippay` payee a seeder already has for vouchers.
- **The seeder client**: how a torrent client polls a market, takes work,
seeds it, proves it, and resumes after a restart. torlink's headless
daemon is the reference.
Consent, offers, the requester's view and notices are
[`pay2seed`](./pay2seed.md). A hub implements both. A seeder client
implements only this document.
### Non-goals
Anything about what the data is. A seeder sees an infohash or a file key,
a size, a basis and a visibility, and decides whether to hold it. It is
never asked to judge it and never able to read a private swarm.
## 2. Terminology
- **Seeder.** An identity that holds a lease and serves the swarm.
- **Verifier.** Whoever issues challenges and probes: the hub, a c0mpute
verifier node, or the requester. The offer names who may.
- **Lease.** One seeder's share of an offer (§3). **Period.** The unit of
accrual and proof, `offer.proof.everyHours` long.
- Other terms as `pay2seed` §2.
## 3. Leases
### 3.1 Record
```json
{
"openswarm": "0.1",
"type": "paid2seed.lease",
"hub": "ed25519:c9f1…",
"offer": "sha256:1b7f…",
"seeder": "ed25519:a41e…",
"slot": 1,
"priceUsdPerGibMonth": "0.150000",
"startsAt": "2026-09-05T18:30:00.000Z",
"endsAt": "2026-10-05T18:00:00.000Z",
"graceHours": 24,
"createdAt": "2026-09-05T18:30:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:c9f1…", "sig": "…" }]
}
```
| Field | Rule |
| --- | --- |
| `offer` | Record id of the `pay2seed.offer`. |
| `seeder` | The seeder key. MUST be a registered `ippay` payee (§5.4). |
| `slot` | 1-based, at most `offer.seeders.max`. One lease per seeder per offer. |
| `priceUsdPerGibMonth` | Copied from the offer at lease time so a later edit never changes what was agreed. |
| `graceHours` | Time from `startsAt` in which the seeder MUST pass its first proof, or the lease is `abandoned` and the slot reopens. Long enough to fetch the swarm. |
A lease is `fetching` until its first proof, `proven` while its most recent
period passed, `lapsed` when a period failed, `abandoned` after
`graceHours` without a first proof, `ended` at `endsAt`, and `voided` with
its offer.
### 3.2 Taking and losing a lease
`POST /leases` with `{ "offer": <id>, "seeder": <key>, "sig" }`, `sig` by
the seeder key over `"openswarm:paid2seed:lease:v1" || offer id`. The hub
answers `201` with the lease, `409` when the offer has no free slot or this
seeder already holds one, or `403` when the seeder's standing (§6.3) is
below the offer's or hub's floor.
A hub SHOULD hand out slots in order of standing and MUST NOT hand out
more than `seeders.max`. A seeder SHOULD NOT take a lease it cannot serve
within `graceHours`; an abandoned lease counts against standing. When an
offer is voided (`pay2seed` §8) every lease on it is `voided`, the hub
pushes `paid2seed.lease.voided`, and a seeder learns of it on its next
poll at the latest.
## 4. Proof of seeding
### 4.1 Storage challenges
Once per period, a verifier named in the offer sends the seeder:
```json
{
"openswarm": "0.1",
"type": "paid2seed.challenge",
"lease": "sha256:5c02…",
"period": 7,
"nonce": "1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f",
"pieces": [17, 2048, 91, 4400],
"deadline": "2026-09-07T12:10:00.000Z",
"createdAt": "2026-09-07T12:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:c9f1…", "sig": "…" }]
}
```
The seeder answers within `deadline`:
```json
{
"openswarm": "0.1",
"type": "paid2seed.proof",
"challenge": "sha256:9d3a…",
"answers": ["sha256:…", "sha256:…", "sha256:…", "sha256:…"],
"createdAt": "2026-09-07T12:00:04.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:a41e…", "sig": "…" }]
}
```
Each answer is `SHA-256(nonce || piece bytes)` for the piece at that index,
which a verifier holding a copy can check and which cannot be answered
from the piece hash alone. `pieces` MUST be at least four indices chosen
uniformly at random per challenge; `deadline` MUST be at least ten minutes
after `createdAt`, enough to read four pieces from a spinning disk and not
enough to fetch them from the swarm.
### 4.2 Probes
A verifier connects to the seeder as an ordinary peer, over BEP 3 for a
vanilla torrent or the `ipfile` extension for a private swarm, and requests
the challenge's pieces. The seeder's ordinary serving is the proof; the
verifier records `{ "lease", "period", "pieces", "receivedBytes", "peer" }`
signed by its own key, `type` still `paid2seed.proof`. A probe MUST fetch
from the seeder's own announced address; a swarm being alive is not
evidence that this seeder is keeping it alive.
For a `private` swarm whose `pricing.perGib` is non-zero the verifier pays
vouchers for the probe like any leecher, and those are the seeder's in the
usual way. Hubs SHOULD prefer probes for public swarms and challenges for
private swarms they already pin.
### 4.3 Delivery
A hub pushes challenges to a seeder's registered webhook when it has one,
and otherwise leaves them to be polled from `GET /leases/<id>/challenges`.
A seeder without a webhook MUST poll at least every `everyHours / 4`
hours. `deadline` is measured from `createdAt`, not delivery, so a seeder
that polls slowly fails challenges; the cadence is the seeder's to meet.
## 5. Settlement
### 5.1 Accrual
A lease earns, for each period in which its proof passed:
```
earnedUsd = priceUsdPerGibMonth * (sizeBytes / 2^30) * (everyHours / 720)
```
in integer micro-USD, rounding down. A failed or missed period earns
nothing and moves the lease to `lapsed`; two consecutive failures end it
and reopen the slot.
### 5.2 Receipts
```json
{
"openswarm": "0.1",
"type": "paid2seed.receipt",
"lease": "sha256:5c02…",
"period": 7,
"proof": "sha256:e77b…",
"earnedUsd": "0.002604",
"balanceUsd": "0.018228",
"createdAt": "2026-09-07T12:00:05.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:c9f1…", "sig": "…" }]
}
```
### 5.3 Fees and payout
The hub's fee is charged to the requester at purchase, `hubBps` of
`budgetUsd` with `hubBps` no less than the hub record's `minHubBps`; it is
never deducted from a seeder's earnings. Payout is `ippay` §5.6, on the
same schedule and floor as voucher earnings.
### 5.4 Payees
A seeder is an `ippay` payee (`ippay` §6.1). Registering once covers
vouchers, key grants and leases. A seeder key that is not registered
cannot take a lease, because there would be nowhere to pay.
## 6. The seeder's hub
### 6.1 API, seeder side
Paths relative to the `pay2seed.base` in the hub record (`pay2seed`
§6.1). The requester side is `pay2seed` §6.2.
| Method and path | Auth | Purpose |
| --- | --- | --- |
| `GET /offers?visibility&basis&minPrice&maxSize&feed&status` | none | The market: open offers with free slots. |
| `POST /leases` | seeder signed | Take a slot (§3.2). |
| `GET /leases/<id>` | none | The lease, status, periods proven, `earnedUsd`. |
| `GET /seeders/<key>/leases?status` | none | A seeder's leases; what a client resumes after a restart. |
| `GET /leases/<id>/challenges?since` | seeder | Pending challenges for a seeder that cannot receive them pushed. |
| `POST /proofs` | seeder or verifier signed | Answer a challenge or report a probe. Returns the receipt. |
| `GET /seeders/<key>` | none | `{ "standing", "leases", "proven", "failed", "abandoned", "balanceUsd" }`. |
| `POST /webhooks` | signed | Register a CloudEvents endpoint for a seeder key. |
### 6.2 The market listing
`GET /offers` is what a seeder client shows its operator, or chooses from
on its own. Each row carries the offer, the attestation's `basis`,
`description` and `visibility`, the free slot count, and the projected
`earnedUsd` for a full lease. A client MUST show `basis` and `visibility`
wherever it shows an offer, so an operator knows whether they are being
asked to hold ciphertext for a stranger's backup or to seed an openly
licensed dataset in the clear.
### 6.3 Standing
Seeder standing is `proven - 2 * failed - 3 * abandoned` over the trailing
year, floored at zero. Requester standing is `honoured - 3 * voided`, the
same way. Both are public and are one hub's opinion signed by that hub; a
second hub MAY read it and MAY ignore it.
## 7. Seeder client behaviour
A seeder client (torlink's headless daemon is the reference) does the
following, and a conformant one does all of it:
1. Holds a seeder key, registered as an `ippay` payee with a payout
address, and stores the key as an OpenCreds `key` item.
2. Polls `GET /offers` on the hubs it is configured for, filtered by the
operator's policy: which `visibility` and `basis` values to accept, the
minimum `priceUsdPerGibMonth`, the maximum `sizeBytes`, free disk to keep
in reserve.
3. Takes a lease, fetches the swarm (as a paying peer when `offer.pass` is
set), adds the offer's `trackers`, and seeds with a per-torrent seed time
of at least `lease.endsAt`, so its own reaper never drops a swarm it is
being paid to keep.
4. For a feed offer, follows the feed (`ipdb` §5.4), fetches each new
segment within one period of its appearance in the head, seeds all of
them, and re-announces the head as a BEP 44 mutable item at least once
per period so the catalogue stays findable when the publisher is
offline.
5. Answers challenges from local pieces within `deadline`, and serves
probes as ordinary peer traffic.
6. On restart, resumes every lease in
`GET /seeders/<key>/leases?status=proven,fetching` before taking new ones.
7. Stops seeding and deletes the data when a lease ends or is voided,
unless the operator has chosen to keep it; for a `private` swarm the
ciphertext is worthless without a grant, and for a `personal` one it
MUST be deleted.
8. Shows the operator, for every held lease, the `basis`, `visibility`,
`earnedUsd` so far, and the next challenge due.
## 8. Relationship to `ipfile.pin`
An `ipfile.pin` job (c0mpute §3.1) is a `pay2seed.offer` with
`visibility: "private"`, `seeders.min = seeders.max = 1`, `days` and
`proof.everyHours` copied, `priceUsdPerGibMonth` derived from
`max_price_usd`, and c0mpute's storage challenge standing in for §4.1. A
c0mpute node that wins such a job holds a lease in this document's sense
and MAY report proofs to the hub to build standing there. A hub MAY
republish a `pay2seed.offer` as an `ipfile.pin` job on the c0mpute auction
when no other seeder takes it, which is how the market reaches the paid
storage network without the requester doing anything.
## 9. Events
CloudEvents as core §8, `source` the hub base:
| `type` | Data | Subscriber |
| --- | --- | --- |
| `pay2seed.attestation.registered`, `.honoured`, `.voided` | attestation id | requester |
| `pay2seed.offer.listed`, `.active`, `.settled`, `.voided` | offer id, status | requester, market followers |
| `paid2seed.lease.granted`, `.proven`, `.lapsed`, `.ended`, `.voided` | lease id, period, `earnedUsd` | seeder, requester |
| `paid2seed.challenge.issued` | the challenge record | seeder |
| `pay2seed.notice.received`, `.resolved` | notice id, outcome | requester |
## 10. Security notes
- **Sybil seeders.** One machine holding several leases on one offer under
several keys earns several times for one copy. `seeders.max` bounds the
loss; probes from distinct verifiers to the announced addresses, and a
hub refusing leases to keys that share a payout address on one offer,
bound it further. Standing makes it expensive to repeat.
- **Answer relaying.** A seeder that holds nothing could fetch challenged
pieces from the swarm within `deadline`. Ten minutes is the floor; a hub
SHOULD set shorter deadlines for small swarms, and probes measure the
seeder's own serving rather than what it can fetch.
- **Payout addresses are the identity that matters.** A seeder key is
cheap; the address it pays to is where a hub's Sybil checks bite.
## 11. Implementations
| Piece | Where | Status |
| --- | --- | --- |
| Reference hub, seeder side: leases, challenges, probes, receipts, standing | `profullstack/media-streamer` (bittorrented.com) | planned |
| Seeder client: market polling, leases, per-lease seed time, challenges | `torlink` (`torlnk serve` and `watch`) | planned; per-torrent seed time landed as baairon/torlink#186 |
| Shared seeder client and records | `@profullstack/pay2seed` | planned |
| Seeder and verifier on the storage network | `profullstack/c0mpute` via `ipfile.pin` | mapping in §8 |
## 12. Conformance
A **hub** is conformant on this side when it hands out leases as §3,
issues at least one challenge or probe per period and pays only proven
periods (§4, §5), and publishes seeder standing (§6.3). It MUST also be
conformant to `pay2seed`.
A **seeder client** is conformant when it does all of §7.
## 13. Version history
- 0.1 (2026-09-06): split out of the first pay2seed draft (2026-09-05) as
the server half; leases, challenges and probes, accrual and receipts,
the seeder API and client, the `ipfile.pin` mapping.

View file

@ -0,0 +1,276 @@
# paid2stream: The Server Protocol for Paid Live Streams
Status: 0.1 draft. Member of the [OpenSwarm](../openswarm.md) family.
Depends on the [core](./spec.md), [`iplive`](./iplive.md), [`ippay`](./ippay.md)
and [`paid2seed`](./paid2seed.md). Its client half is
[`pay2stream`](./pay2stream.md).
## 1. Scope
`paid2stream` is the side that gets paid to carry a live stream. It is
spoken by relays and gateways: peers on the `iplive` wire that take a
lease from a hub, stay online for a window, fan out segments, and, for a
gateway, turn the swarm into standard HLS for any player. It defines:
- **Leases** on a `pay2stream.offer`: one relay, one window, one price
per relay-hour.
- **Proof of relaying.** Presence proofs by a verifier that connects as a
peer and pulls segments, and a served-bytes ratio so a relay that is
online and idle is not paid the floor for nothing.
- **Settlement.** Relay-hour accrual, a gateway bonus, receipts, payout
through the `ippay` payee a relay already has for vouchers.
- **The gateway role**: the record, the HLS presentation in clear and
sealed modes, the M3U and EPG, gateway-bound vouchers, and what a
gateway must do when a channel is voided.
- **Regions and standing.**
Consent, offers, tickets and the viewer are [`pay2stream`](./pay2stream.md).
A hub implements both; a relay or gateway implements only this document.
### Non-goals
Deciding what a channel is. A relay sees a channel key, a basis, a
visibility and a price, and decides whether to carry it.
## 2. Terminology
- **Relay, viewer, channel, head, segment, part, epoch, origin.** As
`iplive` §2.
- **Gateway.** A relay that also speaks HTTP: presents the channel as HLS
and M3U, holds a grant, and signs vouchers on behalf of the HLS viewers
bound to it.
- **Relay-hour.** One relay, connected and serving, for one hour.
- **Period.** `offer.proof.everyMinutes` long.
## 3. Leases
A `paid2stream.lease` is a `paid2seed.lease` with `priceUsdPerRelayHour`
in place of the GiB-month price, `graceMinutes` (default 5) in place of
`graceHours`, a `window` copied from the offer, and a `gateway` boolean
set when the relay commits to §5.
`POST /relays` with `{ "offer", "relay", "regions", "gateway": bool,
"sig" }`, `sig` by the relay key over `"openswarm:paid2stream:lease:v1" ||
offer id`. `201` with the lease; `409` when no slot is free or the relay
already holds one for this window; `403` below the standing floor. A
relay MUST be online and announced in the channel's peer set within the
grace window, and holds at most one lease per channel window. When an
offer is voided every lease on it is `voided` and the hub pushes
`paid2stream.lease.voided`.
## 4. Proof and settlement
### 4.1 Presence
A leased relay is proven for a period when a verifier finds it, at its
announced address, serving the channel: the verifier connects as an
`iplive` peer, receives a `head` no older than two segments, requests one
segment at each rendition in `capacity.renditions`, and receives them
within one segment duration each. The verifier signs a
`paid2stream.proof` with `{ "lease", "period", "head", "segments",
"latencyMs" }`. A relay that is connected but answers `deny rate` to a
verifier fails the period.
For a gateway, the verifier also fetches the media playlist and one
segment over HTTP (§5.3) and records `hls: true`.
### 4.2 Served bytes
Vouchers a relay redeems for this channel during the window are the
second measure. A hub MAY require, per offer, a minimum ratio of served
bytes to pulled bytes for a period to count, so a relay that is present
and idle while others carry the viewers is not paid the floor for nothing.
The default ratio is 1.0 after the first two periods.
### 4.3 Accrual and receipts
```
earnedUsd = priceUsdPerRelayHour * (everyMinutes / 60) * (1 + gatewayBonusBps / 10000 if gateway)
```
per proven period, integer micro-USD, rounded down, credited to the
relay's `ippay` payee balance with a `paid2stream.receipt` shaped as
`paid2seed.receipt`. Two consecutive failed periods end the lease and
reopen the slot. Vouchers are separate and additive; the hub's fee is the
broadcaster's at purchase, never the relay's.
### 4.4 Regions and standing
A relay declares `regions` in its lease request, self-reported and
verified only in the sense that a verifier records the latency it saw.
Standing is `paid2seed` §6.3 with periods in place of days; a hub shows a
relay's median verified latency per region beside it.
## 5. Gateways and HLS
A gateway is what makes this usable by a person with a television. Any
relay MAY be one; a c0mpute node with `c0mpute:role:gateway` is expected
to be one; the reference hub runs one.
### 5.1 Record
```json
{
"openswarm": "0.1",
"type": "paid2stream.gateway",
"gateway": "ed25519:a41e…",
"base": "https://gw-eu1.example.net/openswarm",
"regions": ["eu"],
"modes": ["clear", "sealed"],
"lowLatency": true,
"hubs": ["https://bittorrented.com/api/openswarm"],
"createdAt": "2026-09-10T09:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:a41e…", "sig": "…" }]
}
```
Registered with `POST /gateways`; listed in `GET /gateways` and, per
channel, in the `gateways` array of `GET /streams/<id>`. An origin MAY
name preferred gateways in its channel record's `relays` block.
### 5.2 Presenting a pass
A gateway MUST accept the ticket three ways: `?pass=<base64url pass>` on
the URL, as a cookie, and as `Authorization: Bearer`. It verifies the
pass against the hub key and its scope (`ippay` §3.1), checks `expiresAt`,
and MUST rate-limit failed presentations by source address. A pass bought
against another gateway is valid here if the hub lists this gateway for
the channel; the pass is the hub's record.
### 5.3 HLS presentation
For a channel it relays, a gateway serves:
| Path | Body |
| --- | --- |
| `hls/<channel>/master.m3u8` | One `EXT-X-STREAM-INF` per rendition in the channel record, `CODECS` and `BANDWIDTH` from it, `EXT-X-INDEPENDENT-SEGMENTS`. |
| `hls/<channel>/<rendition>.m3u8` | A media playlist rebuilt from the latest signed head: `EXT-X-MAP` for the init segment, one `EXTINF` per segment in the DVR window, `EXT-X-DISCONTINUITY` at a key epoch boundary in `sealed` mode, `EXT-X-PROGRAM-DATE-TIME` from the head's timestamp. With `lowLatency`, `EXT-X-PART` entries from `iplive` parts and `EXT-X-SERVER-CONTROL` with `CAN-BLOCK-RELOAD=YES`. |
| `hls/<channel>/<rendition>/<seq>.m4s` | The segment. |
| `hls/<channel>/init/<rendition>.mp4` | The init segment. |
| `hls/<channel>/key/<epoch>` | `sealed` mode only: the 16-byte HLS key, to a pass holder. |
**`clear` mode.** The gateway decrypts each `iplive` segment with the
grant it holds and serves plaintext over TLS. Every IPTV player
understands it, and it is exactly as private as the TLS session and the
pass. A gateway MUST NOT serve `clear` for a channel whose record has
`keys.modes` without `gateway` in it; that is how an origin refuses to be
decrypted by anyone but viewers.
**`sealed` mode.** The gateway re-encrypts each segment under a
per-epoch AES-128-CBC key and writes
`EXT-X-KEY:METHOD=AES-128,URI="key/<epoch>"`, so the playlist is standard
HLS with standard encryption and the key endpoint is where the pass is
checked. Segments may then be cached by any HTTP cache in the path. The
re-encryption is the gateway's cost; the origin's content key never
leaves the `iplive` layer.
Both modes are one segment behind the peer edge; `lowLatency` narrows that
to one part.
### 5.4 The M3U and the EPG
`GET <base>/channels.m3u?pass=<pass>` returns `#EXTM3U` with one
`#EXTINF` per channel the pass is in scope for, carrying `tvg-id`,
`tvg-name`, `tvg-logo` and `group-title` from the channel record's
`title`, `logo` and `group` fields (added to `iplive.channel` as OPTIONAL
by this document), each pointing at that channel's `master.m3u8` with the
pass attached. `GET <base>/epg.xml?pass=` returns XMLTV built from the
`pay2stream.listing` entries of those channels. This pair is what every
IPTV app asks for on setup, and it is how a gateway looks, to a
television, like the provider it replaces.
### 5.5 Gateway-bound vouchers
A gateway signs vouchers to its upstream for what it pulls, as any relay
does, and signs vouchers to itself on behalf of each bound viewer for what
it serves them, bounded by that viewer's ticket `capUsd`. It MUST NOT
sign a voucher for bytes it did not serve; the hub compares signed bytes
against the segment count in the window, and the gateway's standing is
what it loses.
### 5.6 Stopping
On `pay2stream.channel.voided` a gateway MUST stop serving the channel
within one segment duration, answer its playlists with `410`, and drop
the channel from `channels.m3u`. A gateway that keeps serving a voided
channel is delisted and its standing zeroed.
## 6. Hub API, relay and gateway side
Relative to `pay2stream.base`. The client side is `pay2stream` §7.
| Method and path | Auth | Purpose |
| --- | --- | --- |
| `GET /streams?open&region` | none | Offers with free relay slots, and what each pays per hour. |
| `POST /relays` | relay signed | Take a lease (§3). |
| `GET /relays/<key>` | none | Standing, leases, median latency per region. |
| `GET /relays/<key>/leases?status` | none | What a relay resumes after a restart. |
| `POST /proofs` | verifier signed | Report a presence proof; returns the receipt. |
| `POST /gateways` | gateway signed | Register or update a gateway record. |
| `GET /passes/<id>` | none | Verify a presented ticket against the hub's record (`ippay` §5.2). |
| `POST /vouchers` | none | Redeem, as `ippay` §5.3; gateway-bound vouchers carry `boundBy`. |
| `POST /webhooks` | signed | Register a CloudEvents endpoint for a relay or gateway key. |
## 7. Relay and gateway client behaviour
1. Holds a relay key, registered as an `ippay` payee, stored as an
OpenCreds `key` item.
2. Polls `GET /streams?open` on its hubs, filtered by the operator's
policy: which `basis` values to carry, minimum price per hour,
regions, upload to keep in reserve.
3. Takes a lease, connects to the channel's peers using the offer's pass,
accepts up to `capacity.downstreamPerRelay` downstream peers, and
stays for the window.
4. As a gateway: holds a grant, serves §5.3 in at least `clear` mode,
§5.4, and the three pass presentations of §5.2.
5. Answers verifiers as ordinary peer traffic, and over HTTP for the
gateway probe.
6. On restart, resumes leases from `GET /relays/<key>/leases?status=proven`.
7. Stops on a void (§5.6). Shows the operator, per lease, the basis,
earned so far, viewers served, and the next period due.
## 8. Events
| `type` | Data | Subscriber |
| --- | --- | --- |
| `pay2stream.offer.listed`, `.active`, `.settled`, `.voided` | offer id | broadcaster, market followers |
| `paid2stream.lease.granted`, `.proven`, `.lapsed`, `.ended`, `.voided` | lease id, period, `earnedUsd` | relay, broadcaster |
| `pay2stream.channel.live`, `.ended`, `.voided` | channel key, gateways | relays, gateways, viewers with tickets |
| `pay2stream.listing.published` | listing id | market followers |
## 9. Security notes
- **Presence without service.** The served-bytes ratio (§4.2) is what
stops a relay from collecting the floor while doing nothing.
- **Gateway over-signing.** Bounded per viewer by `capUsd`, and in
aggregate by the hub's segment-count check and the gateway's standing.
- **Clear mode is clear.** A gateway is trusted with plaintext by the
origin's choice of `keys.modes`; an origin that does not want that
offers `sealed` only, or no gateways at all.
- **Relay regions are self-declared.** Only the verified latency beside
them is evidence.
## 10. Implementations
| Piece | Where | Status |
| --- | --- | --- |
| Reference hub, server side, and the hub's own gateway | `profullstack/media-streamer` (bittorrented.com), Live TV | planned; the IPTV resale rail (`/api/public/iptv/<slug>/stream`) already seals HLS manifests per session, the central version of §5.3 |
| Gateway on the storage network | `profullstack/c0mpute` `c0mpute:role:gateway`, DIP 0019 playlist rebuild | mapping; DIP 0019 already rebuilds playlists from swarmed segments |
| Relay client | `torlink` with the `iplive` extension | planned |
| Shared relay and gateway code | `@profullstack/pay2seed` (one package for the four protocols) | planned |
## 11. Conformance
A **hub** on this side: leases as §3, at least one presence proof per
period and pays only proven periods (§4), lists gateways per channel,
pushes `channel.voided` to relays and gateways.
A **relay**: `iplive` §12 as a relay, plus §7. A **gateway**: a relay,
plus §5.2, §5.3 in at least `clear` mode, §5.4, §5.5 and §5.6.
## 12. Version history
- 0.1 (2026-09-06): first draft, as the server half; leases per
relay-hour with a gateway bonus, presence proofs and the served-bytes
ratio, gateways with clear and sealed HLS, the M3U and EPG pair,
gateway-bound vouchers, stopping on a void.

554
docs/openswarm/pay2seed.md Normal file
View file

@ -0,0 +1,554 @@
# pay2seed: The Client Protocol for Paid Seeding
Status: 0.1 draft. Member of the [OpenSwarm](../openswarm.md) family.
Depends on the [core](./spec.md), [`ipfile`](./ipfile.md), [`ippay`](./ippay.md)
and [`ipdb`](./ipdb.md). Its server half is [`paid2seed`](./paid2seed.md).
## 1. Scope
`pay2seed` is the side that pays. It is spoken over HTTPS by a requester, a
person or a program that wants data kept alive on a swarm by machines they
do not control, to a hub. It defines:
- **Consent.** A signed attestation, made when the data is added, that the
requester has the right to distribute it and accepts notice. A hub MUST
NOT list an offer without one. This is the part BitTorrent never had.
- **Offers.** A request to store, archive or share a swarm for a period,
with a budget escrowed at the hub, a number of seeders wanted, and a
proof cadence. Public offers cover vanilla torrents and `ipdb` feeds;
private offers cover `ipfile` swarms whose ciphertext a seeder holds
without ever reading.
- **Encryption by default, and access.** A private swarm is `ipfile`
ciphertext under a key the requester holds. Who else may decrypt it is
a team: members the requester invites, and a keeper that issues grants
to them when the requester is offline. This is what a hub charges for.
Public is encryption switched off by an explicit act, and is listed and
kept like anything else.
- **The requester's view of the market**, of standing, and of notices.
Everything on the BitTorrent side, how a seeder takes a lease, proves it
holds and serves the pieces, and is paid, is [`paid2seed`](./paid2seed.md).
The two documents share one hub, one set of record types, and one
lifecycle; the split is who speaks which half. A hub implements both. A
requester client implements only this one. A seeder client implements
only the other.
`ipfile.pin` ([c0mpute](./c0mpute.md) §3.1) is this document's offer
carried on c0mpute's job auction; `paid2seed` §8 gives the mapping.
### Non-goals
Judging content. A hub sees an attestation, an infohash and, for a private
swarm, nothing else. It enforces that the attestation exists, is signed and
is honoured on notice; whether it is true is the requester's liability, and
§8 is how a false one is undone. Not a DRM. Not a token. Not a replacement
for `ippay` vouchers: a swarm that charges leechers keeps charging them.
## 2. Terminology
- **Requester.** The identity that posts an offer and funds it. Usually the
publisher of the swarm; for a public torrent it may be anyone who wants
it kept alive, subject to §3. A key, not necessarily a person: an
autonomous agent is a requester like any other (§5.8).
- **Seeder.** An identity that holds a lease and serves the swarm
(`paid2seed`).
- **Hub.** An `ippay` hub that also implements this document and
`paid2seed`. bittorrented.com is the reference hub.
- **Attestation.** The consent record (§3). **Offer.** The request (§4).
- **Period.** The unit of accrual and proof, `proof.everyHours` long.
- **Visibility.** `public` (a vanilla torrent or an `ipdb` feed) or
`private` (an `ipfile` swarm; ciphertext only).
Keys, records, signatures, amounts and times are as in the core.
## 3. Consent: the attestation
An attestation is the requester saying, under signature, what the data is
and why they may distribute it. It is made once per swarm, before the first
offer, and every offer for that swarm references it.
### 3.1 Record
```json
{
"openswarm": "0.1",
"type": "pay2seed.attestation",
"subject": { "infohashV1": "4330ddb304b78b1398a77297f57ea22c1ef3fb56", "infohashV2": null, "file": null },
"visibility": "public",
"basis": "own",
"license": "CC-BY-4.0",
"description": "Field recordings, Dartmoor, 2026",
"notice": "https://example.com/.well-known/pay2seed-notice",
"acceptsTakedown": true,
"createdAt": "2026-09-05T18:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:0d87…", "sig": "…" }]
}
```
| Field | Rule |
| --- | --- |
| `subject` | Exactly one of `file` (an `ipfile` file key; visibility MUST be `private`), `infohashV1`/`infohashV2` (at least one; visibility MUST be `public`), or `channel` (an `iplive` channel key; see `pay2stream` §3). A feed offer (§4.4) attests the feed key in `file`. |
| `visibility` | `public` or `private`. Fixed for the life of the swarm. |
| `basis` | Why the requester may distribute this. One of §3.2. |
| `license` | REQUIRED when `basis` is `open-license`: an SPDX identifier. |
| `description` | OPTIONAL, at most 280 characters, shown on public listings. |
| `notice` | An HTTPS URL or `mailto:` at which the requester receives notices (§8). REQUIRED for `public`. |
| `acceptsTakedown` | MUST be `true`. Present so the acceptance is in the signed bytes. |
| `readme`, `readmeSha256` | REQUIRED. The swarm's `README.md` and the hash of the copy inside the swarm (§5.6). No README, no listing. |
Signer: the requester key. For an `ipfile` subject the attestation MUST
also carry a signature by the file's publisher key, which is how a
stranger is prevented from attesting somebody else's private swarm. For a
public infohash no such proof of authorship exists; §3.3 stands in for it.
### 3.2 Basis values
| `basis` | Meaning | Allowed visibility |
| --- | --- | --- |
| `own` | The requester made it or holds the rights outright. | either |
| `licensed` | The requester holds a licence that permits redistribution. | either |
| `open-license` | Released under the SPDX licence in `license`. | either |
| `public-domain` | No rights subsist, or they were waived. | either |
| `personal` | The requester's own data, kept for themselves: backup, archive, sync. | `private` only |
A hub MUST reject an attestation whose `basis` and `visibility` disagree
with this table, and any with an unlisted `basis`. Hubs MAY refuse to list
particular bases; the hub record says which (§6.1).
### 3.3 Public infohashes and the claim window
Anyone can attest a public infohash, so the attestation is a claim. A hub
lists a public offer only after a **claim window** (`claimHours` in the
hub record, default 24) during which the infohash and the attestation are
visible at `GET /offers/<id>` and on the `pay2seed.offer.listed` event,
and a notice voids it. An offer for a swarm that already has an honoured
attestation from a different key is refused unless the new basis is
`open-license` or `public-domain`, the two that do not depend on who is
asking.
A requester's standing (`paid2seed` §6.3) records every attestation of
theirs that was voided. A hub SHOULD refuse new public attestations from a
key whose voided count exceeds its accepted count, and MUST refuse them
from a key with three or more voided in the last year.
## 4. Offers
An offer is money on the table for a swarm to be kept for a period. It is
bought from the hub as an `ippay` pass is, so the budget is settled before
anyone starts seeding.
### 4.1 Record
```json
{
"openswarm": "0.1",
"type": "pay2seed.offer",
"hub": "ed25519:c9f1…",
"requester": "ed25519:0d87…",
"attestation": "sha256:8e1c…",
"subject": { "infohashV1": "4330ddb304b78b1398a77297f57ea22c1ef3fb56", "infohashV2": null, "file": null, "feed": null },
"visibility": "public",
"sizeBytes": 7462331904,
"days": 30,
"seeders": { "min": 3, "max": 10 },
"priceUsdPerGibMonth": "0.150000",
"budgetUsd": "12.000000",
"proof": { "everyHours": 6, "verifiers": ["hub"] },
"pass": null,
"trackers": ["udp://tracker.opentrackr.org:1337/announce", "wss://tracker.openwebtorrent.com"],
"startsAt": "2026-09-05T18:00:00.000Z",
"expiresAt": "2026-10-05T18:00:00.000Z",
"payment": { "network": "eip155:8453", "nonce": "0x2f0a…" },
"createdAt": "2026-09-05T18:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:c9f1…", "sig": "…" }]
}
```
| Field | Rule |
| --- | --- |
| `hub` | The hub that sold it and holds the budget. Signer. |
| `requester` | Who paid, bound at purchase as `ippay` §3.3 binds a payer. |
| `attestation` | Record id of an attestation whose `subject` and `visibility` match. |
| `subject` | As the attestation, plus OPTIONAL `feed`: an `ipdb` feed key (§4.4). |
| `sizeBytes` | Ciphertext or payload size. For a feed, the size at posting. |
| `days` | Period the swarm is to be held from `startsAt`. |
| `seeders.min` | Leases before the offer is `active`; `max` caps them. |
| `priceUsdPerGibMonth` | What one seeder earns for the whole swarm for 30 days, pro rata. |
| `budgetUsd` | Escrowed. At least `max * priceUsdPerGibMonth * sizeBytes/2^30 * days/30`, rounded up. |
| `proof.everyHours` | Period length, 1 to 168. |
| `proof.verifiers` | `hub`, `requester`, or `c0mpute`. |
| `pass` | For a `private` swarm whose `pricing.perGib` is non-zero: an `ippay.pass` scoped to the file so a seeder can fetch as a paying peer. |
| `trackers` | Announce URLs the seeder MUST add. At least one `wss://` when browser peers should find it. |
| `startsAt`, `expiresAt` | `expiresAt` is `startsAt + days`. Leases never outlive it. |
| `payment.nonce` | The settlement that funded `budgetUsd`. |
### 4.2 Purchase
`POST /offers` with an unsigned draft (every field but `hub`, `payment`,
`sigs`, `createdAt`) and no `X-PAYMENT` header answers `402` with an x402
offer for `budgetUsd` plus the hub's fee (`paid2seed` §5.3), exactly as
`ippay` §3.2 answers a pass purchase, with the same `X-OpenSwarm-Payer`
binding. Repeating with a valid proof answers `201` with the signed offer.
A hub MUST verify the referenced attestation before quoting, and MUST
refuse a `private` offer whose `sizeBytes` disagrees with the manifest.
An offer is `pending` until `seeders.min` leases exist, `active` from then
until `expiresAt`, then `settled`. It is `voided` by a notice (§7) or by
the requester (`DELETE /offers/<id>`, signed), and the unearned budget is
refunded to the requester's payout address less what leases have accrued.
### 4.3 Public swarms
A `public` offer is a vanilla torrent. Nothing in the wire changes; the
only new thing on the network is another well behaved seeder that stays.
A podcast archive, a dataset, a software release, an out-of-print
recording with a clean basis: kept alive by strangers who are paid for it.
### 4.4 Public feeds
An offer whose `subject.feed` is set covers an `ipdb` feed: every segment
swarm the feed publishes, present and future, and the duty to keep the
feed's head reachable. What a leased seeder must do about it is
`paid2seed` §7. `sizeBytes` is the feed's size at posting; each new segment
joins the accrual base at the period it is proven, and the hub MUST NOT
hand out leases whose projected accrual exceeds `budgetUsd`. The requester
tops up with a further offer for the same feed.
### 4.5 Private swarms
A `private` offer is an `ipfile` swarm. The seeder holds ciphertext and,
without a grant, cannot read a byte of it. `personal` is the basis for a
backup nobody else is ever meant to read; its `pricing` SHOULD be zero and
its manifest SHOULD not be catalogued. Where the manifest has `keys.public`
set the data is readable by anyone with the manifest and the offer is
still `private` in this document's sense: the attestation says the
requester may store it.
## 5. Encryption by default, and who may decrypt
### 5.1 The default
A client adding data through `pay2seed` encrypts it. The default swarm is
an `ipfile` swarm: AES-256-CTR over pieces under a content key the
requester's publisher key derives and holds (`ipfile` §4). Nothing about
the data, not its name, not its size to the byte, not a single block,
is readable by a seeder, a tracker, the DHT, or the hub. `visibility`
is `private`.
The client encrypts. The hub never does, never holds a content key it
was not delegated as keeper, and never sees plaintext; what it manages
is access to keys, not the bytes.
Encryption is a choice the requester makes off, not on. A requester who
wants the world to have the data sets `visibility` to `public`: the
swarm is a vanilla torrent and there is no content key. A public swarm
is not a fallback and not a second class: it is attested, listed, kept
by paid seeders and rendered on the hub exactly as a private one is. The
only difference is that anyone who finds it, on the hub or on the DHT,
can read it. A client MUST make the default private, MUST make turning
it off an explicit act, and MUST say plainly what off means: anyone.
### 5.2 Access is the product
What a private swarm needs, and what BitTorrent never had, is a way for
the requester to say who else may decrypt it, and for that to keep
working when the requester's laptop is closed. That is a **team**: a
named set of member keys held at the hub, with a policy over which of
the requester's swarms each member may receive a grant for. The hub, or
a keeper it delegates to (`ipfile` §3.2 `delegate`, c0mpute's keeper
role), issues the grant when a member asks. The requester never has to
be online for a colleague to open a file, and never has to hand anyone
the content key itself.
Seeding is priced at the cost of disk. Teams are where a hub earns:
seats, groups, an organisation with many groups, audit, and the
guarantee that a grant is there at three in the morning.
### 5.3 Records
```json
{
"openswarm": "0.1",
"type": "pay2seed.team",
"hub": "ed25519:c9f1…",
"owner": "ed25519:0d87…",
"name": "field-recordings",
"members": [
{ "key": "ed25519:a1b2…", "box": "x25519:4c40…", "role": "admin", "since": "2026-09-06T10:00:00.000Z" },
{ "key": "ed25519:c3d4…", "box": "x25519:9e11…", "role": "member", "since": "2026-09-06T10:05:00.000Z" }
],
"scope": { "files": ["*"], "publishers": ["ed25519:0d87…"] },
"keeper": "ed25519:c9f1…",
"createdAt": "2026-09-06T10:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:0d87…", "sig": "…" }]
}
```
| Field | Rule |
| --- | --- |
| `owner` | The requester key. Signer. Only the owner and `admin` members may change the record; every change is a new signed record and the hub keeps the history. |
| `members[].key`, `box` | The member's signing key and the X25519 box key a grant is sealed to (core §4.4). |
| `members[].role` | `admin` (may invite and remove), `member` (may receive grants), `readonly` (may receive grants for files the team marks so). |
| `scope` | As a pass's scope (`ippay` §3.1): which file keys or publisher keys members may be granted. `["*"]` files under the owner's publisher key is the common case. |
| `keeper` | Who issues grants: the hub's key, or a keeper the owner has delegated to. The owner's publisher key remains able to grant directly. |
An **invitation** is `pay2seed.invite`: `{ "team", "to": <email, phone,
ipname or key>, "role", "expiresAt" }`, signed by an admin. It travels
out of band (the hub emails or messages it) and is redeemed with a key:
the invitee's client mints one if they have none, and the hub adds the
member. An invite to a key that already exists is added at once. An
invitation MUST expire, 7 days by default.
A **grant** to a member is an `ipfile.grant` (`ipfile` §3.2) with
`delegate: true` and `pass: null`, sealed to the member's `box`, issued
by the keeper. It is what an `ippay` pass buys for a stranger, handed
instead to a member because the team says so.
### 5.4 Removal and rotation
Removing a member ends future grants at once. It does not, and cannot,
unread what they have already read; a grant they hold for a file still
decrypts that file's current key epoch. A team with `rotateOnRemove:
true` (the default) has the hub trigger `ip file reencrypt` on the files
in scope (`ipfile` §11, a new content key and a new swarm), so the next
version is closed to them. A client MUST show this distinction to the
owner rather than imply removal is retroactive.
### 5.5 What the hub charges
A hub prices teams as it likes and says so in its record:
```json
"teams": { "freeMembers": 3, "seatUsdPerMonth": "2.000000", "orgUsdPerMonth": "50.000000", "keeperBps": 500 }
```
`freeMembers` seats per owner cost nothing, so a person sharing with
their family or a three-person shop never pays for access. Above that a
seat is billed monthly, an organisation (many teams, one bill, shared
admins) at a flat rate, and `keeperBps` is the hub's share when it acts
as keeper for a paid file (`ipfile` §7). All of it is settled through
the same pay plugins the rest of the family uses, CoinPay and x402 over
the owner's `ippay` payee, never a card form of the hub's own.
Both sides earn. A seeder rents out disk through `paid2seed`. A requester
sells access: to strangers through `ippay` passes at the swarm's
`keyUsd` and `perGib`, to colleagues through a team. A hub takes its
`hubBps` on payments that cross it, **1 percent at the reference hub**
(`ippay` §5.1), plus its seat fees on teams, and nothing at all on the
seeder's floor. Public swarms are not charged for; they carry an ad
instead (§5.7).
### 5.6 The README
Every swarm on the market MUST contain a `README.md` at its root. No
exceptions: an attestation without one is refused, and a hub that lists
a swarm without one is not conformant. The README is the swarm's face:
what this is, who made it, what a buyer or a member gets, how to use it.
The attestation carries it, so the hub can show it without a key:
| Field | Rule |
| --- | --- |
| `readme` | The README's Markdown, UTF-8, at most 64 KiB. Signed with the rest of the attestation. |
| `readmeSha256` | SHA-256 of the exact bytes of `README.md` inside the swarm. For a private swarm, of the plaintext; a member with a grant MAY verify it. |
The hub renders `readme` as HTML on the swarm's page: CommonMark with
GFM tables and fenced code, sanitised, no raw HTML, no scripts, images
only from the swarm itself or over HTTPS. A relative link resolves to a
file in the swarm and is gated the same way the file is: a public swarm's
link downloads, a private swarm's link asks for a pass or a grant. That
page, on bittorrented.com, is the listing; the DHT crawl's bare
infohashes never had one, and this is what a consented swarm looks like
beside them.
### 5.7 Public swarms pay for themselves
Public distribution is free to the requester and free to the fetcher, and
it is not charity. A public swarm has a page on the hub, built from the
README (§5.6), and that page carries an advertisement. The hub earns from
the page; the swarm's bytes are never metered, never gated and never
counted.
This is the trade, stated so nobody has to guess at it:
| | Public | Private |
| --- | --- | --- |
| Bytes | free, over the ordinary wire and the DHT | `ipfile` ciphertext, `perGib` and `keyUsd` as the manifest says |
| Who may read it | anyone | pass holders and team members |
| What the hub earns | the ad on the swarm's page | its percentage of each payment, plus seats (§5.5) |
| What the requester pays | nothing, unless they buy an offer to keep it alive | nothing, unless they buy an offer or seats |
A requester who wants no advertisement on their page pays for a seat
(§5.5); a hub MUST make that switch available rather than making the ad
the price of being listed at all. An ad never appears inside a swarm, is
never injected into a file, and never rides the wire: it is on the hub's
page about the swarm and nowhere else. `ipdb` entries and the market API
carry no advertising, so a client that reads the catalogue directly sees
none of it.
### 5.8 An agent may be a requester
Nothing in this family assumes a person. A requester is a key, and an
autonomous agent holds keys as readily as anyone: it can attest what it
made, publish a swarm, price access to it, sell tickets to a live
channel, take payment through its own `ippay` payee, and spend what it
earns buying offers so its own work stays online. The same is true on the
other side: an agent with disk can hold leases and be paid for them
(`paid2seed` §7).
Two rules, and they are the same rules a person gets:
- **The attestation is the agent's own claim**, signed by the agent's key,
and its standing (§3.3) is the agent's. An agent that attests what it
had no right to distribute loses standing exactly as a person does, and
a hub applies §3.3's refusals without asking which kind of requester it
is looking at.
- **Consent does not become weaker because a machine gave it.** An agent
publishing work made from somebody else's material has the same bases
available and no others; `own` means the agent or its operator holds
the rights, and there is no basis meaning "a model produced this, so
nobody owns it".
A hub MAY require an agent's attestations to name a responsible operator
key alongside the agent's own, and the reference hub does for `public`
swarms: an agent can sell freely, and somebody remains reachable when a
notice arrives.
### 5.9 API
| Method and path | Auth | Purpose |
| --- | --- | --- |
| `POST /teams` | owner signed | Create or replace a team record. |
| `GET /teams/<id>` | member signed | The record, its history, the files in scope. |
| `POST /teams/<id>/invites` | admin signed | Issue an invitation; the hub delivers it. |
| `POST /invites/<id>/accept` | invitee signed | Redeem with a key; returns the updated team. |
| `DELETE /teams/<id>/members/<key>` | admin signed | Remove; triggers rotation when the team says so. |
| `GET /teams/<id>/grant?file=` | member signed | A sealed grant for a file in scope, from the keeper. `403` outside scope or after removal. |
| `GET /teams/<id>/audit?since` | admin signed | Who was granted what, when. |
## 6. The requester's hub
### 6.1 Hub record
An `ippay.hub` record that offers this family adds:
```json
"pay2seed": {
"base": "https://bittorrented.com/api/openswarm/pay2seed",
"bases": ["own", "licensed", "open-license", "public-domain", "personal"],
"claimHours": 24,
"minStanding": 0,
"verifiers": ["hub", "c0mpute"],
"maxDays": 365
}
```
`bases` is what this hub will list. `minStanding` is the floor a seeder
needs to take a lease (`paid2seed`). A hub MAY run a market for private
backups only, or for open data only, by narrowing `bases`.
### 6.2 API, requester side
All bodies JSON, records verified on receipt, paths relative to `base`.
The seeder side of the same API is `paid2seed` §6.1.
| Method and path | Auth | Purpose |
| --- | --- | --- |
| `POST /attestations` | signed record | Register. `201` with its id; `409` if one is honoured for the subject by another key (§3.3). |
| `GET /attestations/<id>` | none | The record and `{ "status": "claimed|honoured|voided", "notices": n }`. |
| `POST /offers` | x402 + `X-OpenSwarm-Payer` | Buy an offer (§4.2). |
| `GET /offers?visibility&basis&minPrice&maxSize&feed&status` | none | The market, newest first, paged. What a seeder client reads; what a requester watches. |
| `GET /offers/<id>` | none | The offer, its status, leases held, slots free, periods proven per lease. |
| `DELETE /offers/<id>` | requester signed | Void and refund the unearned budget. |
| `GET /requesters/<key>` | none | `{ "standing", "attestations", "voided", "offers" }`. |
| `POST /notices` | signed | A notice against an attestation (§7). |
| `POST /webhooks` | signed | Register a CloudEvents endpoint for a requester key. |
## 7. Requester client behaviour
A requester client (the `ip seed` commands in [cli.md](./cli.md), a web
app such as bittorrented.com's upload form) does the following:
1. Holds a requester key, stored as an OpenCreds `key` item, registered
as an `ippay` payee so refunds have somewhere to go.
2. Makes the attestation before anything else, and MUST NOT let a user
post an offer without choosing a `basis` and, for `public`, a `notice`
endpoint. The basis list is a choice, never a default.
2b. Encrypts by default (§5.1). Turning encryption off is an explicit
act with its consequence stated. Shares a private swarm by inviting
members to a team (§5.3), never by handing out a content key.
3. Buys offers only against attestations the key actually signed.
4. Includes a pass in a `private` offer when the manifest charges per GiB.
5. Watches `GET /offers/<id>` or the events for `active`, `lapsed` leases
and `settled`, and shows the user seeders held out of wanted, periods
proven, and spend against the budget.
6. Receives notices at the endpoint it named, and shows them.
## 8. Notices and takedown
Anyone may `POST /notices` against an attestation:
```json
{
"openswarm": "0.1",
"type": "pay2seed.notice",
"attestation": "sha256:8e1c…",
"kind": "rights|illegal|personal-data|other",
"claimant": { "name": "…", "contact": "mailto:…" },
"statement": "…",
"createdAt": "2026-09-06T09:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:77aa…", "sig": "…" }]
}
```
On a notice the hub MUST forward it to the requester's `notice` endpoint
and MUST, within `claimHours`, either void the attestation or record that
it was reviewed and stands. Voiding an attestation voids every offer that
references it: leases end (`paid2seed` §3.2), the requester is refunded
the unearned budget less accrued earnings, and their `voided` count rises.
A hub that voids on every notice without review is conformant; a hub that
never voids is not.
For a `private` swarm the hub cannot inspect the content and does not
pretend to. What it has is the requester's identity, attestation and
standing, which is what accountability looks like for ciphertext.
## 9. Security notes
- **Attestation is a claim.** §3.3's window, standing and notices are the
defence. A hub that lists a public offer the moment it is paid is not
conformant.
- **Hub as escrow.** The budget sits with the hub for `days`, as a pass
buyer's `capUsd` does for 30. The hub's signed receipts and refunds are
its auditable trail.
- **Private means ciphertext, not secret existence.** An offer reveals to
its seeders the file key, the size and the requester's key. `personal`
swarms SHOULD use a fresh unrelated file pair (core §4.2) so backups are
not linkable to a publisher's catalogue.
## 10. Implementations
| Piece | Where | Status |
| --- | --- | --- |
| Reference hub, requester side: attestations, offers, market, notices | `profullstack/media-streamer` (bittorrented.com) | planned; the seedbox rental rail is the pay-per-pass half today |
| Requester client and shared records | `@profullstack/pay2seed` | planned |
| Requester CLI | `ip seed …` ([cli.md](./cli.md)) | proposed |
## 11. Conformance
A **hub** is conformant on this side when it refuses offers without an
honoured attestation (§3), enforces the basis table and claim window
(§3.2, §3.3), escrows the budget before listing (§4.2), publishes the
market and requester standing (§5.2), and forwards and resolves notices
within `claimHours` (§7). It MUST also be conformant to `paid2seed`.
A **requester client** is conformant when it does all of §6.
## 12. Version history
- 0.1 (2026-09-06): split out of the first pay2seed draft (2026-09-05) as
the client half; attestation, offers, requester API, notices. Same day:
encryption by default, teams, invitations, member grants, rotation on
removal, and the hub's pricing for access. Then: the 1 percent reference
fee, the advertisement that pays for public swarms, and agents as
requesters.

View file

@ -0,0 +1,260 @@
# pay2stream: The Client Protocol for Paid Live Streams
Status: 0.1 draft. Member of the [OpenSwarm](../openswarm.md) family.
Depends on the [core](./spec.md), [`iplive`](./iplive.md), [`ippay`](./ippay.md)
and [`pay2seed`](./pay2seed.md). Its server half is
[`paid2stream`](./paid2stream.md).
## 1. Scope
`pay2stream` is the side that pays for a live stream. Two parties speak it
over HTTPS to a hub and to a gateway:
- **A broadcaster**, who attests a channel (consent), buys guaranteed
relay capacity for a window, publishes listings, and sets ticket prices.
- **A viewer**, who buys a ticket and watches, either as an `iplive` peer
or, out of the box, on any player that speaks **HLS**: VLC, TiviMate,
Kodi, hls.js, an Apple TV, a Smart TV app. The viewer never installs
anything new. What they get is a URL with a pass on it, and an M3U plus
an EPG that look, to a television, like the provider they replace.
The BitTorrent side, how a relay takes a lease, proves it is online and
serving, runs a gateway that turns swarmed segments into HLS, and is paid,
is [`paid2stream`](./paid2stream.md). One hub implements both. A player
implements nothing; that is the point.
### Prior art
Ace Stream (TorrentStream, 2013) proved BitTorrent carries live television
to millions of viewers at once. It never paid a relay, never recorded
consent, and never opened its engine. `pay2stream` and `paid2stream` are
the open answer to the same problem with those three things added.
### Non-goals
Encoding and ingest (`iplive` §6, c0mpute DIP 0019). Chat, ads, a guide
format beyond the M3U attributes every IPTV player already reads. DRM past
the key a viewer is granted; a gateway serving clear HLS is serving
plaintext to whoever holds the pass, and this document says so.
## 2. Terminology
- **Origin, relay, viewer, channel, head, segment, part, epoch.** As
`iplive` §2.
- **Broadcaster.** The identity that attests the channel and buys relay
offers. Usually the publisher of the `iplive.channel`.
- **Gateway.** A relay that presents the channel over HTTP as HLS and M3U
(`paid2stream` §5).
- **Ticket.** An `ippay.pass` scoped to one or more channel keys, with a
window: a game, a day, a month.
- **Listing.** A signed `ipdb` entry saying what is on when, at what price.
## 3. Consent
A channel is listed on a hub only against a `pay2seed.attestation`
(`pay2seed` §3) whose `subject` names the channel:
```json
"subject": { "channel": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d" }
```
`visibility` is `public` for a channel any viewer may buy, `private` for
one whose tickets a broadcaster hands out themselves. `basis` is any value
from `pay2seed` §3.2 except `personal`. The attestation MUST be signed by
the channel key and the publisher key, as the channel record itself is
(`iplive` §3.1).
Two things are stated because they are where live streaming has always
gone wrong:
- A broadcaster relaying a feed they receive under a subscription that
forbids redistribution has no basis in the table and MUST NOT attest
`licensed`. A hub that learns of it treats the attestation as false.
- A broadcaster with a licence that does permit redistribution (a
league's own OTT rights, a broadcaster's own channel, a creator's own
camera) attests `licensed` or `own` and names the licence in
`description`. That is the entire difference between an IPTV reseller
and a pirate, and it is now a signed record on a public standing.
The claim window (`pay2seed` §3.3) applies to a public channel before its
first listing, not to each broadcast.
## 4. Relay offers
A broadcaster buys relays the way a requester buys seeders: an offer with
a budget escrowed at the hub, so a stream has relays before its first
viewer arrives and keeps them when vouchers alone would not.
```json
{
"openswarm": "0.1",
"type": "pay2stream.offer",
"hub": "ed25519:c9f1…",
"broadcaster": "ed25519:5d29…",
"attestation": "sha256:8e1c…",
"channel": "ed25519:0d87…",
"window": { "startsAt": "2026-09-12T18:30:00.000Z", "endsAt": "2026-09-12T22:00:00.000Z" },
"relays": { "min": 4, "max": 12 },
"regions": ["eu", "us-east"],
"capacity": { "downstreamPerRelay": 8, "renditions": ["1080p", "720p"] },
"priceUsdPerRelayHour": "0.050000",
"gatewayBonusBps": 5000,
"budgetUsd": "4.200000",
"proof": { "everyMinutes": 10, "verifiers": ["hub", "broadcaster"] },
"pass": { "…": "an ippay.pass scoped to the channel, so relays can pull" },
"payment": { "network": "eip155:8453", "nonce": "0x2f0a…" },
"createdAt": "2026-09-10T09:00:00.000Z",
"sigs": [{ "alg": "ed25519", "key": "ed25519:c9f1…", "sig": "…" }]
}
```
| Field | Rule |
| --- | --- |
| `attestation` | An honoured attestation for `channel` (§3). |
| `window` | When relays are wanted. A standing channel MAY post rolling 24 h windows. |
| `relays.min`, `max` | Leases before the offer is `active`; the cap. |
| `regions` | OPTIONAL. Region labels a relay self-declares; the hub prefers matching relays and never refuses others. |
| `capacity.downstreamPerRelay` | The `iplive` `maxDownstream` a leased relay MUST accept. |
| `priceUsdPerRelayHour` | The floor a relay earns per proven hour online, on top of vouchers. |
| `gatewayBonusBps` | OPTIONAL. Extra floor, in basis points of the hourly price, for a relay that also runs a gateway. |
| `budgetUsd` | Escrowed: at least `max * priceUsdPerRelayHour * hours * (1 + gatewayBonusBps/10000)`. |
| `proof.everyMinutes` | Period length, 5 to 60. |
| `pass` | So a leased relay can pull from upstream as a paying peer; the upstream `perGib` cost is the broadcaster's, not the relay's. |
Purchase, lifecycle, voiding and refund are as `pay2seed` §4.2, over x402
at `POST /streams`.
## 5. Tickets and listings
A **ticket** is an `ippay.pass` with `scope.channels` (added to the pass
record by this document; the hub verifies it as it verifies
`scope.files`) and a window: the hub sells `expiresAt` as short as one
hour for a channel whose record sets `ticket.minHours`, and as long as its
`passDays`. Pricing is the channel's `keyUsd` per grant plus `perGib`
served; a broadcaster who wants a flat ticket sets `perGib` to zero and
`keyUsd` to the ticket price, and the pass's `capUsd` to zero. Nobody
pays for bytes twice.
A **listing** is an `ipdb` entry of type `pay2stream.listing`: channel,
title, start, end, description, and the ticket price at the time, signed
by the channel key. Listings are what a market page and an EPG are built
from, and what a viewer buys against.
A channel whose `keyUsd` and `perGib` are both zero is **free to watch**:
no ticket, no grant, and a gateway serves it to anyone. Its page on the
hub carries an advertisement the same way a public swarm's does
(`pay2seed` §5.7), which is what pays for listing it. The stream itself
is never interrupted: nothing is injected into a segment, into the
playlist, or into the M3U. A broadcaster selling tickets pays the hub its
percentage instead, 1 percent at the reference hub, and their page
carries no advertising.
## 6. Watching
### 6.1 As a peer
`iplive` §7: resolve the channel, buy or reuse a ticket with `grant: true`,
connect to two peers, pull segments, send vouchers. This is the browser
player on bittorrented.com and the `ip live watch` command.
### 6.2 On any HLS player
An HLS player cannot sign a voucher, so the viewer's ticket is bought with
a gateway's key as `payer` (`ippay` §3.3) and the viewer's own identity
recorded by the hub as `boundBy`. The gateway then signs vouchers on the
viewer's behalf, bounded by the ticket's `capUsd`, which is the whole of
the viewer's exposure. A viewer who disputes a gateway's spend has the
hub's `GET /passes/<id>` record and the gateway's standing.
The ticket reaches the player as a bearer token in the URL, because that
is the only credential an IPTV player can carry:
```
https://gw-eu1.example.net/openswarm/hls/<channel key>/master.m3u8?pass=<base64url pass>
```
Gateways also accept the pass as a cookie and as `Authorization: Bearer`
for browser players (`paid2stream` §5.2).
The pair every IPTV app asks for on setup:
```
https://gw-eu1.example.net/openswarm/channels.m3u?pass=<pass>
https://gw-eu1.example.net/openswarm/epg.xml?pass=<pass>
```
The M3U lists every channel the ticket is in scope for, with `tvg-id`,
`tvg-name`, `tvg-logo` and `group-title`; the EPG is XMLTV built from the
listings. Point TiviMate or Kodi at those two and it is done. Latency is
one segment behind the peer edge, one part with low latency.
Which gateway? `GET /streams/<id>` lists the gateways currently relaying
the channel with their regions and median verified latency; a hub's own
gateway is always among them. A viewer client picks the nearest, and a
ticket bought against one gateway is presentable at any gateway the hub
lists for the channel, since the pass is the hub's record, not the
gateway's.
## 7. Hub API, client side
Relative to a `pay2stream.base` the hub adds to its `ippay.hub` record as
`pay2seed` adds its own. The relay and gateway side is `paid2stream` §6.
| Method and path | Auth | Purpose |
| --- | --- | --- |
| `POST /streams` | x402 + `X-OpenSwarm-Payer` | Buy a relay offer (§4). |
| `GET /streams?live&region&basis&minPrice` | none | Channels currently live with their gateways and listings; offers with free relay slots. |
| `GET /streams/<id>` | none | The offer, leases, gateways, the latest listing. |
| `DELETE /streams/<id>` | broadcaster signed | Void and refund the unearned budget. |
| `POST /listings` | channel signed | Publish a `pay2stream.listing` (also replicated on the channel's `ipdb` feed). |
| `GET /tickets?payer&channel&hours` | x402 + `X-OpenSwarm-Payer` | Buy a ticket (§5). `payer` MAY be a gateway key with the viewer's own key as `X-OpenSwarm-Bound-By`. |
| `GET /gateways?region&channel` | none | Gateways, and which channels each currently relays. |
| `POST /notices` | signed | As `pay2seed` §8; voiding ends every lease and delists every gateway for the channel. |
## 8. Client behaviour
A **broadcaster client** (`ip live` plus `ip stream offer` and
`ip stream ticket` in [cli.md](./cli.md); bittorrented.com's Live TV rent-out
form) attests before listing, buys offers only against its own attestation,
includes a pass in the offer, publishes a listing for every window, and
receives notices at its endpoint.
A **viewer client** (a web page, `ip live watch`, or a plain IPTV app with
a URL pasted in) buys or reuses a ticket, prefers a gateway by region, and
MUST NOT send a ticket to a gateway the hub does not list for that channel.
## 9. Security notes
- **Clear HLS is clear.** A ticket holder can capture the stream. So could
a peer with a grant. The spec does not pretend otherwise.
- **Pass in the URL.** It is a bearer token in a log line on every proxy
in the path. Hubs SHOULD sell short tickets to gateway-bound viewers and
viewers SHOULD prefer the cookie form where a player supports it.
- **Gateway as payer.** The viewer's exposure is `capUsd`; the gateway's
incentive to over-sign is bounded by its standing and by the hub
comparing signed bytes with the segment count in the window.
- **A stolen feed with a signed attestation** is the failure this spec
moves from "impossible to know" to "a named key, a public standing, and
a notice that ends it".
## 10. Implementations
| Piece | Where | Status |
| --- | --- | --- |
| Reference hub, client side: attestations, offers, tickets, listings, market | `profullstack/media-streamer` (bittorrented.com), Live TV | planned; the IPTV resale rail sells per-game passes today, centrally |
| Viewer in the browser | bittorrented.com, tipoffwatch, genrewatch players (hls.js, mpegts.js) | no change for HLS; `iplive` peer mode planned |
| Players | VLC, TiviMate, Kodi, Apple, Android TV | no work |
| Broadcaster CLI | `ip stream …` ([cli.md](./cli.md)) | proposed |
## 11. Conformance
A **hub** on this side: everything `pay2seed` §11 requires of it, over
channels; sells tickets bound to gateways; lists gateways per channel.
A **broadcaster client** and a **viewer client**: §8.
## 12. Version history
- 0.1 (2026-09-06): first draft, as the client half; consent for
channels, relay offers, tickets and listings, watching as a peer or on
any HLS player.

View file

@ -502,6 +502,19 @@ Member conformance is defined per document; the checklist is collected in
| `ipvideo.title` | publisher key | ipvideo | | `ipvideo.title` | publisher key | ipvideo |
| `iplive.channel`, `iplive.head` | channel key | iplive | | `iplive.channel`, `iplive.head` | channel key | iplive |
| `ipname.pin` | publisher key | ipname | | `ipname.pin` | publisher key | ipname |
| `pay2seed.attestation` | requester key (and the file's publisher key, or the channel key, for `ipfile` and `iplive` subjects) | pay2seed |
| `pay2seed.offer` | hub key | pay2seed |
| `pay2seed.notice` | claimant key | pay2seed |
| `pay2seed.team` | owner key (or an admin member) | pay2seed |
| `pay2seed.invite` | an admin member | pay2seed |
| `paid2seed.lease`, `paid2seed.receipt` | hub key | paid2seed |
| `paid2seed.challenge` | verifier key | paid2seed |
| `paid2seed.proof` | seeder key, or verifier key for a probe | paid2seed |
| `pay2stream.offer` | hub key | pay2stream |
| `pay2stream.listing` | channel key | pay2stream |
| `paid2stream.lease`, `paid2stream.receipt` | hub key | paid2stream |
| `paid2stream.proof` | verifier key | paid2stream |
| `paid2stream.gateway` | gateway key | paid2stream |
## 12. Version history ## 12. Version history

View file

@ -203,7 +203,7 @@ describe("MCP: OpenPRD", () => {
it("reports the next free id and the allowed lifecycle moves", async () => { it("reports the next free id and the allowed lifecycle moves", async () => {
const client = await connect(); const client = await connect();
// Asserted against the live prd/ directory, so this advances with every PRD added. // Asserted against the live prd/ directory, so this advances with every PRD added.
expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0006"); expect(toolText(await client.callTool({ name: "prd_next_id", arguments: {} }))).toBe("0007");
const moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } }); const moves = await client.callTool({ name: "prd_next_statuses", arguments: { ref: "0001" } });
const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] }; const payload = JSON.parse(toolText(moves)) as { status: string; allowedNext: string[] };

View file

@ -0,0 +1,246 @@
---
openprd: "0.2"
id: "0006"
title: "Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family"
status: Draft
authors:
- anthony@profullstack.com
created: 2026-09-05
updated: 2026-09-05
repo: profullstack/logicsrc
discussion:
implementation:
tags:
- openswarm
- pay2seed
- paid2seed
- pay2stream
- paid2stream
- iplive
- hls
- ipfile
- ippay
- ipdb
- bittorrent
- torlink
- bittorrented
- c0mpute
supersedes:
superseded-by:
---
## Problem
BitTorrent is the best distribution network ever built and it has never
been a legitimate business, because two things are missing from the
protocol. Nobody is paid to seed, so a swarm lives exactly as long as
somebody feels like leaving a client open. And nothing about an upload says
who put it there or whether they were allowed to, so every index is a pile
of unattributed infohashes and every seeder is presumed to be doing
something wrong.
OpenSwarm (PRD 0005) fixed the first half for encrypted media: `ippay` pays
a seeder per verified piece served. It does not pay anyone to *stay*. A
piece that nobody downloads this month earns nothing, so an archive, a
backup, a dataset waiting for its buyer or a podcast's back catalogue is
exactly as unfunded as it was before. `ipfile.pin` on c0mpute pays a
storage node to hold a swarm, but only a c0mpute node, only for a private
`ipfile` swarm, and only through c0mpute's auction.
The second half, consent, is not addressed anywhere. bittorrented.com
indexes what the DHT crawl finds and runs a seedbox rental rail, and
neither has a place for "I made this, here is the licence, here is where to
send a notice". A user of ours who wants to share their own work, keep an
off-site copy of their own data, or seed an openly licensed dataset for
money has no way to say so that anyone else can check.
## Naming
`pay2*` is the client protocol: the side that pays, over HTTPS, and plays.
`paid2*` is the server protocol: the BitTorrent side that earns. One hub
implements both halves of a pair; a requester or viewer client implements
only `pay2*`; a seeder, relay or gateway implements only `paid2*`. The
first draft of pay2seed carried both halves in one document and was split
the same day.
## Goals
- **Consent is part of the upload.** A signed attestation with a fixed set
of bases (`own`, `licensed`, `open-license`, `public-domain`, `personal`)
and a notice endpoint, without which no hub lists the swarm. Public
claims get a claim window and a public standing; false ones are voided
on notice and count against the requester.
- **Anyone can pay to have anything kept.** An offer escrows a budget at a
hub for a swarm to be held for N days by M seeders at a price per
GiB-month. Public offers cover vanilla torrents and `ipdb` feeds; private
offers cover `ipfile` ciphertext.
- **Anyone can be paid to keep it.** A seeder takes a lease, proves each
period by challenge or probe, and is paid per proven period through the
same `ippay` payee it already has for vouchers. torlink's headless daemon
is the reference client; a laptop qualifies.
- **The market is one HTTP surface.** `GET /offers` is what a client picks
work from. bittorrented.com is the reference hub.
- **Encryption on by default, access as the product.** Private swarms are
the default; teams, invitations, member grants via the hub as keeper,
rotation on removal, and per-seat pricing are in the spec. Public is
opt-out and rides the DHT for free.
- **Reuse.** Offers are bought like passes (x402, `X-OpenSwarm-Payer`),
payouts are `ippay` §5.6, challenges are c0mpute's storage challenge,
feeds are `ipdb`, events are the family envelope. One new document, six
new record types, no new primitive.
## Business model
Encryption is on by default: every private swarm is `ipfile` ciphertext
under a key the requester holds. Seeding is priced at the cost of disk and
is not where the money is. **Access is.** A requester invites team,
group or organisation members, and the hub (as keeper) issues them grants
to decrypt, at three in the morning, when the requester is offline, with
an audit trail and rotation on removal. A few seats are free so a family
or a three-person shop never pays; above that, seats and organisations
are billed monthly through the same CoinPay rail as everything else.
Public distribution is encryption switched off: a vanilla torrent that
the DHT finds for nothing and that the hub never charges for. It is not
charity either. A public swarm has a page on the hub built from its
README, and that page carries an advertisement; the bytes are never
metered and nothing is ever injected into a file, a segment or a
playlist. A requester who wants no ad on their page buys a seat.
The hub's cut is **1 percent** of any payment that crosses it, charged to
whoever is paying and never taken out of a seeder's or a relay's
earnings. So there are three lines: 1 percent of transactions, seats for
managed access, and ads on the free tier.
Sellers need not be people. A requester is a key, and an autonomous agent
can attest what it made, price access, sell tickets to a live channel,
take payment through its own payee and spend what it earns keeping its
own work online. The consent rules do not soften because a machine signed
them, and the reference hub asks an agent's public attestations to name a
responsible operator key so somebody is reachable when a notice
arrives.
## Non-Goals
- Deciding what content is acceptable. The hub enforces that an
attestation exists, is signed and is honoured on notice. Its truth is
the requester's liability.
- Encrypting public swarms. Public is public; private is `ipfile`.
- A reputation system that crosses hubs. Standing is one hub's signed
opinion.
- A token, a chain, or a DAO.
- The seeder client, the hub, or the library themselves. This PRD adds the
specification; implementation is tracked in each repo (§ Requirements
lists them).
## Users
- **A creator or label** who wants their own release kept online for a
year without running a server, and wants it on record that they are the
one who put it there.
- **A person keeping a backup** who wants three copies of ciphertext held
by strangers for 30 days and deleted after, and never wants the strangers
to know what it is.
- **An open data or open source publisher** with a 4 GB installer or a
dataset who wants ten seeders paid to stay, and downloaders to pay
nothing.
- **A seedbox operator or torlink user** with disk and bandwidth to spare
who wants to be paid for holding swarms, and to see, before accepting,
whether they would be holding a stranger's ciphertext or seeding a
CC-BY dataset in the clear.
- **bittorrented.com** as the hub: attestations, offers, the market, and
the seedbox rental rail it already runs, with IPTV and other passes sold
beside it.
- **A c0mpute node** that already bids on `ipfile.pin` and now sees the
same offers from any hub.
## Requirements
- R1 [P0] Define `pay2seed`, the client half: the attestation record and
basis table, the mandatory README and its hash, the claim window and
requester standing, the offer record and its purchase over x402, the
offer lifecycle, public swarms, public `ipdb` feeds and private swarms,
the hub record extension, the requester API, notices and takedown.
- R2 [P0] Define `paid2seed`, the server half: the lease record and slot
rules, storage challenges and wire probes, GiB-month accrual, receipts
and payout through `ippay`, the seeder API and client behaviour, seeder
and requester standing, the `ipfile.pin` mapping, events, conformance.
- R3 [P0] Define `pay2stream`, the client half for live: consent for
channels with the two rules that separate a licensed rebroadcast from a
stolen feed, relay offers per relay-hour with a gateway bonus, tickets
and listings, watching as a peer or on any HLS player.
- R4 [P0] Define `paid2stream`, the server half for live: relay leases,
presence proofs and the served-bytes ratio, gateways serving standard
HLS in clear and sealed modes with an M3U and XMLTV EPG so any IPTV
player works with no new client, gateway-bound vouchers, stopping on a
void.
- R5 [P0] Specify encryption by default and access as the product: teams
with roles and scopes, invitations that expire, member grants issued by
the hub as keeper, re-encryption on removal, per-seat pricing, the 1
percent fee on payments that cross the hub, and the advertisement that
pays for public swarms.
- R6 [P1] Say that a requester is a key rather than a person, so an
autonomous agent can attest, price access, sell tickets and be paid,
under the same consent rules and with a responsible operator key named
on public attestations.
- R7 [P1] Wire the family into the repo: `docs/openswarm.md` family table,
stack diagram and "what already exists"; `spec.md` §11 record type
registry; the `ip seed` and `ip stream` command groups in `cli.md`; and
the four protocols listed on the `/openswarm` page.
Implementations are tracked outside this repo: bittorrented.com
(`profullstack/media-streamer`) as the reference hub; torlink
(`baairon/torlink`, via the `ralyodio` fork) as the seeder client, whose
per-torrent seed time (#186) is the first piece; `@profullstack/pay2seed`
as the shared client; and c0mpute for `ipfile.pin` interop.
## UX Notes
- An attestation is one form on the upload screen: what is it, why may
you share it, where do notices go. The basis list is a radio group;
`personal` is greyed out unless the swarm is private.
- The market listing shows, for every offer, the basis and visibility in
words a seeder can act on: "Open licence (CC-BY-4.0), public, 7.4 GB,
30 days, earns about $1.04". A seeder client MUST show the same.
- A seeder's own view is a table of leases: what, basis, earned so far,
next challenge due, ends on. torlink's `/downloads` grows a `lease`
block per torrent.
- A requester sees, per offer: status, seeders held out of wanted, proven
periods, and a spend bar against the budget.
## Success Metrics
- A public swarm with an honoured attestation and a funded offer on
bittorrented.com is held by its `seeders.min` within `graceHours`, and
still has that many proven seeders at `expiresAt`.
- A torlink daemon takes a lease, passes every challenge for 30 days, and
is paid out without an operator touching it.
- An `ipfile.pin` job from c0mpute and a `pay2seed.offer` from
bittorrented.com are served by the same node with one code path.
- A notice against a public attestation ends every lease on it within
`claimHours` and the requester's standing shows it.
## Risks & Open Questions
- **Sybil seeding.** One machine, many keys, one copy. `seeders.max`,
probes to announced addresses, and refusing a shared payout address on
one offer bound it; they do not remove it. Open: whether a hub should
require a small stake per lease that a failed period forfeits.
- **Answer relaying on challenges.** A seeder holding nothing can fetch
the four pieces inside a ten-minute deadline on a fast link. Probes are
the stronger evidence for public swarms; deadlines should scale with
piece count. Open: whether a challenge should also demand a piece the
swarm currently has no other seeder for.
- **Claim window versus liveness.** Twenty-four hours before a public
offer is listed is a day of nobody being paid. Open: a shorter window
for `open-license` and `public-domain` bases, which a licence file in
the swarm can substantiate.
- **Hub as escrow.** Budgets sit with the hub for up to a year. A hub
going away takes them. Same trust as `ippay` passes, longer duration.
Open: whether budgets should be released to the hub per period rather
than at purchase.
- **Private and `personal` swarms are unreviewable by design.** Consent is
the requester's signature and standing, nothing else. This is stated
in the spec rather than hidden.
- **Naming.** The rest of the family uses `ip` names. `pay2seed` is the
product name and reads as one to a seeder choosing work; the record
types are `pay2seed.*` so the two never need reconciling.

View file

@ -16,3 +16,4 @@ Status lives in each file's front-matter and is the source of truth:
| [0003](./0003-add-logicsrc-opencontext-spec.md) | Add the LogicSRC OpenContext specification | Draft | opencontext, context, agents, permissions, provenance, schemas | | [0003](./0003-add-logicsrc-opencontext-spec.md) | Add the LogicSRC OpenContext specification | Draft | opencontext, context, agents, permissions, provenance, schemas |
| [0004](./0004-add-logicsrc-opencreds-spec.md) | Add the LogicSRC OpenCreds specification | Draft | opencreds, credentials, vault, encryption, portability, schemas | | [0004](./0004-add-logicsrc-opencreds-spec.md) | Add the LogicSRC OpenCreds specification | Draft | opencreds, credentials, vault, encryption, portability, schemas |
| [0005](./0005-add-logicsrc-openswarm-spec.md) | Add the LogicSRC OpenSwarm specification family | Draft | openswarm, ipfile, ipdb, ipaudio, ipvideo, iplive, bittorrent, x402, c0mpute | | [0005](./0005-add-logicsrc-openswarm-spec.md) | Add the LogicSRC OpenSwarm specification family | Draft | openswarm, ipfile, ipdb, ipaudio, ipvideo, iplive, bittorrent, x402, c0mpute |
| [0006](./0006-add-pay2seed-spec.md) | Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family | Draft | openswarm, pay2seed, paid2seed, pay2stream, paid2stream, iplive, hls, ipfile, ippay, ipdb, bittorrent, torlink, bittorrented, c0mpute |