The Antiphony lexicons
The lexicons are the heart of Antiphony. They are the AT Protocol record definitions under the dev.antiphony.* namespace that describe everything the system stores — portably, content-addressably, and legibly to existing AT Protocol tooling. The REST API, the Zod schemas in packages/shared, and every app built on the core all derive from these.
If you read one thing before building on Antiphony, read this.
The source of truth is the JSON in lexicons/dev/antiphony/. This page is the guided tour.
Design stance
Section titled “Design stance”Antiphony’s lexicons mirror the Bluesky (app.bsky.*) shapes wherever one exists, and contribute new ones only where the protocol has a gap. dev.antiphony.audio.post is structured like app.bsky.feed.post; dev.antiphony.embed.audio is modeled on app.bsky.embed.video. The payoff: a record an AT Protocol client already understands, differing only where audio call-and-response genuinely needs something new.
Two such gaps are filled here:
- Audio embeds. atproto has image, video, external, and record embeds — but no audio.
dev.antiphony.embed.audiois the protocol’s audio attachment. - Timed transcripts. A machine transcript modeled as platform enrichment, not a field the author writes.
The records
Section titled “The records”dev.antiphony.audio.post — the one canonical content record
Section titled “dev.antiphony.audio.post — the one canonical content record”A single record type carries both halves of call-and-response:
- A post without a
replyis a prompt — the root of a thread. - A post with a
replyis a reply.
reply presence is the discriminator — not title, which is an optional prompt-only headline. The audio rides in embed; the typed text is the author’s question or caption (max 300 graphemes / 3000 bytes, bsky-semantic), never the transcript.
| Field | Type | Notes |
|---|---|---|
text |
string | User-authored text. May be empty for pure-audio posts. Required. |
title |
string? | Optional headline. A prompt feature; not the discriminator. |
embed |
union? | dev.antiphony.embed.audio, …embed.recordWithAudio, app.bsky.embed.record, or …embed.external. |
reply |
ref? | Present iff this is a reply. { root, parent }, each a com.atproto.repo.strongRef. |
langs |
string[]? | BCP-47 language tags (max 3). |
labels |
union? | Author-applied com.atproto.label.defs#selfLabels (content warnings). |
createdAt |
datetime | ISO 8601. Required. |
Threading is content-addressed. A reply’s reply.root points at the prompt at the top of the thread; reply.parent points at the post being directly answered (the prompt, or another reply). Both are StrongRefs ({ uri, cid }) — portable pointers that replace the legacy flat promptId.
Who may reply isn’t a field on this record — it’s enforced by the AppView (replies form participant-only sub-threads). See reply gating.
dev.antiphony.embed.audio — the audio attachment
Section titled “dev.antiphony.embed.audio — the audio attachment”Antiphony’s contribution to the AT Protocol embed family. The record form holds the stored, render-time-independent bytes and metadata; the view form is what a read returns.
Record (#main):
| Field | Type | Notes |
|---|---|---|
audio |
blob | audio/*, up to 100 MB, content-addressed by CID. Required. (The upload endpoint is deliberately tighter — see Limits.) |
durationMs |
integer? | Duration in milliseconds (the platform-wide unit). |
alt |
string? | User-authored short description (the audio analogue of image alt text). Not the transcript. |
waveform |
integer[]? | Pre-computed visual peaks, normalized 0–100, for instant rendering. |
View (#view) — what hydration returns:
| Field | Type | Notes |
|---|---|---|
url |
uri | Resolved, playable URL — the processed variant once opt-in processing has produced one, otherwise the original. In the centralized deployment this is a short-lived signed Storage URL, not the raw blob. Required. |
durationMs |
integer? | The processed variant’s duration once a byte-mutating stage (trim) has run; otherwise the record’s value. |
waveform |
integer[]? | Server-computed peaks once the waveform stage is ready; otherwise the client-supplied peaks from the record. |
alt |
string? | Copied from the record embed (never processed). |
transcript |
ref? | The timed transcript, lifted from the enrichment record at read time. Absent until transcription completes. |
The split is the important part: the record stores universal facts; the view carries the resolved playback URL, the lifted transcript, and — for url, durationMs, and waveform — whichever value describes the audio the reader actually plays. url, durationMs, and waveform move together: peaks are drawn across a duration and a duration describes specific bytes, so all three resolve to the processed variant or none do. A client that cached durationMs/waveform at upload time should re-read them from the view, since they change once processing runs (trim shortens the audio, waveform recomputes the peaks). The transcript is never stored on the post.
dev.antiphony.audio.transcript — platform enrichment
Section titled “dev.antiphony.audio.transcript — platform enrichment”A machine-generated, timed transcript of a post’s audio. It is platform-owned enrichment (like denoise or waveform generation), stored as its own record that references the post by StrongRef — the same pattern as likes and labels. It is lifted into dev.antiphony.embed.audio#view.transcript at read time.
| Field | Type | Notes |
|---|---|---|
subject |
strongRef | The post whose audio this transcribes. Required. |
transcript |
ref | A #timedTranscript (segments + optional text rollup). Required. |
lang |
string? | BCP-47 language tag of the transcript. |
model |
string? | Identifier of the model/provider (provenance). |
createdAt |
datetime | Required. |
A #timedTranscript is an array of #segments — each { startMs, endMs, text } — plus an optional concatenated text rollup for consumers that don’t need timing (the audio analogue of WebVTT captions).
dev.antiphony.embed.recordWithAudio — quote + audio
Section titled “dev.antiphony.embed.recordWithAudio — quote + audio”The audio analogue of app.bsky.embed.recordWithMedia: a post that both quotes another record (app.bsky.embed.record) and carries its own dev.antiphony.embed.audio.
dev.antiphony.actor.profile — the actor profile
Section titled “dev.antiphony.actor.profile — the actor profile”One record per actor at the well-known self rkey, mirroring app.bsky.actor.profile. Carries a public handle (distinct from the AT Protocol identity handle), an optional usageIntent (e.g. Podcaster, Listener), and an optional rssFeed URL.
This one is lexicon-only: the core never stores, serves, or CRUDs it — profile data is owned by the calling app, and this shape exists so a federating or exporting deployment has a well-known record to project that profile into.
How the records relate
Section titled “How the records relate”dev.antiphony.audio.post (prompt: no reply) ▲ reply.root / reply.parent (StrongRef) │dev.antiphony.audio.post (reply: has reply) │ embed ▼dev.antiphony.embed.audio ──(#view.transcript lifted at read time)──▶ dev.antiphony.audio.transcriptA prompt and its replies are all audio.post records, threaded by StrongRef. Each carries an embed.audio. The transcript lives in its own record and is folded into the embed’s view only when it exists.
How faithful is this to AT Protocol?
Section titled “How faithful is this to AT Protocol?”The records validate against the official @atproto/lexicon parser, blob refs
use the canonical JSON shape ({ "$type": "blob", "ref": { "$link": "<cid>" }, "mimeType", "size" }),
and CIDs are real content addresses computed by the AT Protocol rules:
- Blob CIDs — CIDv1,
rawcodec, sha2-256 over the audio bytes, computed at upload. Storage location is derived from the CID (never stored on the record), so records stay portable across deployments. - Record CIDs — CIDv1,
dag-cborcodec, sha2-256 over the canonical lexicon record (the public fields only). Thecidon every view — and in every reply’sreply.root/reply.parentStrongRef — is therefore a verifiable content address.
One thing to know about identity so integrators aren’t surprised:
at://URI authority is the tenant app DID. Antiphony is the repo owner (app-as-repo-owner — seespecs/atproto-authority-model.md, and Tenant identity for how a deployment pins and proves it), so a post’s URI authority is always the tenant’s owndid:web(at://did:web:<tenant>/dev.antiphony.audio.post/<rkey>), never an internal id or handle. The acting user’s own identity, when the caller asserts one, rides alongside as theauthorDidattribution facet — outside the record CID, distinct from the URI authority.
Where next?
Section titled “Where next?”- API reference — the REST surface derived from these records.
- Build your own app — put a client on top of them.
- Architecture — where the record→lexicon transform lives in the core.