switching from the bluesky typeahead API
public.api.bsky.app with typeahead.waow.tech.
typeahead is a community-run actor search for atproto,
aiming to be a drop-in replacement for bluesky's
app.bsky.actor.searchActorsTypeahead endpoint. the endpoint path and query
params are identical; the response shape is compatible but slimmer (see
response comparison below).
the index is built from a few jetstream collections — profiles, posts, likes, and follows — so any account that creates or interacts with content gets discovered automatically. for accounts that predate the index or haven't been seen yet, search queries trigger a throttled backfill from the bluesky API to fill gaps on demand. searches use FTS5 prefix matching against handles and display names, with results edge-cached for 60s.
replace the base URL. everything else stays the same:
- https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=...&limit=10
+ https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead?q=...&limit=10
const response = await fetch(
`https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`
);
const TYPEAHEAD_URL = 'https://typeahead.waow.tech';
const response = await fetch(
`${TYPEAHEAD_URL}/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`
);
extracting the base URL into a constant (or env var) makes it easy to switch back if you ever need to.
set the X-Client header so your app shows up by name in our
traffic stats instead of as "unknown":
const response = await fetch(
`${TYPEAHEAD_URL}/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`,
{ headers: { 'X-Client': 'my-app.example.com' } }
);
use your domain or app name — whatever you want to be identified as.
browser-based apps will also be identified automatically via the Origin header,
but X-Client is preferred since it works everywhere (server-side, CLI, native apps).
both return { "actors": [...] }. the actor objects differ:
| field | bluesky | typeahead |
|---|---|---|
did | ✓ | ✓ |
handle | ✓ | ✓ |
displayName | ✓ | ✓ |
avatar | ✓ | ✓ |
associated | ✓ | ✓ |
labels | ✓ | ✓ |
createdAt | ✓ | ✓ |
viewer | ✓ | — |
viewer — this API doesn't return it
(it requires authentication). we return did + handle + displayName + avatar +
associated + labels + createdAt, which covers the full profileViewBasic
surface minus viewer.
1–100 (bluesky defaults to 10)
atproto is a network, not a single app. bluesky PBC runs the largest appview and applies its
own moderation labels — !hide, !takedown, !suspend,
spam — which hide actors from its search results. we respect these labels by
default: actors flagged by bluesky's moderation service
(did:plc:ar7c4by46qjdydhdevvrndac) are excluded from search.
however, not every account banned by bluesky PBC is harmful. some were suspended for policy disputes, handle issues, or reasons unrelated to abuse. because atproto is designed for credible exit, we think these people should still be findable — identity is not content moderation.
to support this, the index enriches banned/suspended accounts via their PDS directly
(using com.atproto.repo.getRecord), so profiles are discoverable even when
bluesky's API refuses to serve them. these accounts start hidden and can be explicitly
un-hidden via an admin override on a case-by-case basis.
handles containing explicit slurs are always filtered, regardless of override status. these regexes are ported from bluesky's own PDS and catch Unicode diacritics, Cyrillic substitutions, and leetspeak.
plyr.fm uses typeahead for actor search. the integration looks roughly like:
// config.ts
export const TYPEAHEAD_URL = 'https://typeahead.waow.tech';
// HandleSearch.svelte
const response = await fetch(
`${TYPEAHEAD_URL}/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`
);
const data = await response.json();
const actors = (data.actors ?? []).map(actor => ({
did: actor.did,
handle: actor.handle,
display_name: actor.displayName ?? actor.handle,
avatar_url: actor.avatar ?? null,
}));
if someone isn't showing up in results, you (or your users) can request indexing from the homepage. newly created accounts are picked up automatically via jetstream, but accounts created before the index existed may need a manual nudge.