{"version":"1.5","generated_at":"2026-07-29T14:08:22.085Z","about":{"name":"Zuke","pitch":"White-label live audio for Farcaster communities. Built on Juke. Graduated from ZAO OS - The ZAO is the first deployment.","farcaster":"https://farcaster.xyz/~/channel/zao","site":"https://zuke.thezao.com","juke_path_a_route":"/live/{spaceId}","juke_path_b_route":"/api/juke/space (admin or shared password)","public_status_route":"https://zuke.thezao.com/juke-status"},"shipped":[{"id":"path-a-iframe","title":"Path A — keyless iframe at /live/{spaceId}","description":"Public route that embeds juke.audio/embed/{id} with ZAO chrome. No API keys, anonymous listen by default, SIWF inside the iframe for participation.","shippedAt":"2026-05-20","pr":"https://github.com/bettercallzaal/ZAOOS/pull/598","files":["src/lib/spaces/juke.ts","src/components/spaces/JukeEmbed.tsx","src/app/live/[spaceId]/page.tsx"],"reference":"juke.audio/llms.txt — Fastest Integration: Hosted Iframe"},{"id":"path-b-developer-create","title":"Path B — server-side space creation via POST /v1/developer/spaces","description":"Key-only auth (X-Juke-Api-Key), room owner derived from app.owner_fid. Admin-or-password gated route at /api/juke/space; /live/create web form. Persists juke_spaces row on success.","shippedAt":"2026-05-22","pr":"https://github.com/bettercallzaal/ZAOOS/pull/630","files":["src/lib/spaces/juke-api.ts","src/app/api/juke/space/route.ts","src/app/live/create/page.tsx","scripts/test-juke-space.ts"],"reference":"juke.audio/llms.txt — Custom Server Integration: Developer Keys"},{"id":"webhook-consumer","title":"Inbound webhook consumer at /api/juke/webhooks","description":"HMAC-SHA256 verifier for X-Juke-Signature: t={ts},v1={hex} over `{ts}.{body}`. 5-minute replay window. Idempotent via signature_hash unique constraint. Handlers cover room.started, room.finished, participant.joined, participant.left, recording.ready.","shippedAt":"2026-05-23","pr":"https://github.com/bettercallzaal/ZAOOS/pull/640","files":["src/app/api/juke/webhooks/route.ts","src/lib/spaces/jukeWebhookVerify.ts","src/lib/spaces/jukeWebhookHandlers.ts","src/lib/spaces/jukeSpacesDb.ts","scripts/juke-spaces-migration.sql","scripts/register-juke-webhook.ts"],"reference":"Juke 2026-05-23 PR — outbound developer webhooks"},{"id":"audio-off-second-screen","title":"?audio=off second-screen mode","description":"jukeEmbedUrl(spaceId, { audioOff: true }) returns the embed with audio disabled. UI offers a \"Mute (second screen)\" toggle on /live/{id}. Solves the laptop-alongside-iOS-app double-broadcast case.","shippedAt":"2026-05-23","pr":"https://github.com/bettercallzaal/ZAOOS/pull/640","files":["src/lib/spaces/juke.ts","src/components/spaces/JukeEmbed.tsx"],"reference":"Juke 2026-05-23 PR — embed audio=off (item #6)"},{"id":"og-image","title":"OG image per space","description":"generateMetadata pulls juke.audio/space/{id}/opengraph-image for Open Graph + Twitter card meta tags. Cast/X shares of /live/{id} render the Juke-branded card without ZAO having to render its own.","shippedAt":"2026-05-23","pr":"https://github.com/bettercallzaal/ZAOOS/pull/640","files":["src/app/live/[spaceId]/page.tsx","src/lib/spaces/juke.ts"],"reference":"Juke 2026-05-23 PR — item #10"},{"id":"ios-deeplink","title":"\"Open in Juke app\" CTA","description":"jukeAppDeeplinkUrl(spaceId) returns juke.audio/space/{id}?open=app. Button on /live/{id} routes desktop visitors into the iOS app via universal link.","shippedAt":"2026-05-23","files":["src/lib/spaces/juke.ts","src/app/live/[spaceId]/page.tsx"],"reference":"Juke 2026-05-23 PR — item #8"},{"id":"recording-shelf","title":"Public /live/recordings shelf","description":"Merges two sources, most-recently-ended first: legacy juke_spaces rows with recording_url set (recording.ready webhook), and any space with a row in juke_recordings (host uploads, imported X Spaces, multi-part) even if recording_url was never set on juke_spaces. Each card shows the Juke OG image (Juke-hosted only) or a generic icon, a source badge (Juke / X Space), and an inline audio player with an \"Open in new tab\" link when a recording URL exists.","shippedAt":"2026-05-23","files":["src/app/live/recordings/page.tsx","src/lib/spaces/jukeSpacesDb.ts","src/lib/spaces/recordingsDb.ts"]},{"id":"recap-cast","title":"Auto-cast on recording.ready (wiring shipped, posting not yet live)","description":"After persisting recording_url, the webhook handler calls autoCastToZao with a recap cast to /zao, embedding the Juke /live/{id} URL so the Juke OG image renders in the cast preview. In the Zuke repo autoCastToZao (src/lib/publish/auto-cast.ts) is currently a stub - no @thezao Farcaster signer credential is provisioned yet, so it logs and no-ops rather than actually posting. The call site is wired and ready; only the signer credential is missing.","shippedAt":"2026-05-23","files":["src/lib/spaces/jukeWebhookHandlers.ts","src/lib/publish/auto-cast.ts"]},{"id":"public-status-surfaces","title":"Public build-status surfaces for the Juke team","description":"Three mirrors of this manifest: /juke-status (HTML dashboard with live stats + architecture diagram), /api/juke/status (JSON, CORS open, X-ZAO-Juke-Status: v3 header), /juke-integration.md (llms.txt-style markdown, X-ZAO-Juke-Status: v2 header). Single source of truth in jukeIntegrationManifest.ts.","shippedAt":"2026-05-23","files":["src/lib/spaces/jukeIntegrationManifest.ts","src/app/juke-status/page.tsx","src/app/api/juke/status/route.ts","src/app/juke-integration.md/route.ts"]},{"id":"live-public-discovery","title":"Public /live index of ZAO Juke spaces","description":"Anyone can browse Live / Scheduled / Recent ZAO Juke spaces without auth. Each card routes to /live/{id} (keyless iframe). Includes a paste-link form for non-ZAO spaces.","shippedAt":"2026-05-23","files":["src/app/live/page.tsx","src/app/live/JukeLinkOpener.tsx"]},{"id":"schedule-space-ui","title":"scheduledAt support in the space-create API","description":"POST /api/juke/space accepts an optional ISO-8601 scheduledAt and threads it through to Juke (createSpaceSchema in the route). The public /live/create page itself only exposes password + title fields, no scheduled_at input or time prefill - scheduling a space currently requires calling the API directly with a scheduledAt value.","shippedAt":"2026-05-23","files":["src/app/live/create/page.tsx","src/app/api/juke/space/route.ts"],"reference":"Juke 2026-05-23 PR — scheduled spaces (item #5)"},{"id":"admin-register-webhook","title":"Admin route to register the Juke webhook server-side","description":"POST /api/juke/admin/register-webhook calls Juke /v1/developer/webhooks from a Vercel context that already has JUKE_API_KEY loaded. Juke generates the HMAC secret server-side and returns it in the response; the admin caller copies it into the JUKE_WEBHOOK_SECRET env var (Production + Preview + Development) and redeploys. Admin-only.","shippedAt":"2026-05-24","pr":"https://github.com/bettercallzaal/ZAOOS/pull/666","files":["src/app/api/juke/admin/register-webhook/route.ts"]},{"id":"juke-status-richer","title":"Richer /juke-status: recent webhooks + recent spaces + code examples","description":"Three new sections on the public dashboard. (1) Recent webhooks - up to 8 events shown (of the 15 fetched; the fuller 15-row list is what /api/juke/status and /juke-integration.md return) with type / space_id / age / processed-vs-failed pill. (2) Recent spaces - up to 6 rows shown (of the 10 fetched, same JSON/markdown-vs-page split) with status pill + time marker + participant count + recording link. (3) Code examples - 4 reference snippets matching production (create-space, embed, webhook verify, subscribe). Plus OG + Twitter card meta on the page itself, and recent_spaces + recent_events arrays added to /api/juke/status and /juke-integration.md.","shippedAt":"2026-05-24","pr":"https://github.com/bettercallzaal/ZAOOS/pull/668","files":["src/lib/spaces/jukeSpacesDb.ts","src/app/juke-status/page.tsx","src/app/api/juke/status/route.ts","src/app/juke-integration.md/route.ts"]},{"id":"register-webhook-fix","title":"Register-webhook fix: Juke generates the HMAC secret, not us","description":"Initial admin route POSTed { url, events, secret } and Juke returned 422 extra_forbidden on the secret field - Juke generates the secret server-side and returns it in the response. Route now POSTs { url, events } only, captures juke.secret from the response, returns it with an action_required instructing the admin to copy it into Vercel's JUKE_WEBHOOK_SECRET env. Server logs the registration with the secret redacted.","shippedAt":"2026-05-24","pr":"https://github.com/bettercallzaal/ZAOOS/pull/669","files":["src/app/api/juke/admin/register-webhook/route.ts"]},{"id":"webhook-payload-parser","title":"Webhook payload parser: event_type / data.room_id / event_id","description":"parseWebhookEvent now reads Juke 2026-05-23 shape (event_type + event_id at top level, data.room_id for the space id) instead of the legacy event / type / data.id fields. Defensive aliases keep the older shape working. readParticipant accepts fid / participant_fid / user_fid / host_fid + display_name / displayName / username for human-or-agent identification. Result: webhooks no longer log \"no space_id\" and lifecycle updates apply.","shippedAt":"2026-05-24","pr":"https://github.com/bettercallzaal/ZAOOS/pull/677","files":["src/lib/spaces/jukeWebhookHandlers.ts"]},{"id":"recap-cast-room-finished","title":"Recap cast on room.finished (ended_via host/api only; wiring shipped, posting not yet live)","description":"When a Juke space ends with ended_via in {host, api}, the webhook handler calls autoCastToZao with a 'Just wrapped: {title}' message for /zao, embedding the /live/{id} URL so Farcaster would unfurl the OG card. Skips silent idle-timeouts (ended_via=null) since there's nobody to recap to. Same caveat as recap-cast: autoCastToZao is a stub in the Zuke repo pending a @thezao signer credential, so no cast is actually posted yet. The recording.ready handler still fires its own 'Recording up' follow-up call independently when a recording is on - two-cast pattern is intentional so listeners get a re-engagement ping when the file lands, once casting is live.","shippedAt":"2026-05-25","files":["src/lib/spaces/jukeWebhookHandlers.ts"],"reference":"Branches on Nicky 2026-05-24 ended_via payload addition."},{"id":"developer-reads-and-observability","title":"Consumer code for Juke developer reads + rate-limit observability","description":"Wraps Juke's PR #175 ship (2026-05-25): GET /v1/developer/spaces/{id} returns RoomDetailResponse (status + participants + recording in one call), GET /v1/developer/webhooks/{id} returns delivery health, DELETE /v1/developer/webhooks/{id} cleans up orphans (already existed). New helper at src/lib/spaces/juke-api-reads.ts surfaces all three behind one client + extracts X-Juke-Rate-Limit-Limit / Remaining / Reset from every response, logging a warn when remaining drops below 20% of the limit. Stale-room cron at /api/cron/juke-stale-rooms now uses GET /spaces/{id} as the authoritative source - only flips a row to ended when Juke confirms ended (or 404s), trusting Juke over our webhook timeline. Fallback to the older heuristic when JUKE_API_KEY is absent (local/preview). Admin route /api/juke/admin/delete-webhook wraps DELETE with an introspection-before-delete audit log.","shippedAt":"2026-05-25","files":["src/lib/spaces/juke-api-reads.ts","src/app/api/juke/admin/delete-webhook/route.ts","src/app/api/cron/juke-stale-rooms/route.ts"],"reference":"Nicky 2026-05-25 ship: GET reads + X-Juke-Rate-Limit-* + X-Juke-Idempotency-Key headers (PR #175)."},{"id":"host-end-space-button","title":"Host \"End space\" button on /live/{id} + admin end-space route","description":"Iframe Leave is a pure LiveKit room.disconnect() with anon: participant identity - no API call, so rooms we create via developer API stay alive until LiveKit's 300s empty-room timeout. EndJukeSpaceButton on /live/{id} (gated to host or admin via SSR session) calls POST /api/juke/admin/end-space which proxies to Juke's POST /v1/developer/spaces/{id}/end (Nicky's PR #174). On a 404 from Juke (a cross-app / iOS-native room we don't own - the end-space endpoint itself has shipped since PR #174), the route falls back to flipping our local juke_spaces row to ended so /live stops showing dead rooms as Live. The webhook handler remains the source of truth for the canonical room.finished event - we do not pre-flip our DB on the success path. Two-step confirm pattern on the button prevents fat-finger ends.","shippedAt":"2026-05-24","files":["src/app/api/juke/admin/end-space/route.ts","src/app/api/juke/admin/mark-ended/route.ts","src/components/spaces/EndJukeSpaceButton.tsx","src/app/live/[spaceId]/page.tsx"],"reference":"Nicky 2026-05-24 confirmation: POST /v1/developer/spaces/{room_id}/end, X-Juke-Api-Key auth, idempotent, fires room.finished synchronously with ended_via: 'host'|'api' payload."},{"id":"agent-join-consumer","title":"Consumer code for the agent-join endpoint (admin-triggered; auto-join off by default)","description":"Juke's free, key-only POST /v1/developer/rooms/{id}/agent-join (shipped 2026-05-23; data-publish only in v1, audio-publish is v1.x roadmap) has two independent consumers in this repo: POST /api/juke/admin/agent-join (an admin-only route with its own inline fetch to the endpoint, mints a short-lived session_token for the caller to store) and the shared helper joinAgentInJukeRoom (src/lib/spaces/jukeAgentJoin.ts), called from an auto-join hook on the room.started webhook, gated off by default behind ZAO_AUTO_AGENT_JOIN (isAutoAgentJoinEnabled()). The two do not share code - the admin route does not call joinAgentInJukeRoom. The mechanism works today for a human-triggered admin join; the auto-join hook exists but stays off because ZOE has no VPS-side consumer for the minted session_token yet - flipping the flag before that exists would join + immediately drop the token with no value. See the 'agents' OPEN_ASKS entry for what's still actually blocked.","shippedAt":"2026-05-23","files":["src/lib/spaces/jukeAgentJoin.ts","src/app/api/juke/admin/agent-join/route.ts","src/lib/spaces/jukeWebhookHandlers.ts"],"reference":"Nicky 2026-05-23 ship: POST /v1/developer/rooms/{id}/agent-join, X-Juke-Api-Key auth, data-only publish in v1."}],"open_asks":[{"id":"embed-siwf-sign-in-failing","title":"Hosted embed \"Sign in to participate\" fails instantly on approve","reason":"Confirmed live 2026-07-21: tapping Sign in to participate inside the hosted iframe (juke.audio/embed/{id}) shows the correct Warpcast consent screen (app name, requested scope all correct), but the instant the user taps Sign in inside Warpcast, it fails immediately with a bare \"Sign in failed\" and zero further detail - repeatable every time, on an up-to-date Warpcast app. Ruled out account/device: the SAME Farcaster account completes SIWF successfully on an unrelated third-party app moments later, so this is isolated to the embed's own sign-in implementation, not the user's client. We independently hit and fixed the identical symptom in our own separate SIWF integration (Zuke's own admin login, unrelated to this embed) - two root causes there: (1) a nonce format with non-alphanumeric separators, which SIWE/EIP-4361 requires to be entirely alphanumeric - Warpcast validates this at signing time and rejects a non-conforming nonce with exactly this silent instant-fail; (2) a domain/uri field mismatch in the SIWE message construction. Per your own SDK docs (juke.audio/SKILL.md, 'How the trust boundary works'), the SDK builds its SIWE message using domain = window.location.hostname - worth checking whether the nonce format issued by POST /v1/auth/siwf/nonce is guaranteed entirely alphanumeric, and whether domain/uri construction is consistent, since those are the two things that fixed the identical symptom on our side.","blocks":"Any listener promoting to speaker/reacting inside the hosted embed - listening-only works fine","priority":"p0"},{"id":"agents","title":"Agent join surface — when can ZOE sit in a Juke room, unattended?","reason":"Juke's free key-only agent-join endpoint (2026-05-23 ship, data-publish only in v1) already covers the read-only/observer case this ask originally requested, and we have a working consumer (see the agent-join-consumer SHIPPED entry) an admin can trigger by hand today - but only against a room actually created with allow_agents:true. The real /live/create flow used for real ZAO events never sets that flag (createJukeSpace defaults allow_agents to false); today only the internal admin test console and a manual test script pass true. What's still actually missing: (1) allow_agents needs to be exposed on the real create path before agent-join can target an actual event room, and (2) ZOE has no VPS-side consumer for the minted session_token, so our own auto-join hook (ZAO_AUTO_AGENT_JOIN) stays off regardless - flipping it before that exists would join + immediately drop the token. We want ZOE (Claude Opus) sitting silently and unattended in the weekly fractal + ZAOstock standups taking notes and posting a recap cast after room.finished; that needs both pieces, not anything further from Juke.","blocks":"Unattended ZOE-in-Juke (concierge note-taker + recap-cast generator)","priority":"p0","juke_resolved":{"id":"partner-agent-join","shipped_at":"2026-05-23","category":"developer-api","title":"Free agent-join scoped to your own rooms","summary":"POST /v1/developer/rooms/{room_id}/agent-join bypasses the x402 toll for the calling app's own rooms. Same response shape and session_token semantics as the public agent-join. Rate-limited 10/min + 100/day per key, with a per-room concurrent agent cap of 5.","endpoints":["POST /v1/developer/rooms/{room_id}/agent-join"],"docs":"https://juke.audio/llms.txt","docs_section":"Partner agents (free, scoped to your own rooms)","resolves":["agents"]}},{"id":"desktop-mic","title":"Desktop browser mic publish confirmation","reason":"When a desktop SIWF user is promoted, does the web SDK actually grant mic-publish, or does it fail silently? We do not want a \"Speak\" CTA on desktop that breaks under load. A \"yes works\" / \"iOS-only for now\" answer is enough to set the UI right.","blocks":"Honest desktop \"Speak from this browser\" CTA on /live/{id}","priority":"p1","juke_resolved":{"id":"desktop-mic-confirmed","shipped_at":"2026-05-23","category":"docs","title":"Confirmed: web SDK supports mic publish after host promotion","summary":"SIWF-authed visitors can become speakers and publish a mic track on web after host promotion — same code path as iOS. Not iOS-only.","endpoints":[],"docs":"https://juke.audio/llms.txt","docs_section":"Auth Ladder","resolves":["desktop-mic"]}},{"id":"webhook-delivery-log","title":"GET /v1/developer/webhooks/{id}/deliveries - delivery audit log","reason":"Nicky filed as issue #177 on 2026-05-25 (P1). We need per-delivery visibility (timestamp, event_type, status, retry count, last_error, body) so when our /api/juke/webhooks endpoint was down during a retry window we can see exactly what was dropped. Unblocks the webhook-replay endpoint (issue #181) - replay needs a delivery id to target.","blocks":"Detecting missed webhooks during downtime + queueing replays","priority":"p1"},{"id":"participant-role-changed","title":"participant.role_changed webhook event","reason":"Nicky filed as issue #183 on 2026-05-25 (P1). Fires when a host promotes a hand-raiser to speaker. Real social signal - we'd cast 'X just stepped up to speak in {title}' to /zao, driving organic discovery of who is contributing. Payload shape we want: { participant_fid, old_role, new_role, occurred_at }.","blocks":"Speaker-promotion recap casts + contributor discovery","priority":"p1"},{"id":"agent-visibility-flag","title":"Agent silent-observer flag (hide ZOE from iframe avatar bar)","reason":"Nicky filed as issue #190 on 2026-05-25 (P1). When ZOE joins as a partner-scoped agent for note-taking, we want her hidden from the iframe avatar bar / participant count to avoid the \"why is there a robot in the room\" UX surprise. Pairs with ZAO_AUTO_AGENT_JOIN going live - without this flag, every ZAO room would visibly grow a robot.","blocks":"Clean UX for ZOE-in-Juke once we flip ZAO_AUTO_AGENT_JOIN","priority":"p1"}],"conventions":["All Juke calls server-side. JUKE_API_KEY never leaves the server.","Webhook receiver is idempotent on signature_hash + caps replay window at 5 minutes.","juke_spaces is publicly readable (RLS allow-all); writes are service-role only.","/live/{spaceId} is fully public — no auth required to listen.","OG metadata pulls juke.audio/space/{id}/opengraph-image, so Juke renders the share card.","The only live-audio backend in this repo is Juke (provider id 'juke'). A full-A/V 'video room' backend ('hms') is registered as a reserved provider id but is an unimplemented stub (see src/lib/spaces/providers/hms.ts) - it does not run alongside Juke yet. 'songjam' is not a live-audio backend but is real and implemented: it's the provider id tagged on X/Twitter Spaces imported after the fact via POST /api/recordings/import-x (src/lib/spaces/xSpaces.ts), not a room Zuke ever hosts live. 'stream' remains a reserved id with no implementation at all."],"contact":{"zao_dev":"@zaal (Farcaster) / zaal@thezao.com","general":"https://zuke.thezao.com","partnership":"See /juke-status on zuke.thezao.com for the live build state."},"juke_release_feed":"https://juke.audio/changelog.json","stats":{"total_spaces":2,"active":0,"scheduled":0,"ended":2,"with_recording":0,"total_webhook_events":0,"recent_event_types":{},"last_event_at":null},"recent_spaces":[{"id":"eaf35c83-8b50-4280-a3b1-eca33f98b337","title":"test","status":"ended","participant_count":0,"scheduled_at":null,"started_at":"2026-07-21T16:39:03.796+00:00","ended_at":"2026-07-21T19:57:09.871+00:00","recording_url":null,"updated_at":"2026-07-21T19:57:10.645627+00:00"},{"id":"54bb5d24-7893-4d25-89cf-6623e2ed8078","title":"zuke-create-verify (safe to ignore, auto-ended)","status":"ended","participant_count":0,"scheduled_at":null,"started_at":"2026-07-21T15:19:10.66+00:00","ended_at":"2026-07-21T15:19:11.518+00:00","recording_url":null,"updated_at":"2026-07-21T15:19:11.576969+00:00"}],"recent_events":[],"release_feed":"https://juke.audio/changelog.json"}