A jam is a shared-deadline creative challenge: one prompt, a roster of participants, a single due date. Submissions point at records that li
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: add "Built on atjam" showcase page

New /docs/built-on (TSX route + markdown mirror): a truthful "in the wild"
adopters table (the atjam.at app itself + EPTSS) plus an end-to-end worked
example mapping a judged, votable game jam — the rpg.actor shape — onto the
at.atjam.* spine. It shows the two illustrative actor.rpg.* lexicons an
adopter would publish under their own namespace, the round wired through the
extension points, and vote tallying via the existing Queries.getBacklinks
substrate. Explicit callout that rpg.actor does not actually run on atjam.
Wired into the docs sidebar, the overview list, and the footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

+633
+249
lexicons/docs/built-on.md
··· 1 + # Built on atjam 2 + 3 + > Who uses the `at.atjam.*` lexicons — and one end-to-end worked example of 4 + > mapping a judged, votable game jam onto them. 5 + 6 + atjam is a generic coordination layer: it knows about jams, rounds, signups, 7 + invitations, and submissions, and nothing about your domain. This page has two 8 + halves — the projects actually reading and writing `at.atjam.*` today, and a 9 + worked example that carries one adopter's jam all the way through the four 10 + [extension points](./extending.md). 11 + 12 + ## In the wild 13 + 14 + | Project | What it is | How it uses atjam | 15 + |---|---|---| 16 + | [atjam.at](https://tangled.org/natespilman.at/atjam.at) (this repo) | The reference web app. | Reads and writes all five records; the canonical consumer of the `@atjam/lexicons` read layer and validator. | 17 + | EPTSS | "Everyone Plays the Same Song" — a monthly cover-song challenge. | The motivating adopter. Rounds carry a `site.eptss.song` subject and accept `fm.plyr.track` submissions. Walked through end to end in [Extending atjam](./extending.md). | 18 + 19 + > EPTSS's own implementation lives in a separate repository and is not part of 20 + > this repo. Its pattern is described from atjam's published facts; treat the 21 + > EPTSS-side code as conceptual. Only the `at.atjam.*` contract is verified here. 22 + 23 + ## Worked example: a judged game jam (the rpg.actor shape) 24 + 25 + > **This is an illustration, not an adopter.** [rpg.actor](https://rpg.actor) is 26 + > a real AT Protocol project — a cross-game character registry where your 27 + > character travels between games (its published lexicons include 28 + > `actor.rpg.stats` and `actor.rpg.sprite`). It runs its own jam tooling and 29 + > does **not** use atjam. We borrow the shape of its 30 + > [Builder Jam](https://rpg.actor/jam) here only because a judged, votable game 31 + > jam happens to exercise every atjam extension point at once. The 32 + > `actor.rpg.game` / `actor.rpg.vote` schemas below are **illustrative** — what 33 + > an adopter would publish under their own namespace — not rpg.actor's real 34 + > records. 35 + 36 + The jam: over a fixed window, builders make a game that reads or writes a shared 37 + character registry; players then vote; winners get prizes. The whole thing lands 38 + on atjam with **no change to the contract** — the adopter publishes two small 39 + lexicons under their own domain, and everything else rides the spine. 40 + 41 + ### The split: spine vs. domain 42 + 43 + | Layer | Owned by | Records | 44 + |---|---|---| 45 + | Coordination spine | atjam (`at.atjam.*`) | `jam`, `round`, `signup`, `submission` (`invitation` if gated) | 46 + | Deliverable, theme, votes, results | the adopter (`actor.rpg.*`) | `game`, `vote` (and optionally `jamTheme`, `result`) | 47 + 48 + Design principle #4 (one namespace per owner) is the whole trick: `at.atjam.*` 49 + never learns what a "game" or a "vote" is. The adopter defines those under 50 + `actor.rpg.*` and embeds them by reference. 51 + 52 + ### 1. The adopter's lexicons (`actor.rpg.*`) 53 + 54 + Published under their own domain, exactly as atjam publishes `at.atjam.*` (see 55 + [PUBLISHING.md](../PUBLISHING.md)). The deliverable is a record describing the 56 + entry; the audio, build, or web app lives wherever it already lives. 57 + 58 + ```jsonc 59 + // actor.rpg.game — the deliverable an at.atjam.submission points at 60 + { 61 + "lexicon": 1, 62 + "id": "actor.rpg.game", 63 + "defs": { 64 + "main": { 65 + "type": "record", 66 + "description": "A game/tool entered into a Builder Jam.", 67 + "key": "tid", 68 + "record": { 69 + "type": "object", 70 + "required": ["title", "url", "createdAt"], 71 + "properties": { 72 + "title": { "type": "string", "maxLength": 300 }, 73 + "url": { "type": "string", "format": "uri" }, 74 + "engine": { "type": "string" }, 75 + "integratesLexicons": { 76 + "type": "array", 77 + "items": { "type": "string", "format": "nsid" } 78 + }, 79 + "createdAt": { "type": "string", "format": "datetime" } 80 + } 81 + } 82 + } 83 + } 84 + } 85 + ``` 86 + 87 + ```jsonc 88 + // actor.rpg.vote — a player's vote, strong-reffing a submission 89 + { 90 + "lexicon": 1, 91 + "id": "actor.rpg.vote", 92 + "defs": { 93 + "main": { 94 + "type": "record", 95 + "description": "A player's vote for a jam submission.", 96 + "key": "tid", 97 + "record": { 98 + "type": "object", 99 + "required": ["submission", "createdAt"], 100 + "properties": { 101 + "submission": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 102 + "createdAt": { "type": "string", "format": "datetime" } 103 + } 104 + } 105 + } 106 + } 107 + } 108 + ``` 109 + 110 + Two optional extras, also under `actor.rpg.*`: a `jamTheme` for the round's 111 + `subject` and a `result` for the winner announcement. Neither is required to run 112 + the jam. 113 + 114 + ### 2. The round — atjam's spine on the extension points 115 + 116 + The round is a plain `at.atjam.round`; the rpg.actor specifics sit on `subject`, 117 + `acceptedSubmissionTypes`, and `closingEvent`. (`createRecord` here is the app's 118 + own thin wrapper over `com.atproto.repo.createRecord` — see 119 + [Getting started](./getting-started.md); `@atjam/lexicons` ships no write 120 + helper.) 121 + 122 + ```ts 123 + import { NSIDS } from "@atjam/lexicons"; 124 + 125 + const round = await createRecord({ 126 + agent, // the organizer 127 + collection: NSIDS.round, // "at.atjam.round" 128 + record: { 129 + $type: NSIDS.round, 130 + jam: jamRef, 131 + assignment: "Build a game/tool that reads or writes the rpg.actor registry.", 132 + subject: { // extension point (unknown) 133 + $type: "actor.rpg.jamTheme", 134 + constraint: "Use a player's character class", 135 + }, 136 + acceptedSubmissionTypes: ["actor.rpg.game"], // the adopter's deliverable NSID 137 + milestones: [ 138 + { label: "submission-deadline", date: deadlineIso }, 139 + { label: "voting-opens", date: votingIso }, // custom phase 140 + { label: "results", date: resultsIso }, 141 + ], 142 + closingEvent: { // extension point (unknown) 143 + $type: "actor.rpg.awards", 144 + url: "https://rpg.actor/stream", 145 + }, 146 + createdAt: new Date().toISOString(), 147 + }, 148 + }); 149 + ``` 150 + 151 + ### 3. A builder enters 152 + 153 + The builder publishes their entry as an `actor.rpg.game` record on their *own* 154 + PDS, then signs up and submits — the submission's `payload` strong-refs the game 155 + record. 156 + 157 + ```ts 158 + // 1. Publish the entry (on the builder's PDS). Returns { uri, cid }. 159 + const gameRef = await createRecord({ 160 + agent, // the builder 161 + collection: "actor.rpg.game", 162 + record: { 163 + $type: "actor.rpg.game", 164 + title: "Tavern of Forking Paths", 165 + url: "https://my-itch.io/tavern", 166 + engine: "Godot", 167 + integratesLexicons: ["actor.rpg.sprite"], 168 + createdAt: new Date().toISOString(), 169 + }, 170 + }); 171 + 172 + // 2. Sign up for the round. 173 + await createRecord({ 174 + agent, 175 + collection: NSIDS.signup, // "at.atjam.signup" 176 + record: { $type: NSIDS.signup, round: roundRef, createdAt: new Date().toISOString() }, 177 + }); 178 + 179 + // 3. Submit — payload strong-refs the game record from step 1. 180 + await createRecord({ 181 + agent, 182 + collection: NSIDS.submission, // "at.atjam.submission" 183 + record: { 184 + $type: NSIDS.submission, 185 + round: roundRef, 186 + payload: gameRef, // { uri, cid } 187 + createdAt: new Date().toISOString(), 188 + }, 189 + }); 190 + ``` 191 + 192 + ### 4. Players vote — and tallying is free 193 + 194 + A vote is an `actor.rpg.vote` that strong-refs a submission. Counting votes is 195 + the **same** Constellation backlinks query that already counts signups — just 196 + pointed at the submission's AT URI instead of the round's. 197 + 198 + ```ts 199 + import { Queries } from "@atjam/lexicons"; 200 + 201 + // A player votes (on their own PDS). 202 + await createRecord({ 203 + agent, // the player 204 + collection: "actor.rpg.vote", 205 + record: { 206 + $type: "actor.rpg.vote", 207 + submission: { uri: submissionUri, cid: submissionCid }, 208 + createdAt: new Date().toISOString(), 209 + }, 210 + }); 211 + 212 + // Tally: identical to fetchSignupsForRound, retargeted to the submission. 213 + const votes = await Queries.getBacklinks({ 214 + target: submissionUri, 215 + collection: "actor.rpg.vote", 216 + path: ".submission.uri", 217 + limit: 500, 218 + }); 219 + 220 + // Enforce one-vote-per-player at READ time (writes can't be constrained): 221 + const voters = new Set(votes.links.map((l) => Queries.parseAtUri(l.uri).did)); 222 + const tally = voters.size; 223 + ``` 224 + 225 + That read-time posture is the same one atjam's own 226 + [signup validator](./reading-writing-validating.md) takes: anyone can write any 227 + record to their PDS, so eligibility and de-duplication are decided when you 228 + *read*, not enforced when others write. 229 + 230 + ### 5. Results 231 + 232 + The organizer writes an `actor.rpg.result` naming the winners and surfaces it at 233 + the `results` milestone; the awards stream itself rides the round's 234 + `closingEvent`. Both are domain records — atjam just provides the timeline slots. 235 + 236 + ## What this shows 237 + 238 + - **Zero changes to `at.atjam.*`.** The spine carried a judged competition it 239 + has never heard of. 240 + - **Two small lexicons** under the adopter's own namespace (`game`, `vote`) did 241 + all the domain work. 242 + - **Voting and results composed for free** on the existing strong-ref + 243 + backlinks substrate — no new index, no new primitive. 244 + 245 + That's the headline extensibility claim made concrete. To build your own: 246 + [Extending atjam](./extending.md) for the extension points, 247 + [Getting started](./getting-started.md) for the first records, and the 248 + [API reference](./api-reference.md) for exact signatures (including 249 + `Queries.getBacklinks`).
+1
web/app/components/doc.tsx
··· 24 24 label: "Reading, writing, validating", 25 25 }, 26 26 { to: "/docs/extending", label: "Extending atjam" }, 27 + { to: "/docs/built-on", label: "Built on atjam" }, 27 28 { to: "/docs/api-reference", label: "API reference" }, 28 29 ]; 29 30
+4
web/app/root.tsx
··· 114 114 docs 115 115 </Link> 116 116 <span className="mx-2">·</span> 117 + <Link to="/docs/built-on" className="hover:underline"> 118 + built on 119 + </Link> 120 + <span className="mx-2">·</span> 117 121 <Link to="/lexicon" className="hover:underline"> 118 122 lexicon 119 123 </Link>
+6
web/app/routes/docs._index.tsx
··· 176 176 reality, configuring the read layer. 177 177 </Li> 178 178 <Li> 179 + <strong>Showcase</strong> —{" "} 180 + <DocLink to="/docs/built-on">Built on atjam</DocLink>: real adopters, 181 + plus an end-to-end worked example mapping a judged, votable game jam 182 + onto the extension points. 183 + </Li> 184 + <Li> 179 185 <strong>Reference</strong> —{" "} 180 186 <DocLink to="/docs/api-reference">API reference</DocLink>: every 181 187 public export of <C>@atjam/lexicons</C> with exact signatures.
+373
web/app/routes/docs.built-on.tsx
··· 1 + import { 2 + DocTitle, 3 + Lead, 4 + Section, 5 + Sub, 6 + P, 7 + C, 8 + CodeBlock, 9 + Ul, 10 + Li, 11 + Note, 12 + DocLink, 13 + Ext, 14 + Table, 15 + TH, 16 + TR, 17 + TD, 18 + REPO, 19 + RepoFile, 20 + } from "~/components/doc"; 21 + import type { Route } from "./+types/docs.built-on"; 22 + 23 + export const meta: Route.MetaFunction = () => [ 24 + { title: "Built on atjam · docs" }, 25 + { 26 + name: "description", 27 + content: 28 + "Real adopters of the at.atjam.* lexicons, plus an end-to-end worked example of mapping a judged, votable game jam onto atjam's primitives.", 29 + }, 30 + ]; 31 + 32 + export default function BuiltOn() { 33 + return ( 34 + <div> 35 + <DocTitle>Built on atjam</DocTitle> 36 + <Lead> 37 + Who uses the <C>at.atjam.*</C> lexicons — and one end-to-end worked 38 + example of mapping a judged, votable game jam onto them. 39 + </Lead> 40 + 41 + <div className="mt-6 space-y-4 text-ink-2"> 42 + <P> 43 + atjam is a generic coordination layer: it knows about jams, rounds, 44 + signups, invitations, and submissions, and nothing about your domain. 45 + This page has two halves — the projects actually reading and writing{" "} 46 + <C>at.atjam.*</C> today, and a worked example that carries one 47 + adopter's jam all the way through the four{" "} 48 + <DocLink to="/docs/extending">extension points</DocLink>. 49 + </P> 50 + </div> 51 + 52 + <Section title="In the wild"> 53 + <Table 54 + head={ 55 + <> 56 + <TH>Project</TH> 57 + <TH>What it is</TH> 58 + <TH>How it uses atjam</TH> 59 + </> 60 + } 61 + > 62 + <TR> 63 + <TD> 64 + <Ext href={REPO.root}>atjam.at</Ext> (this repo) 65 + </TD> 66 + <TD>The reference web app.</TD> 67 + <TD> 68 + Reads and writes all five records; the canonical consumer of the{" "} 69 + <C>@atjam/lexicons</C> read layer and validator. 70 + </TD> 71 + </TR> 72 + <TR> 73 + <TD>EPTSS</TD> 74 + <TD> 75 + “Everyone Plays the Same Song” — a monthly cover-song challenge. 76 + </TD> 77 + <TD> 78 + The motivating adopter. Rounds carry a <C>site.eptss.song</C>{" "} 79 + subject and accept <C>fm.plyr.track</C> submissions. Walked through 80 + end to end in{" "} 81 + <DocLink to="/docs/extending">Extending atjam</DocLink>. 82 + </TD> 83 + </TR> 84 + </Table> 85 + <Note> 86 + EPTSS's own implementation lives in a separate repository and is not 87 + part of this repo. Its pattern is described from atjam's published 88 + facts; treat the EPTSS-side code as conceptual. Only the{" "} 89 + <C>at.atjam.*</C> contract is verified here. 90 + </Note> 91 + </Section> 92 + 93 + <Section title="Worked example: a judged game jam (the rpg.actor shape)"> 94 + <Note> 95 + <strong>This is an illustration, not an adopter.</strong>{" "} 96 + <Ext href="https://rpg.actor">rpg.actor</Ext> is a real AT&nbsp;Protocol 97 + project — a cross-game character registry where your character travels 98 + between games (its published lexicons include <C>actor.rpg.stats</C>{" "} 99 + and <C>actor.rpg.sprite</C>). It runs its own jam tooling and does{" "} 100 + <strong>not</strong> use atjam. We borrow the shape of its{" "} 101 + <Ext href="https://rpg.actor/jam">Builder Jam</Ext> here only because a 102 + judged, votable game jam happens to exercise every atjam extension 103 + point at once. The <C>actor.rpg.game</C> / <C>actor.rpg.vote</C>{" "} 104 + schemas below are <strong>illustrative</strong> — what an adopter would 105 + publish under their own namespace — not rpg.actor's real records. 106 + </Note> 107 + 108 + <P> 109 + The jam: over a fixed window, builders make a game that reads or writes 110 + a shared character registry; players then vote; winners get prizes. 111 + The whole thing lands on atjam with <strong>no change to the 112 + contract</strong> — the adopter publishes two small lexicons under 113 + their own domain, and everything else rides the spine. 114 + </P> 115 + 116 + <Sub title="The split: spine vs. domain"> 117 + <Table 118 + head={ 119 + <> 120 + <TH>Layer</TH> 121 + <TH>Owned by</TH> 122 + <TH>Records</TH> 123 + </> 124 + } 125 + > 126 + <TR> 127 + <TD>Coordination spine</TD> 128 + <TD> 129 + atjam (<C>at.atjam.*</C>) 130 + </TD> 131 + <TD> 132 + <C>jam</C>, <C>round</C>, <C>signup</C>, <C>submission</C>{" "} 133 + (<C>invitation</C> if gated) 134 + </TD> 135 + </TR> 136 + <TR> 137 + <TD>Deliverable, theme, votes, results</TD> 138 + <TD> 139 + the adopter (<C>actor.rpg.*</C>) 140 + </TD> 141 + <TD> 142 + <C>game</C>, <C>vote</C> (and optionally <C>jamTheme</C>,{" "} 143 + <C>result</C>) 144 + </TD> 145 + </TR> 146 + </Table> 147 + <P> 148 + Design principle #4 (one namespace per owner) is the whole trick:{" "} 149 + <C>at.atjam.*</C> never learns what a “game” or a “vote” is. The 150 + adopter defines those under <C>actor.rpg.*</C> and embeds them by 151 + reference. 152 + </P> 153 + </Sub> 154 + 155 + <Sub title="1. The adopter's lexicons (actor.rpg.*)"> 156 + <P> 157 + Published under their own domain, exactly as atjam publishes{" "} 158 + <C>at.atjam.*</C> (see <Ext href={REPO.publishing}>PUBLISHING.md</Ext> 159 + ). The deliverable is a record describing the entry; the audio, 160 + build, or web app lives wherever it already lives. 161 + </P> 162 + <CodeBlock>{`// actor.rpg.game — the deliverable an at.atjam.submission points at 163 + { 164 + "lexicon": 1, 165 + "id": "actor.rpg.game", 166 + "defs": { 167 + "main": { 168 + "type": "record", 169 + "description": "A game/tool entered into a Builder Jam.", 170 + "key": "tid", 171 + "record": { 172 + "type": "object", 173 + "required": ["title", "url", "createdAt"], 174 + "properties": { 175 + "title": { "type": "string", "maxLength": 300 }, 176 + "url": { "type": "string", "format": "uri" }, 177 + "engine": { "type": "string" }, 178 + "integratesLexicons": { 179 + "type": "array", 180 + "items": { "type": "string", "format": "nsid" } 181 + }, 182 + "createdAt": { "type": "string", "format": "datetime" } 183 + } 184 + } 185 + } 186 + } 187 + }`}</CodeBlock> 188 + <CodeBlock>{`// actor.rpg.vote — a player's vote, strong-reffing a submission 189 + { 190 + "lexicon": 1, 191 + "id": "actor.rpg.vote", 192 + "defs": { 193 + "main": { 194 + "type": "record", 195 + "description": "A player's vote for a jam submission.", 196 + "key": "tid", 197 + "record": { 198 + "type": "object", 199 + "required": ["submission", "createdAt"], 200 + "properties": { 201 + "submission": { 202 + "type": "ref", 203 + "ref": "com.atproto.repo.strongRef" 204 + }, 205 + "createdAt": { "type": "string", "format": "datetime" } 206 + } 207 + } 208 + } 209 + } 210 + }`}</CodeBlock> 211 + <P> 212 + Two optional extras, also under <C>actor.rpg.*</C>: a{" "} 213 + <C>jamTheme</C> for the round's <C>subject</C> and a <C>result</C>{" "} 214 + for the winner announcement. Neither is required to run the jam. 215 + </P> 216 + </Sub> 217 + 218 + <Sub title="2. The round — atjam's spine on the extension points"> 219 + <P> 220 + The round is a plain <C>at.atjam.round</C>; the rpg.actor specifics 221 + sit on <C>subject</C>, <C>acceptedSubmissionTypes</C>, and{" "} 222 + <C>closingEvent</C>. (<C>createRecord</C> here is the app's own thin 223 + wrapper over <C>com.atproto.repo.createRecord</C> — see{" "} 224 + <DocLink to="/docs/getting-started">Getting started</DocLink>;{" "} 225 + <C>@atjam/lexicons</C> ships no write helper.) 226 + </P> 227 + <CodeBlock>{`import { NSIDS } from "@atjam/lexicons"; 228 + 229 + const round = await createRecord({ 230 + agent, // the organizer 231 + collection: NSIDS.round, // "at.atjam.round" 232 + record: { 233 + $type: NSIDS.round, 234 + jam: jamRef, 235 + assignment: "Build a game/tool that reads or writes the rpg.actor registry.", 236 + subject: { // extension point (unknown) 237 + $type: "actor.rpg.jamTheme", 238 + constraint: "Use a player's character class", 239 + }, 240 + acceptedSubmissionTypes: ["actor.rpg.game"], // the adopter's deliverable NSID 241 + milestones: [ 242 + { label: "submission-deadline", date: deadlineIso }, 243 + { label: "voting-opens", date: votingIso }, // custom phase 244 + { label: "results", date: resultsIso }, 245 + ], 246 + closingEvent: { // extension point (unknown) 247 + $type: "actor.rpg.awards", 248 + url: "https://rpg.actor/stream", 249 + }, 250 + createdAt: new Date().toISOString(), 251 + }, 252 + });`}</CodeBlock> 253 + </Sub> 254 + 255 + <Sub title="3. A builder enters"> 256 + <P> 257 + The builder publishes their entry as an <C>actor.rpg.game</C> record 258 + on their <em>own</em> PDS, then signs up and submits — the 259 + submission's <C>payload</C> strong-refs the game record. 260 + </P> 261 + <CodeBlock>{`// 1. Publish the entry (on the builder's PDS). Returns { uri, cid }. 262 + const gameRef = await createRecord({ 263 + agent, // the builder 264 + collection: "actor.rpg.game", 265 + record: { 266 + $type: "actor.rpg.game", 267 + title: "Tavern of Forking Paths", 268 + url: "https://my-itch.io/tavern", 269 + engine: "Godot", 270 + integratesLexicons: ["actor.rpg.sprite"], 271 + createdAt: new Date().toISOString(), 272 + }, 273 + }); 274 + 275 + // 2. Sign up for the round. 276 + await createRecord({ 277 + agent, 278 + collection: NSIDS.signup, // "at.atjam.signup" 279 + record: { $type: NSIDS.signup, round: roundRef, createdAt: new Date().toISOString() }, 280 + }); 281 + 282 + // 3. Submit — payload strong-refs the game record from step 1. 283 + await createRecord({ 284 + agent, 285 + collection: NSIDS.submission, // "at.atjam.submission" 286 + record: { 287 + $type: NSIDS.submission, 288 + round: roundRef, 289 + payload: gameRef, // { uri, cid } 290 + createdAt: new Date().toISOString(), 291 + }, 292 + });`}</CodeBlock> 293 + </Sub> 294 + 295 + <Sub title="4. Players vote — and tallying is free"> 296 + <P> 297 + A vote is an <C>actor.rpg.vote</C> that strong-refs a submission. 298 + Counting votes is the <strong>same</strong> Constellation backlinks 299 + query that already counts signups — just pointed at the submission's 300 + AT URI instead of the round's. 301 + </P> 302 + <CodeBlock>{`import { Queries } from "@atjam/lexicons"; 303 + 304 + // A player votes (on their own PDS). 305 + await createRecord({ 306 + agent, // the player 307 + collection: "actor.rpg.vote", 308 + record: { 309 + $type: "actor.rpg.vote", 310 + submission: { uri: submissionUri, cid: submissionCid }, 311 + createdAt: new Date().toISOString(), 312 + }, 313 + }); 314 + 315 + // Tally: identical to fetchSignupsForRound, retargeted to the submission. 316 + const votes = await Queries.getBacklinks({ 317 + target: submissionUri, 318 + collection: "actor.rpg.vote", 319 + path: ".submission.uri", 320 + limit: 500, 321 + }); 322 + 323 + // Enforce one-vote-per-player at READ time (writes can't be constrained): 324 + const voters = new Set(votes.links.map((l) => Queries.parseAtUri(l.uri).did)); 325 + const tally = voters.size;`}</CodeBlock> 326 + <P> 327 + That read-time posture is the same one atjam's own{" "} 328 + <DocLink to="/docs/reading-writing-validating"> 329 + signup validator 330 + </DocLink>{" "} 331 + takes: anyone can write any record to their PDS, so eligibility and 332 + de-duplication are decided when you <em>read</em>, not enforced when 333 + others write. 334 + </P> 335 + </Sub> 336 + 337 + <Sub title="5. Results"> 338 + <P> 339 + The organizer writes an <C>actor.rpg.result</C> naming the winners 340 + and surfaces it at the <C>results</C> milestone; the awards stream 341 + itself rides the round's <C>closingEvent</C>. Both are domain 342 + records — atjam just provides the timeline slots. 343 + </P> 344 + </Sub> 345 + </Section> 346 + 347 + <Section title="What this shows"> 348 + <Ul> 349 + <Li> 350 + <strong>Zero changes to <C>at.atjam.*</C>.</strong> The spine carried 351 + a judged competition it has never heard of. 352 + </Li> 353 + <Li> 354 + <strong>Two small lexicons</strong> under the adopter's own 355 + namespace (<C>game</C>, <C>vote</C>) did all the domain work. 356 + </Li> 357 + <Li> 358 + <strong>Voting and results composed for free</strong> on the existing 359 + strong-ref + backlinks substrate — no new index, no new primitive. 360 + </Li> 361 + </Ul> 362 + <P> 363 + That's the headline extensibility claim made concrete. To build your 364 + own: <DocLink to="/docs/extending">Extending atjam</DocLink> for the 365 + extension points, <DocLink to="/docs/getting-started">Getting 366 + started</DocLink> for the first records, and the{" "} 367 + <DocLink to="/docs/api-reference">API reference</DocLink> for exact 368 + signatures (including <C>Queries.getBacklinks</C>). 369 + </P> 370 + </Section> 371 + </div> 372 + ); 373 + }