docs(dht): Phase 4 plan (paid streaming/relay/IndeeHub + cross-mint) + RESUME update

phase4-streaming-ecash-plan.md: design for ecash-paid swarm transport, paying
across different mints (§2a, Lightning-bridged swaps), networking-through-nodes
relay, and an IndeeHub "Archipelago" content source. Records the resolved
iroh-blobs paid-serving spike. dht-RESUME.md: task #12 + step F marked done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-17 04:48:18 -04:00
parent 75b78325e4
commit 1f3b03bc6d
2 changed files with 408 additions and 22 deletions

View File

@ -58,31 +58,46 @@ Design doc: `docs/dht-distribution-design.md` (the full plan).
All tests green at each step. Total new modules: `trust/`, `content_hash.rs`, `swarm/`.
## NEXT STEP — task #12 (Phase 3 glue + wiring)
## task #12 — Phase 3 glue + wiring — DONE (2026-06-17, NOT yet committed)
Implement, in the worktree:
Implemented in the worktree, **uncommitted** (release in flight — do not commit/merge
until the user says so). Verified: default `cargo build` clean, `cargo build
--features iroh-swarm` clean, `cargo test --bin archipelago -- swarm::` → **8/8 pass**.
1. **`NostrSeedDiscovery`** (feature-gated, in `swarm/iroh_provider.rs` or a new
`swarm/discovery.rs`): implement the `ProviderDiscovery` trait by querying relays
with `seed_advert::advertisement_filter(hash)`, then
`seed_advert::endpoint_ids_from_events(...)` → parse each string into
`iroh::EndpointId` (`EndpointId::from_str` / parse). Skip ids that don't parse.
- **NOTE:** `ProviderDiscovery::providers_for` is currently **sync**. The relay
query is async → either change the trait to `#[async_trait] async fn`, or back
it with an in-memory cache refreshed by a background subscription. Async trait
is cleaner (the caller `try_fetch` is already async).
2. **Publish path:** when a node finishes downloading / already holds a public
release/app-image blob, publish `seed_advert::advertisement_builder(blake3, my_endpoint_id)`
signed with the node's Nostr key (`nostr_discovery.rs` has the
`load_or_create_nostr_keys` + `Client` + `send_event_builder` patterns to reuse).
Scope: **releases/catalog blobs only** — never private user blobs.
3. **Wire `swarm::providers()`** to construct an `IrohProvider` (with the
`NostrSeedDiscovery`) from runtime config — needs an enable flag + relay list +
data_dir. Likely make `providers()` async / build it once at startup and pass a
handle into the update path. Until this is wired, `providers()` returns empty and
everything uses origin (safe).
1. **`NostrSeedDiscovery`** (`swarm/iroh_provider.rs`) — `ProviderDiscovery` made
**async** (`#[async_trait]`); impl queries relays via the new
`seed_advert::fetch_seed_endpoint_ids` and parses each string with
`EndpointId::from_str` (`EndpointId = PublicKey`, has `FromStr`/`Display`),
skipping unparseable. `try_fetch` now `.await`s discovery.
2. **Publish path** — dep-free `seed_advert::fetch_seed_endpoint_ids` +
`publish_seed_advert` (reuse now-`pub(crate)` `build_nostr_client` /
`load_or_create_nostr_keys`); `IrohProvider::seed_and_advertise` imports the blob
into the FsStore (`blobs().add_path``TagInfo`) with a defensive hash-match,
then publishes. Scope: releases/catalog only.
3. **Wiring**`swarm::init()` builds the `IrohProvider` once at startup into a
`OnceLock<SwarmRuntime>` (keeps endpoint/router alive → keeps seeding);
`providers()` returns the registered provider; `announce_held_blob()` is called
from `update.rs` after each release component passes both hash gates. New config
`swarm_enabled` (`ARCHIPELAGO_SWARM_ENABLED`, default false); `server.rs` calls
`swarm::init`. All iroh code stays behind `iroh-swarm`; default build inert.
Then verify: `cargo build --features iroh-swarm` + `cargo test --bin archipelago -- swarm::`.
**iroh-blobs paid-serving spike (open Q#1) — RESOLVED:** `BlobsProtocol::new(&store,
Some(EventSender))` + `EventMask` intercept gives native per-request allow/deny
(`RequestMode::Intercept``Result<(), AbortReason>`), connection-level reject
(`ConnectMode::Intercept`), and per-request throttle/meter (`ThrottleMode::Intercept`).
## NEW: Phase 4+ plan (paid streaming / relay / IndeeHub) — `docs/phase4-streaming-ecash-plan.md`
Design for: (1) ecash-paid swarm transport, (2) networking through nodes / relay,
(3) IndeeHub "Archipelago" content source (signed Nostr film catalog, kind 30082).
Headline: ~80% already exists (Cashu wallet, `streaming/` payment gate + metering,
4-tier transport, the swarm above). Also shipped this session: a **Networking Profits
→ Settings** UI in `neode-ui` (new `views/web5/Web5NetworkingProfitsSettings.vue` +
route + button in `Web5QuickActions.vue` + `common.settings` i18n) that drives the
existing `streaming.list-services`/`configure-service` RPCs; free-everything is the
default (all services ship `enabled:false`). Frontend typechecks clean (pre-existing
`Web5ConnectedNodes.vue` `.did` errors are NOT ours). `neode-ui` deps were
`npm install`ed to complete a partial install.
## After Phase 3

View File

@ -0,0 +1,371 @@
# Phase 4+ — Paid swarm streaming & the IndeeHub "Archipelago" source
**Status:** PLAN / design (2026-06-17) · **Branch:** `agent-trust-wip` · not implemented
**Builds on:** `docs/dht-distribution-design.md` (Phases 03, swarm + Blossom), the
Phase 3 swarm work just landed (`swarm/`, `content_hash.rs`, `trust/`).
This plans three things the user asked for, in one coherent architecture:
1. **Pay sats (ecash) for transport** of streaming film data between nodes.
2. **Networking *through* nodes** — relaying/routing a stream via intermediate peers.
3. An **"Archipelago" content source in IndeeHub** that shows every film uploaded
to *backstage*, on every node running the IndeeHub app.
> ## Headline finding
> **Most of the primitives already exist.** This is ~80% integration glue, not
> greenfield. A full Cashu/ecash wallet, a metered streaming payment gate, a
> 4-tier transport layer, the iroh-blobs swarm (just added), signed Nostr
> advertisements, and the Ed25519 trust module are all already in the tree. The
> genuinely new code is: (a) a paid-serving hook on the iroh side, (b) a relay
> protocol, and (c) the IndeeHub film catalog + Archipelago-local API.
---
## 0. Inventory — what we can build on (all already in `core/archipelago/src`)
| Capability | Where | State |
| --- | --- | --- |
| **Cashu ecash wallet** (mint/melt/send/receive, BDHKE) | `wallet/ecash.rs`, `wallet/cashu.rs`, `wallet/mint_client.rs`, `wallet/bdhke.rs` | ✅ implemented |
| **Local mint** (Fedimint) backing the wallet | `apps/fedimint` (`http://127.0.0.1:8175`) | ✅ deployed |
| **Lightning** (invoices, pay, channels) for mint/melt | `api/rpc/lnd/*`, `container/lnd.rs`, `apps/lnd` | ✅ implemented |
| **Streaming payment gate** (accepts `cashuA` tokens, opens metered session) | `streaming/gate.rs` | ✅ implemented |
| **Metering & pricing** (sats per byte / ms / request; e.g. content-download = 1 sat/MB) | `streaming/meter.rs`, `streaming/pricing.rs`, `streaming/session.rs` | ✅ implemented |
| **Revenue/profit accounting** (incl. `StreamingRevenue` tx type) | `wallet/profits.rs` | ✅ implemented |
| **Paid-service discovery** on Nostr (kind 10021, TollGate TIP-01 shape) | `streaming/advertisement.rs` | ✅ implemented |
| **Content server** that verifies+receives payment before serving | `content_server.rs` (`verify_and_receive_payment()`) | ✅ implemented |
| **iroh-blobs swarm** (fetch content-addressed blobs from peers, verify, seed) | `swarm/` (`iroh-swarm` feature) | ✅ just added |
| **Signed seed adverts** (NIP-33 kind 30081, blake3→endpoint) | `swarm/seed_advert.rs` | ✅ just added |
| **BLAKE3 content addressing** | `content_hash.rs` | ✅ implemented |
| **Ed25519 trust / `did:key` / detached signatures** | `trust/` | ✅ implemented (anchor ceremony pending) |
| **4-tier transport** (Mesh > LAN > FIPS > Tor) + `last_transport` | `transport/*`, `fips/dial.rs` | ✅ implemented |
| **Node discovery + federation trust** (Trusted/Observer) | `nostr_handshake.rs`, `federation/*` | ✅ implemented |
What is **NOT** present and must be built:
- **A paid-serving hook on the iroh-blobs provider.** Today the swarm seeds to
anyone (`BlobsProtocol::new(&store, None)` — no authorization). To charge for
swarm bandwidth we need a per-request gate that consults `streaming/gate.rs`.
- **A relay protocol.** No "peer A asks peer B to forward traffic to peer C".
Transport is point-to-point; there is no multi-hop routing, TTL, or relay
accounting.
- **IndeeHub Archipelago catalog.** The shipped IndeeHub points at the external
`staging-api.indeehub.studio` + AWS S3/CloudFront. Nothing makes a film
uploaded on node A visible on node B. No *backstage* code exists yet.
---
## 1. Pay sats (ecash) for transport of streaming films
### Goal
When node B streams a film blob (an HLS `.ts` segment) *from* node A's swarm,
A earns sats for the bytes it serves — using the ecash gate that already meters
`content-download`.
### What exists vs. what's new
- ✅ The economic machinery is done: `streaming/pricing.rs` already ships a
`content-download` service priced per MB; `streaming/gate.rs` turns a `cashuA`
token into a metered session; `meter.rs` deducts bytes; `profits.rs` records
`StreamingRevenue`.
- ❌ The swarm serving path doesn't consult any of it. `IrohProvider::new`
spins up `BlobsProtocol` that answers every blob request unconditionally.
### Design — "paid swarm" as a gated blob protocol
The clean seam is the iroh-blobs **accept** side. Two viable shapes:
**(A) In-band gate via a custom ALPN (preferred).** Keep iroh-blobs for the raw
byte transfer but front it with a tiny request/grant exchange on a second ALPN
(`archy/paid-blobs/1`):
1. B wants `blake3:H`. It dials A's endpoint and sends `{want: H, token?: cashuA}`.
2. A calls `streaming::gate::check_gate("content-download", peer=B, bytes≈len(H), token)`.
- `PaymentRequired` → A replies with price + its accepted mints
(`streaming.list-mints`) and the sat amount; B mints/sends a `cashuA` and retries.
- `PaidAndAllowed` / `Allowed` (within existing session allotment) → A authorizes
the blob hash for this connection and hands off to iroh-blobs to stream it.
3. A meters served bytes via `meter::record_and_check` and records revenue.
**(B) Pre-paid session, then open serving.** B opens a metered session up front
(buys N MB of `content-download` allotment with one token), and A's blob protocol
checks "does this peer have remaining allotment?" before each blob. Simpler, fewer
round-trips, slightly looser accounting. Good first cut.
Recommend **(B) for v1** (least new protocol surface — reuses sessions verbatim),
graduating to **(A)** when we want per-blob price discovery.
### Free vs. paid policy (important)
- **OTA + app-catalog blobs stay FREE.** Charging for security updates is hostile
and breaks the "origin always wins" guarantee. Gating applies **only** to the
IndeeHub film scope (a per-blob or per-advert "monetized" flag).
- Trusted federation peers (`TrustLevel::Trusted`) can be configured to serve each
other free; payment is for untrusted/public swarm peers.
### Integration points
- **DONE (2026-06-17):** `swarm/paid.rs` — the accept-side gate. Builds the
iroh-blobs `EventSender` (intercept connect + GET, hard-disable `push`) and
authorizes each request through `streaming::gate::check_gate("content-download",
peer_endpoint, blob_size, None)`. Free when the service is disabled (default);
denies unpaid peers when enabled; fails OPEN on internal error. Wired into
`IrohProvider::new`; unit-tested. The Settings toggle the user just got drives it.
- Reuse: `streaming/gate.rs`, `meter.rs`, `session.rs`, `wallet/ecash.rs`,
`streaming/advertisement.rs` (advertise the node as a paid blob seeder).
- TODO (fetch side): `swarm::fetch_content_addressed` gains an optional
"willing-to-pay budget + token source" so a downloading node can auto-pay from
its ecash wallet up to a cap (opening a session via `streaming.pay`), then fall
back to origin if too expensive. This is where **cross-mint settlement (§2a)**
plugs in — the payer may need to swap into the seeder's accepted mint first.
---
## 2. Networking *through* nodes (relayed / routed streaming)
This is the largest genuinely-new piece. Two distinct meanings — both useful:
### 2a. iroh-native relays (cheap, already mostly free)
iroh 1.0 already hole-punches and falls back to **relay servers** for connectivity
when a direct QUIC path can't be established. So "streaming through a node that
can reach the seed when I can't" partly exists at the iroh layer. Action: run/seed
our **own** iroh relay(s) on the OVH/hub infrastructure and pin them in config, so
the swarm doesn't depend on n0's public relays. Low effort, high resilience.
### 2b. Application-level paid relay (the real gap)
"Node B pays node A to fetch a film from origin/swarm on B's behalf and forward it"
— useful when B is behind a censored/expensive link and A has good connectivity
(the beta-cellular-node scenario from memory). This needs a real protocol:
- **`relay.offer` advert** (Nostr kind 10021 with a `relay` tag + price/MB) — reuse
`streaming/advertisement.rs`; add a `relay-bandwidth` service to `pricing.rs`.
- **`relay.fetch` request** over the existing transport (`PeerRequest` in
`fips/dial.rs`): `{content: blake3:H | url, pay: cashuA}`. The relay runs the
normal `swarm::fetch_content_addressed` (swarm-assist, origin fallback), meters
the bytes through `streaming/gate`, and streams them back to the requester.
- **Accounting:** add a `RelayBytes` metric to `streaming/meter.rs` distinct from
origin `content-download`, so "relay provided" is tracked separately in
`profits.rs` (the doc already separates `routing_fees` from `streaming_revenue`).
- **Safety rails:** single-hop only for v1 (no A→B→C→D); TTL + loop guard before
any multi-hop; cap per-session bytes; only relay the **public film scope**, never
private user blobs or arbitrary URLs (prevent open-proxy abuse).
### Phasing for §2
1. Pin our own iroh relays (config only). — *days*
2. Single-hop paid `relay.fetch` for film blobs, gated by ecash. — *the core build*
3. Multi-hop routing + path discovery. — *deferred; only if single-hop proves out*
---
## 2a. Cross-mint ecash settlement — paying across *different* mints
**Problem (user, 2026-06-17):** payment must work when the payer and the seeder
use **different** mints — not only two nodes on the same Fedimint. A node holding
tokens on mint **A** must be able to pay a seeder that only accepts mint **B**,
automatically.
### Why this is mostly a generalization, not new crypto
The wallet already tracks proofs **per-mint**: `WalletData::balance_for_mint(url)`,
`select_proofs(url, amount)`, `add_proofs(url, proofs)` are all mint-scoped, and
`MintClient::new(url)` targets any mint. What's hardcoded is convenience: `mint_quote`
/ `melt_quote` / `mint_tokens` / `melt_tokens` always use the single home
`wallet.mint_url`. So the data model is multi-mint already; we add the *swap* and
parameterize the helpers by target mint.
### The swap primitive (Cashu/Fedimint settle over Lightning)
To move value **A → B**, both mints expose BOLT11 mint+melt quotes (already in
`mint_client.rs`), and Lightning bridges them:
1. `MintClient::new(B).mint_quote(amount)` → a BOLT11 invoice `inv_B` (pay it to get B tokens).
2. `MintClient::new(A).melt_quote(inv_B)` → cost in A tokens (`amount + fee_reserve`).
3. Select A proofs and `melt` them on A to pay `inv_B` over Lightning.
4. When `inv_B` settles, `MintClient::new(B).mint_tokens(quote_B)` → claim B tokens;
`wallet.add_proofs(B, …)`.
Net: value lands on B minus (A melt fee + LN routing + B mint fee). The node's LND
isn't strictly required — the mints' own LN gateways settle — but a healthy local
node/route improves success. Implementation = three thin `*_at(mint_url, …)`
variants of the existing helpers + one composer:
`swap_between_mints(data_dir, from, to, amount, max_fee_sats) -> Result<u64>`.
### Where the swap happens — two models
- **Payer-side swap (recommended default).** Before paying seeder S (whose
`accepted_mints` are advertised via `streaming.advertise` / the gate's
`PaymentRequired.pricing.accepted_mints`), the payer picks the cheapest path:
pay directly if it already holds a token on one of S's mints; otherwise
`swap_between_mints(A → S_mint)` then send a token denominated in S's mint. **S
never has to trust mint A** — it only ever receives its own mint's tokens. Clean.
- **Payee-side auto-consolidation (optional, more liberal).** S widens
`accepted_mints` to any mint it's willing to melt-swap from, accepts an A token,
then swaps A → home-mint in the background. Broader acceptance, but S briefly
carries mint-A counterparty risk.
A node can do both: advertise a broad accept list *and* have payers prefer
direct/cheap mints.
### Guardrails (these are the real design decisions)
- **Mint trust list.** Mints can be insolvent or rug. Only swap *into* / accept
mints on a configured allow-list (default: home mint + a small curated set, with
the local Fedimint always trusted). Surface this in the Settings UI alongside the
per-service pricing.
- **Fee/slippage cap.** Every swap costs sats. `max_fee_sats` (or a max %) refuses a
swap that would cost more than the content is worth; the payer then declines and
uses origin. Show the all-in cost (price + swap fee) before auto-paying.
- **Origin always wins.** If the LN swap fails (no route, mint offline, over
budget), fall back to the HTTP origin with no payment. A mint problem must never
block content.
- **Idempotency / crash-safety.** Persist in-flight swaps (`melt` quote id + `mint`
quote id) so a crash between "paid `inv_B`" and "claimed B tokens" resumes the
claim instead of double-paying. Reuse the wallet's tx log.
- **Liquidity.** Swaps need the mints to have inbound/outbound LN liquidity; cache
recent swap success per mint-pair and prefer routes that have worked.
### Phasing for §2a
1. `*_at(mint_url, …)` helpers + `swap_between_mints` + mint trust list + fee cap. — *the core*
2. Payer-side auto-swap in the payment builder (pick cheapest accepted mint). — *wires §1/§2 to it*
3. Idempotent resume + per-pair liquidity cache. — *hardening*
4. (Optional) payee-side auto-consolidation.
This keeps the headline promise intact: **pay anyone, on any trusted mint,
automatically — or fall back to free origin.**
---
## 3. IndeeHub "Archipelago" content source
### Goal
A new source tab inside the IndeeHub app, **"Archipelago"**, listing every film
uploaded to *backstage*, streamable on any node — independent of the external
`indeehub.studio` API.
### Today (from the research)
- IndeeHub frontend (Next.js) is built against `NEXT_PUBLIC_API_URL =
staging-api.indeehub.studio` and pulls media from AWS S3/CloudFront. It is
**not Archipelago-aware**. nginx proxies it at `/app/indeedhub/` and injects a
NIP-07 Nostr provider.
- A MinIO stack exists (`indeedhub-public` / `indeedhub-private` buckets); FFmpeg
produces HLS there. **No backstage upload UI/code exists yet.**
- The design doc's Phase 4 already describes the target: backstage → FFmpeg → HLS
→ each `.ts` is a BLAKE3 blob → signed Nostr "Blossom" catalog event → any node
resolves the content address and streams from the nearest holder; MinIO origin.
### Architecture — four pieces
**(i) Backstage upload + transcode (origin side).**
Minimal creator flow on a publisher node: upload master → FFmpeg → HLS
(`.m3u8` + `.ts`) into MinIO (reuse the existing `indeedhub-ffmpeg`/MinIO stack).
For each segment compute `blake3_hex` (`content_hash::blake3_hex`) and import it
into the iroh seed store (`IrohProvider::seed_and_advertise`, generalized beyond
releases). The playlist references segments by content hash.
**(ii) Signed film catalog on Nostr (the "Archipelago" source).**
Define a new addressable event — **kind 30082, `archy-film`** (sibling of the
30081 seed advert) — published by the publisher node, **signed via `trust/`**:
```jsonc
{
"title": "...", "creator_did": "did:key:z...", "duration_s": 5400,
"poster": "blake3:...", // poster image blob
"playlist": "blake3:...", // the .m3u8 (itself a blob)
"segments": ["blake3:...", ...], // ordered .ts segment hashes
"enc": { "scheme": "aes-128", "key_ref": "nip98" }, // see (iv)
"monetized": { "service": "content-download", "sats_per_mb": 1 } // optional
}
```
The signature uses `trust::sign_detached`; consumers verify with
`trust::verify_detached`. **Publisher trust:** films show in the Archipelago tab
only from publishers on the node's trusted/federation set (or a pinned
"Archipelago film-root" key, mirroring the release-root anchor concept). This is
the key that stops the shared catalog from being a spam vector.
**(iii) Archipelago-local film API (makes it appear on every node).**
New RPC + HTTP endpoints in `api/`:
- `film.catalog` / `GET /api/film-catalog` — query Nostr relays for kind-30082
events from trusted publishers, verify signatures, dedupe, return merged JSON.
Cache like `app_catalog.rs` does (mtime/TTL, atomic write).
- `GET /api/film/:blake3` — serve a segment: `swarm::fetch_content_addressed`
(swarm-assist → MinIO/OVH origin), BLAKE3-verified, with HTTP range support so
the player can seek. This is where §1 (paid serving) and §2 (relay) plug in.
- The IndeeHub frontend gets an **"Archipelago" source** that points at
`/api/film-catalog` instead of `indeehub.studio`. Cleanest: a small build/runtime
flag or an injected config (same nginx `sub_filter` mechanism already used to
inject the NIP-07 provider) that registers the Archipelago source alongside the
existing studio source — additive, not a replacement.
**(iv) Encryption / access (private films).**
Public films: plaintext segments, freely cacheable, swarm-distributable. Private
films: keep AES-128 HLS; **untrusted seeds cache only ciphertext** (they never see
plaintext), and the decryption key is delivered per-viewer via NIP-98 auth (the
mechanism IndeeHub already uses) or NIP-44 DM. Payment (§1) gates *bytes*; the
*key* gates *plaintext* — two independent locks. This lets us pay strangers to
seed encrypted blobs without leaking content.
### "On every node" — propagation
Propagation is **pull**, not push: every node's `film.catalog` periodically queries
the same Nostr relays (already configured for discovery) for trusted-publisher film
events. A film uploaded on node A is therefore visible on node B as soon as B
refreshes its catalog — exactly how `app_catalog.rs` already distributes app
updates fleet-wide. No central server; the relays carry only signed metadata, the
blobs flow peer-to-peer with MinIO/OVH as origin.
---
## 4. Suggested end-to-end phasing
| Step | Deliverable | Risk | Reuses |
| --- | --- | --- | --- |
| **A** | Generalize `seed_and_advertise` beyond releases → arbitrary public blob scope (films) | low | swarm/ |
| **B** | `film.catalog` RPC + signed kind-30082 events + trusted-publisher gating | lowmed | trust/, app_catalog.rs pattern |
| **C** | `GET /api/film/:blake3` range-streaming via swarm-assist + MinIO origin | med | swarm/, content_server.rs |
| **D** | IndeeHub "Archipelago" source wired to the local API (additive) | med (frontend, external repo) | nginx sub_filter |
| **E** | Backstage: upload → FFmpeg → HLS → blob import + catalog publish | med | MinIO/ffmpeg stack |
| **F** | **DONE** — paid swarm serving (`swarm/paid.rs` gates the blob protocol via `streaming/gate`); free by default | med | streaming/* |
| **F2** | Cross-mint settlement (§2a): `swap_between_mints` + payer-side auto-swap + mint trust list + fee cap | medhigh | wallet/ecash, mint_client, lnd |
| **G** | Pin our own iroh relays (config) | low | iroh |
| **H** | Single-hop paid `relay.fetch` for film blobs | high | transport/, streaming/* |
| **I** | Multi-hop routing | high / deferred | — |
A→E delivers "films on every node" with free volunteer seeding (the design-doc
vision). F→H layer the sats economy on top. I is genuinely future work.
---
## 5. Open questions / decisions needed
1. **iroh-blobs authorization granularity.** ✅ **RESOLVED (2026-06-17 spike).**
iroh-blobs 0.103 exposes exactly the hook we need: `BlobsProtocol::new(&store,
Some(EventSender))`. With an `EventMask` set to intercept, the provider asks our
handler to authorize each request and we return `EventResult = Result<(),
AbortReason>`:
- `RequestMode::Intercept` / `InterceptLog` — per-blob-request allow/deny
(`Err(AbortReason::Permission)` denies, `Err(AbortReason::RateLimited)` defers).
- `ConnectMode::Intercept` — reject at the connection handshake (cheap pre-filter).
- `ThrottleMode::Intercept` — per-request throttle/meter hook for byte accounting.
- `RequestMode::Disabled` — hard-reject a whole request kind (e.g. disable `Push`
so peers can never write into our store).
**§1 shape (A) is the recommended path** (native, no fork): the accept-side
handler calls `streaming::gate::check_gate("content-download", peer_endpoint,
bytes, token)` and maps `PaymentRequired`/`InsufficientPayment` →
`Err(Permission)`, `Allowed`/`PaidAndAllowed``Ok(())`. Peer identity comes
from the `Connection`'s remote endpoint id. (See `iroh_blobs::provider::events`.)
2. **Film-publisher trust anchor.** One global "Archipelago film-root" key (curated
store, like release-root) vs. per-node trusted-publisher sets vs. both. Affects
spam resistance and who can publish to *everyone's* Archipelago tab.
3. **MinIO as origin across the fleet** — single canonical MinIO on the hub vs.
per-node MinIO with cross-seeding. The swarm makes per-node origin viable but
the *first* upload needs a home.
4. **IndeeHub frontend is an external repo** (`~/Projects/indeehub-frontend`,
built into `apps/indeedhub`). Adding an "Archipelago" source needs changes
there; scope whether it's a build-time source registration or a runtime-injected
config (preferred — keeps the node OS in control).
5. **Pricing defaults & free tier.** What's free (OTA, trusted peers, first N MB?)
vs. paid, and the default sats/MB. `pricing.json` already supports this; needs a
policy.
6. **Payment UX / auto-pay caps.** A downloading node auto-paying from its ecash
wallet needs a user-set ceiling and a "prefer free origin if peer wants > X"
rule, so streaming never silently drains the wallet.
---
## 6. Why this is tractable
The hard, slow-to-build substrate — an ecash wallet, a metered payment gate,
content addressing, a verifying swarm, signed discovery, a trust module, a
multi-transport stack — is **already in the tree and (for the swarm) just tested**.
The remaining work is wiring those together along the three axes above, with the
two new protocols (paid blob serving, single-hop relay) being the only substantial
net-new surface. Everything stays behind feature flags / opt-in config and obeys
the project's north star: **swarm-assist, origin always wins** — and now,
**free updates, optional paid films.**