1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
plan: "0018"
title: "An application owns its PDS: the PDS is a DenoPod in the application's own kcp and its records are kcp objects"
status: done
note: >
Shared branch: task/own-pds on GitHub origin. The shared pieces (the AtprotoRecord kind, the repo-pds
DenoPod manifest set deploy/apps/repo-pds, the hono-repo-pds entry and its record reconciler, the
association flow) land there first; the bidder side (M1) and the gateway requester side (M2) build on
them on the same branch. Rebase on it often. Pushes: a58553ac (AtprotoRecord kind, deploy/apps/repo-pds,
RepoPds values, bidder profiles map to localRepo, the core keeps the repo key in the bidder's OpenBao),
2fbeca24 (hono-repo-pds), 9f4e8508 (bidder side), 6dd6e1be (canonical association shape), 37cb52d0
(RepoPds.VaultMountClient), 5370e006 (no serviceAccount); later shas are in the git log of task/own-pds.
source: >
2026-10-17: Bob's bsky.social account (bobvmbuilder) hit the hosted PDS cap of 35000 write points per
24 hours, and every prod contract then failed with 429. The prod bidder (profile prod-qr, MarketIdentity
mode oauthQr) wrote every market record (Bid, bids.free, wif.simple, receipts, events, payloads) into
Bob's own bsky.social repo. docs/ARCHITECTURE.md section 15 "Bidder PDS write budget" cut the cost per
contract from 35.4 to 23.4 points; that only moves the cap. The cap belongs to Bob's account, not to the
bidder.
principle: >
An application that writes market records owns the repo they live in. The repo is a PDS DenoPod in the
application's own kcp, rendered from the application's root object by its manifest set, bound to port 0
and found by service name (pds.default.app.svc.kcp.local). Its key is in the application's own OpenBao.
Every record the application declares (an association, a vouch, any operator-declared record) is an
AtprotoRecord object in that kcp, and the PDS DenoPod reconciles the object into the repo. The
operator's real account is written once, to associate the application's DID with it. The RFP flow is
unchanged: the PDS only changes where the records live.
state_rules:
- Read AGENTS.md first. status is one of todo, doing, done, blocked. The lead flips statuses.
items:
- id: M0-SHARED-PDS-DENOPOD
title: "The shared pieces: the AtprotoRecord kind, the repo-pds DenoPod, its record reconciler, the association flow"
depends_on: []
decision: >
api/v1alpha1 AtprotoRecord (export marketidentities, so the existing marketidentities binding covers
it): spec {repo (the PDS DenoPod name, pds), collection, rkey, record (an open object)}, status {phase
Pending|Applied|Failed, observedGeneration, did, uri, cid, message, conditions (ReferencesResolved)},
finalizer atprotorecord.socialweb.computer/repo. Records live in git, in the application's manifest
set (deploy/apps/<app>/records.yaml), applied with the profile like any object. Two substitutions
only: render-time Go template values of the root object ({{.Kcp.Server}} and the like), and runtime
references: a record value that is exactly {valueFrom: {kind or resource, name, fieldPath, workspace?,
namespace?, apiVersion?}} is the field of that kcp object (kind MarketIdentity, AtprotoRecord, DenoPod or
any socialweb.computer kind; resource names the plural, apiVersion an outside kind; workspace absent is
the record's own workspace, set it is read through /clusters/<workspace>; fieldPath is dotted with
[n] indexes). The reconciler keeps the record Pending with condition ReferencesResolved False, reason
MissingReference, naming every missing or unreadable reference (404, 403), writes it once all
resolve, and rewrites it when a referenced value changes. That is the ordering: objects wait on each
other's status. deploy/apps/repo-pds/pds.values.yaml renders the DenoPod pds from manifests.RepoPds
(field Pds of a set's values; any set can render it), deploy/apps/repo-pds/rbac.yaml its account
pds. The entry hono-repo-pds reads the repo key from the application's OpenBao (the core creates it
once), registers the did:plc, serves the repo on port 0 and on the did-key-ingress-proxy subdomain of
its key, accepts writes only from a service-auth token signed by the repo key, and reconciles the
AtprotoRecord objects whose spec.repo is pds. associateConfirm decides with the MarketIdentity spec
(associateWith, approved and rejected lists), records an approval in spec.approvedAssociations, and
answers once a declared record {challenge: caller, keyId: own DID, service} is in the repo; the
bidder's record is deploy/apps/market-bidder/records.yaml (challenge from
spec.approvedAssociations[0], keyId from status.did of MarketIdentity bidder).
files: ["api/v1alpha1/types_atprotorecord.go", "deploy/apps/repo-pds", "ts/atproto-market/hono-repo-pds", "ts/atproto-market/lib/hono-factory-repo-pds", "ts/atproto-market/lib/atproto-record-kcp"]
done_when: "a market network bidder runs with the pds DenoPod; deleting an AtprotoRecord object deletes its record from the repo; the pds DenoPod reports its listeners in its status."
status: done
- id: M1-BIDDER-OWN-PDS
title: "Bob's bidder writes its market records to its own PDS DenoPod"
depends_on: [M0-SHARED-PDS-DENOPOD]
decision: >
Every MarketBidder profile (prod-qr, market-network, dev-local) maps to MarketIdentity mode localRepo,
and the market-bidder set renders pds@<profile>. The bidder DenoPod uses the pds DenoPod by service
name for every repo read and write; the in-process PDS of the bidder is gone. The MarketIdentity
associateWith is the bidder's handle, so the scan by that account (bobvmbuilder) is approved without
a second step. In the market network the core plays the operator's browser once: it signs in as the
bidder's account on the network's bob PDS and runs the same associateConfirm call.
done_when: "on a market network, run request ends exec ok against a bidder whose records are all in its pds DenoPod, and the operator account's PDS takes zero writes for the contract."
status: done
- id: M2-GATEWAY-REQUESTER-OWN-PDS
title: "The gateway's requester (Alice) writes its market records to its own PDS DenoPod"
depends_on: [M0-SHARED-PDS-DENOPOD]
decision: >
Before: the gateway's requester DenoPod ran in accounts mode (--account-custody-mount atproto): every
ComputeContract was carried with the OAuth session of the account named by spec.ownerDid, held by the
atproto auth mount of the root vault, so every rfp, compute.vm, accept and rbac record of every node
went into alice's bsky.social repo, and a gateway with no web app session for alice failed every claim.
Now: the gateway set renders pds@<profile> (deploy/apps/repo-pds) in root:app for the MarketIdentity
requester (mode localRepo, associateWith = Gateway spec.operator); the core writes the repo key once at
requester/identity/requester/keys of the root vault, the pds client reads it, and the pds account may
get the root-vault OpenBaoServer. The requester DenoPod takes --repo-pds-url
http://pds.default.app.svc.kcp.local: it signs with the repo key and every record goes to that repo; it
answers bids and events on a callback DID of its own key (identity/requester/callback). spec.ownerDid
stays the account of a contract for the per-account limits; the orphan scan reads the one repo. The
web app's sign-in of people (k8s.socialweb.computer, the atproto auth mount) is unchanged. In a market
network the gateway child's operator is the tenant cluster's owner account on alice's PDS, and the core
associates once with it, as the bidder's core does. The requester's policy reads alice's vouches
through the association record (delegated trust), so the repo needs no vouch records of its own.
done_when: "a prod contract posts no record to alice's bsky.social repo."
status: done
- id: ACC18-PDS-IS-A-DENOPOD
title: "The PDS is a DenoPod in the application's own kcp"
depends_on: [M1-BIDDER-OWN-PDS]
done_when: "units list / the MarketBidder status shows the DenoPod pds Running with its listeners; the bidder finds it as pds.default.app.svc.kcp.local."
status: done
- id: ACC18-ZERO-OPERATOR-WRITES
title: "Zero writes to the operator's bsky.social repo per contract"
depends_on: [M1-BIDDER-OWN-PDS]
done_when: "the operator account's repo has the same record count before and after a contract (market network bob PDS; prod bobvmbuilder on bsky.social)."
status: done
- id: ACC18-KEY-SURVIVES
title: "The repo key and the DID survive a restart and an upgrade"
depends_on: [M1-BIDDER-OWN-PDS]
done_when: "the bidder's DID is the same after a restart of the bidder core and after `socialweb-computer upgrade`; the key is read from the bidder's OpenBao, never created twice."
status: done
- id: ACC18-ASSOCIATION-ONCE
title: "The association is done once and the requester checks it"
depends_on: [M1-BIDDER-OWN-PDS]
done_when: "one association write to the operator's repo per bidder DID; a restart does not ask again; the requester's policy resolves the bidder's operator from the association record (requester-tangled-vouch)."
status: done
- id: ACC18-GATEWAY-SAME-PATTERN
title: "The gateway requester uses the same PDS DenoPod pattern"
depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
done_when: "the gateway's kcp shows its own pds DenoPod and AtprotoRecord objects; alice's bsky.social repo takes no write per contract."
status: done
- id: ACC18-GATEWAY-PDS-IS-A-DENOPOD
title: "The gateway's PDS is a DenoPod in the gateway's own kcp"
depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
done_when: "the Gateway status shows the DenoPod pds Running with its listener next to socialweb-computer-k8s and requester; the requester finds it as pds.default.app.svc.kcp.local."
status: done
- id: ACC18-GATEWAY-ZERO-OPERATOR-WRITES
title: "Zero writes to alice's repo per contract"
depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
done_when: "the operator account's repo has the same rev and record count before and after a node contract (market network alice PDS; prod alice on bsky.social)."
status: done
- id: ACC18-GATEWAY-KEY-SURVIVES
title: "The gateway requester's repo key and DID survive a restart and an upgrade"
depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
done_when: "the Gateway status did is the same after a restart of the gateway core and after `socialweb-computer upgrade`; the key is read from the root vault, never created twice."
status: done
- id: ACC18-GATEWAY-ASSOCIATION-ONCE
title: "Alice associates the gateway requester DID once"
depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
done_when: "one association write to alice's repo per requester DID; a restart does not ask again; a bidder under bidder-tangled-vouch resolves alice as the requester's operator from the association record."
status: done
- id: M3-OPERATED-BIDDER-DISCOVERY
title: "A requester finds the bidders its vouched operators operate"
depends_on: [M1-BIDDER-OWN-PDS, M2-GATEWAY-REQUESTER-OWN-PDS]
decision: >
A bidder DID made by its own PDS is in no relay index and in no vouch record, so a requester that knew
Bob's account found no bidder. Discovery adds, for every DID the requester vouches (its own and,
through its association, its operator's), the DIDs that DID's repo associates (a badgeBlueKeys record
bidder_associate {challenge: the vouched DID, keyId: the bidder}, the one record the QR page or the
core writes into the operator's repo) whose own repo names that operator back with the same canonical
record (discoverOperatedDids in operator-discovery-badge-blue-keys). bidderDids stays an extra source.
done_when: "a gateway with no bidderDids rents a node from a bidder that only its operator's vouched account operates."
status: done
- id: M3-RECORDS-BEFORE-PDS-STOPS
title: "Deleting an application removes its AtprotoRecord objects before its pds DenoPod stops"
depends_on: [M0-SHARED-PDS-DENOPOD]
decision: >
The MarketBidder and Gateway delete paths delete every AtprotoRecord object of the application first and
stop their DenoPods only when none is left; the running pds DenoPod removes each record from the repo
and releases its finalizer. When the pds DenoPod is not running, the core releases the finalizers itself.
done_when: "dev down of a market network with a bidder and a gateway leaves no AtprotoRecord object and no stuck workspace."
status: done
- id: M4-DYNAMIC-ASSOCIATION
title: "An application learns its operator from the atproto event stream, not from its config"
depends_on: [M1-BIDDER-OWN-PDS, M2-GATEWAY-REQUESTER-OWN-PDS]
decision: >
docs/ARCHITECTURE.md section 14 "The operator comes from the event stream". The kind
AtprotoRecordWatch (spec {repo, collection, match [{fieldPath, value | valueFrom}], source {relayUrls,
jetstreamUrls}}, status {records, cursors, conditions Verified}) is reconciled by the pds DenoPod: events
are triggers, getRecord from the author's PDS verifies, a delete or a failed refetch removes, a start
re-verifies and resumes from the cursors. deploy/apps/repo-pds/association-watch.<profile>.yaml declares
association (keyId = MarketIdentity status.did, service = the application's service; source = network
relayUrls and firehoseUrls, prod adds Jetstream). The MarketIdentity reconciler shows a single-use nonce
from the application's OpenBao only in status.pendingAction.url; the first verified claim record with
that nonce (or from the optional spec.operator allow-list) is written to spec.approvedAssociations,
the nonce is deleted, and the identity is locked. The association AtprotoRecord has
onMissingReference Delete.
done_when: "on a market network whose Gateway and MarketBidder specs name no operator, the operators associate by writing one badgeBlueKeys record with the nonce from the MarketIdentity status, the watch lists it as verified, the association AtprotoRecord is Applied, and a tenant cluster gets a node through RFP, bid, accept and cloud-init."
status: done
- id: ACC18-NO-OPERATOR-IN-CONFIG
title: "No operator in the Gateway or MarketBidder spec, in the market network and in prod"
depends_on: [M4-DYNAMIC-ASSOCIATION]
done_when: "the prod Gateway and the hat-8 MarketBidder run with no spec.operator; each is associated by one claim record that the operator writes with the nonce of its pendingAction url, read from the public Jetstream."
status: done
- id: ACC18-REVOCATION
title: "Deleting the operator's claim record revokes the association; a restart keeps it by re-verifying"
depends_on: [M4-DYNAMIC-ASSOCIATION]
done_when: "deleting the operator's badgeBlueKeys record removes it from the watch status, removes the DID from spec.approvedAssociations, sets condition Associated False reason Revoked, removes the association record from the application's repo and shows a new nonce; a restart of the core with the record in place keeps the association (the watch re-verifies it, Verified True) with no operator in any config."
status: done