mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-09-10 19:26:00 +00:00
Add pay2seed, paid2seed, pay2stream and paid2stream to the OpenSwarm family (#143)
* 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:
parent
4fed2681ec
commit
be99e683bd
12 changed files with 1751 additions and 3 deletions
|
|
@ -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]> = [
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
325
docs/openswarm/paid2seed.md
Normal 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.
|
||||||
276
docs/openswarm/paid2stream.md
Normal file
276
docs/openswarm/paid2stream.md
Normal 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®ion` | 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
554
docs/openswarm/pay2seed.md
Normal 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.
|
||||||
260
docs/openswarm/pay2stream.md
Normal file
260
docs/openswarm/pay2stream.md
Normal 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®ion&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.
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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[] };
|
||||||
|
|
|
||||||
246
prd/0006-add-pay2seed-spec.md
Normal file
246
prd/0006-add-pay2seed-spec.md
Normal 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.
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue