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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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