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).
accounts are discovered straight from the atproto firehose via jetstream — anyone who creates a profile, posts, likes, or follows is picked up automatically, across bluesky and other atproto apps. accounts that predate the index or haven't posted yet can be surfaced on demand (see request indexing below).
every search is served entirely from our own data: a precomputed prefix-index snapshot merged with a live overlay for recent renames, deletes, moderation changes, and brand-new accounts, then edge-cached for 60s. no request does work proportional to the full corpus (millions of actors), and bluesky is never in the request path — it's a rollback-only kill switch, not a fallback.
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)if a result looks surprising, use the ranking report. it shows the stored ranking score, the score if recalculated now, percentile among visible scored actors, and a few example query positions.
ranking score and visibility are separate. follower/post/age signals affect the score; fresh accounts are penalized while very new. moderation labels, domain blocks, and overrides decide whether an actor can appear at all.
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.