The Antiphony lexicons
The AT Protocol record definitions under dev.antiphony.* describe everything Antiphony stores. The REST API, the Zod schemas in packages/shared, and every app built on the core derive from them. If you read one page before building, read this one.
Source of truth: lexicons/dev/antiphony/. This page is the guided tour.
Why do these look like Bluesky’s records?
Section titled “Why do these look like Bluesky’s records?”Antiphony mirrors the app.bsky.* shapes wherever one exists, so an AT Protocol client already understands the record. It adds a new shape only where audio call-and-response needs one. There are two such gaps:
Audio embeds. atproto has image, video, external and record embeds, but no audio. dev.antiphony.embed.audio is the protocol’s audio attachment.
Timed transcripts. A machine transcript stored as platform enrichment, not a field the author writes.
What is a post?
Section titled “What is a post?”dev.antiphony.audio.post
One record carries both halves of the conversation. A post without a reply is a prompt, the root of a thread. A post with one is a reply. The audio rides in embed; text is what the author typed, never the transcript.
Threading is content-addressed. reply.root points at the prompt; reply.parent points at the post being answered. Who may reply is not a field: the AppView gates replies into participant-only sub-threads. See reply gating.
{ "$type": "dev.antiphony.audio.post", "text": "First-job advice?", "title": "Your first year at work", "embed": { "$type": "dev.antiphony.embed.audio", "audio": { "$type": "blob", "ref": { "$link": "bafkreihdwdcefgh4dqkjv67uzc…" }, "mimeType": "audio/mp4", "size": 672310 }, "durationMs": 42000 }, "langs": [ "en" ], "createdAt": "2026-09-24T18:00:00.000Z"}{ "$type": "dev.antiphony.audio.post", "text": "", "embed": { "$type": "dev.antiphony.embed.audio", "audio": { "$type": "blob", "ref": { "$link": "bafkreihdwdcefgh4dqkjv67uzc…" }, "mimeType": "audio/mp4", "size": 672310 }, "durationMs": 31000 }, "reply": { "root": { "uri": "at://did:web:voxpop.audio/dev.antiphony.audio.post/3k6w", "cid": "bafyreib2rxk3ry…" }, "parent": { "uri": "at://did:web:voxpop.audio/dev.antiphony.audio.post/3k6w", "cid": "bafyreib2rxk3ry…" } }, "langs": [ "en" ], "createdAt": "2026-09-24T18:06:12.000Z"}How is audio attached?
Section titled “How is audio attached?”dev.antiphony.embed.audio
The embed has two forms. The record is what you write: the stored bytes and facts that don’t depend on rendering. The view is what a read gives back: a playable URL, the transcript, and the values that describe the audio the listener actually hears.
url, durationMs and waveform move together. Once processing produces a variant, all three describe it, or none do. If you cached duration or peaks at upload, read them again from the view: trim shortens the audio and waveform recomputes the peaks. Uploads have a tighter size limit than the lexicon; see Limits.
{ "$type": "dev.antiphony.embed.audio", "audio": { "$type": "blob", "ref": { "$link": "bafkreihdwdcefgh4dqkjv67uzc…" }, "mimeType": "audio/mp4", "size": 672310 }, "durationMs": 42000, "alt": "Alice asks about first jobs", "waveform": [ 12, 28, 45, 60, 75, 90, 85, 60 ]}{ "$type": "dev.antiphony.embed.audio#view", "url": "https://…/signed-playback-url", "durationMs": 38400, "waveform": [ 10, 31, 52, 64, 80, 88, 71, 49 ], "alt": "Alice asks about first jobs", "transcript": { "segments": [ { "startMs": 0, "endMs": 2400, "text": "What’s the best advice…" } ], "text": "What’s the best advice you got in your first year of work?" }}Where does the transcript live?
Section titled “Where does the transcript live?”dev.antiphony.audio.transcript
In its own record, never on the post. The transcript is platform enrichment, like denoise or waveforms. It points at the post by StrongRef, the same pattern as likes and labels, and is lifted into embed.audio#view.transcript at read time.
A #timedTranscript is a list of segments, each { startMs, endMs, text }, plus an optional plain-text rollup for consumers that don’t need timing.
{ "$type": "dev.antiphony.audio.transcript", "subject": { "uri": "at://did:web:voxpop.audio/dev.antiphony.audio.post/3k6w", "cid": "bafyreib2rxk3ry…" }, "transcript": { "segments": [ { "startMs": 0, "endMs": 2400, "text": "What’s the best advice" }, { "startMs": 2400, "endMs": 4100, "text": "you got in your first year?" } ] }, "lang": "en", "model": "whisper", "createdAt": "2026-09-24T18:00:41.000Z"}What else is in the namespace?
Section titled “What else is in the namespace?”dev.antiphony.embed.recordWithAudio
A post that quotes another record and carries its own audio. The audio counterpart of app.bsky.embed.recordWithMedia.
dev.antiphony.actor.profile
One record per actor at the self rkey, with a public handle, an optional usage intent and an RSS feed. Lexicon-only: the core never stores or serves it. Profiles belong to the calling app; this shape exists so a federating deployment has somewhere to project them.
How do the records relate?
Section titled “How do the records relate?”A prompt and its replies are all audio.post records, threaded by StrongRef. Each carries an embed.audio. The transcript is its own record, folded into the embed’s view only once it exists.
How faithful is this to AT Protocol?
Section titled “How faithful is this to AT Protocol?”Records validate against the official @atproto/lexicon parser, blob refs use the canonical JSON shape, and CIDs are real content addresses.
Blob CIDs. CIDv1, raw codec, sha2-256 over the audio bytes. Storage location is derived from the CID and never stored, so records stay portable across deployments.
Record CIDs. CIDv1, dag-cbor, sha2-256 over the public record fields. Every view’s cid, and every StrongRef in a reply, is verifiable.
The at:// authority is the tenant app’s DID. Antiphony owns the repo, so a post’s URI authority is always the tenant’s did:web. The acting user’s identity rides alongside as authorDid, outside the record CID. See Tenant identity and the authority model.
{ "$type": "blob", "ref": { "$link": "bafkreihdwdcefgh4dqkjv67uzc…" }, "mimeType": "audio/mp4", "size": 672310}