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
This commit is contained in:
Anthony Ettinger 2026-09-06 00:53:20 +00:00
parent 3794857a6f
commit 12cdd34e6b
6 changed files with 201 additions and 17 deletions

View file

@ -143,6 +143,12 @@ 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>]
```
```

View file

@ -86,7 +86,7 @@ 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` §7) every lease on it is `voided`, the hub
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.
@ -201,7 +201,7 @@ cannot take a lease, because there would be nowhere to pay.
### 6.1 API, seeder side
Paths relative to the `pay2seed.base` in the hub record (`pay2seed`
§5.1). The requester side is `pay2seed` §5.2.
§6.1). The requester side is `pay2seed` §6.2.
| Method and path | Auth | Purpose |
| --- | --- | --- |

View file

@ -18,6 +18,12 @@ do not control, to a hub. It defines:
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
@ -35,7 +41,7 @@ carried on c0mpute's job auction; `paid2seed` §8 gives the mapping.
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
§7 is how a false one is undone. Not a DRM. Not a token. Not a replacement
§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
@ -85,8 +91,9 @@ offer, and every offer for that swarm references it.
| `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 (§7). REQUIRED for `public`. |
| `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
@ -105,7 +112,7 @@ public infohash no such proof of authorship exists; §3.3 stands in for it.
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 (§5.1).
particular bases; the hub record says which (§6.1).
### 3.3 Public infohashes and the claim window
@ -216,9 +223,156 @@ 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. The requester's hub
## 5. Encryption by default, and who may decrypt
### 5.1 Hub record
### 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 passes and its seat fees on teams, and nothing on the
seeder's floor.
### 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 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:
@ -237,7 +391,7 @@ An `ippay.hub` record that offers this family adds:
needs to take a lease (`paid2seed`). A hub MAY run a market for private
backups only, or for open data only, by narrowing `bases`.
### 5.2 API, requester side
### 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.
@ -254,7 +408,7 @@ The seeder side of the same API is `paid2seed` §6.1.
| `POST /notices` | signed | A notice against an attestation (§7). |
| `POST /webhooks` | signed | Register a CloudEvents endpoint for a requester key. |
## 6. Requester client behaviour
## 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:
@ -264,6 +418,9 @@ app such as bittorrented.com's upload form) does the following:
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
@ -271,7 +428,7 @@ app such as bittorrented.com's upload form) does the following:
proven, and spend against the budget.
6. Receives notices at the endpoint it named, and shows them.
## 7. Notices and takedown
## 8. Notices and takedown
Anyone may `POST /notices` against an attestation:
@ -300,7 +457,7 @@ 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.
## 8. Security notes
## 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
@ -313,7 +470,7 @@ standing, which is what accountability looks like for ciphertext.
swarms SHOULD use a fresh unrelated file pair (core §4.2) so backups are
not linkable to a publisher's catalogue.
## 9. Implementations
## 10. Implementations
| Piece | Where | Status |
| --- | --- | --- |
@ -321,7 +478,7 @@ standing, which is what accountability looks like for ciphertext.
| Requester client and shared records | `@profullstack/pay2seed` | planned |
| Requester CLI | `ip seed …` ([cli.md](./cli.md)) | proposed |
## 10. Conformance
## 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
@ -331,7 +488,9 @@ within `claimHours` (§7). It MUST also be conformant to `paid2seed`.
A **requester client** is conformant when it does all of §6.
## 11. Version history
## 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.
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.

View file

@ -200,7 +200,7 @@ Relative to a `pay2stream.base` the hub adds to its `ippay.hub` record as
| `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` §7; voiding ends every lease and delists every gateway for the channel. |
| `POST /notices` | signed | As `pay2seed` §8; voiding ends every lease and delists every gateway for the channel. |
## 8. Client behaviour
@ -239,7 +239,7 @@ MUST NOT send a ticket to a gateway the hub does not list for that channel.
## 11. Conformance
A **hub** on this side: everything `pay2seed` §10 requires of it, over
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.

View file

@ -505,6 +505,8 @@ Member conformance is defined per document; the checklist is collected in
| `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 |

View file

@ -80,11 +80,28 @@ the same day.
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.
## Non-Goals
- Deciding what content is acceptable. The hub enforces that an