Esto no es un sustituto para la «maravillosa» experiencia que resulta de leer la documentación oficial de ATProto y Bluesky, solamente quiero dejar anotado lo que he aprendido para poder revisarlo en el futuro
¿Cómo sabe Bluesky cuál es el algoritmo que proporciona la inteligencia de un feed?
Bueno, la respuesta parece ser que un feed de Bluesky no es más que un servicio HTTP con el que Bluesky interactúa usando la API RPC con los endpoints definidos en el protocolo ATProto.
Entonces, «crear» un feed es exponer a través de un servicio web esos endpoints y prepararlos para que Bluesky pueda lanzarle peticiones cuando una persona consulta el feed.
¿Y cómo sabe Bluesky cuál es el servicio web que debe usar?
Cuando «creas» un feed, es decir, cuando lo haces público para que salga en tu perfil y para que «exista» para Bluesky, hay que darle un parámetro que codifica el hostname. De modo que cuando alguien intenta consultar tu feed, Bluesky usa el hostname para saber a qué servidor hacer las peticiones HTTP que devuelven los datos del feed y así «usar tu algoritmo».
Ejemplo: el feed Linux está creado por @mackuba.eu. El DID de esta cuenta es did:plc:oio4hkxaop4ao4wz2pp3f4cr
. Si uso el endpoint RPC com.atproto.repo.listRecords
para pedirle todos los app.bsky.feed.generator
creados por esta cuenta, el array JSON incluye el feed Linux.

El campo did
de este record tiene como valor did:web:blue.mackuba.eu
. Eso significa que el feed está asociado con el hostname blue.mackuba.eu
y que cuando quieras consultar el feed Linux, Bluesky le tiene que tirar las peticiones a https://blue.mackuba.eu
.
La API de un feed
Leyendo la documentación del generador que he consultado, un servicio web que sirva feeds de Bluesky debe implementar tres métodos:
- /.well-known/did.json: este tiene que validar que, efectivamente, ese servicio web es el correcto, para evitar impersonaciones.
- /xrpc/app.bsky.feed.describeFeedGenerator: este es el que devuelve información de los feeds que se sirven desde ese servicio.
- /xrpc/app.bsky.feed.getFeedSkeleton: este es el que devuelve el contenido del feed, para que los posts se puedan ver. Va paginado.
ericvolp12/go-bsky-feed-generator es un generador de feeds hecho por @jaz.bsky.social que está programado en Go. De aquí es de donde he sacado esta información. Todavía no lo he clonado para ver si es tan sencillo como tomar la plantilla, programar los algoritmos, y dejar que el proyecto levante el servidor web.
did.json
El endpoint de DID parece confirmar que estamos en la ubicación correcta. Imagino que validar el feed quiere decir verificar que no estás usando otro hostname diferente o que alguien se ha equivocado al poner la URL del servidor. No tengo ni idea del contexto de este endpoint.

app.bsky.feed.describeFeedGenerator
El endpoint app.bsky.feed.describeFeedGenerator devuelve la lista de feeds servidos desde ese servicio web. La documentación con los tipos del objeto devuelto están en la API de Bluesky.

app.bsky.feed.getFeedSkeleton
El endpoint app.bsky.feed.getFeedSkeleton es el que devuelve los datos de un feed. Si le pido sin más que me hable, da un error HTTP 500, aunque supongo que esto depende del servidor.

Eso es porque según la documentación de ese endpoint, le tengo que poner como queryparam feed
para indicarle el record del feed que quiero que me devuelva. El feed tiene que estar en formato URL de protocolo ATProto, es decir, at://[did]/app.bsky.feed.generator/[slug]
, que para el feed «Linux» es at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.generator/linux
, siendo el DID de la cuenta de mackuba. En cuanto hago eso, el feed empieza a hablar.

El formato de un feed tiene el siguiente tipo (voy a usar una interfaz de TypeScript por simplicidad):
interface Feed {
cursor: string;
feed: Array<{
post: string;
}>;
}
Uno de los campos de retorno es cursor
, que es el que permite paginar el feed. Es un paginador un poco complejo porque como el feed se puede actualizar en tiempo real a medida que se va paginando si se agregan o se borran posts, se recomienda que lleve también un timestamp para asegurarse que se sincroniza bien.
El otro es feed
, que devuelve un array de objetos con los IDs de cada post. El feed no porta el contenido de los posts, solamente sus IDs externos. Tú devuelves la URL en protocolo ATProto de un post, y ya se ocupa Bluesky de recuperar por separado el contenido de cada post a partir de la propia API del PDS.
Voy a hacer la prueba tratando de resolver desde el PDS https://bsky.social uno de los posts del feed, en este caso, at://did:plc:gxt7mot2ujgovitv6j4eo7n4/app.bsky.feed.post/3lbyn6inlds22
. Puedo sacarlo mediante el endpoint RPC app.bsky.feed.getPosts
. Como este endpoint requiere autenticación, voy a usar https://public.api.bsky.app
, que permite acceso anónimo, para no tener que aprender ahora a crear tokens.

Estrategias para fabricar un algoritmo
Si quiero programar un feed estático, puedo devolver hardcodeado el array con las URLs ATProto de los posts que quiero que porte. Sin embargo, imagino que la gracia está en prestar atención a JetStream o al firehose de Bluesky directamente, que es el websocket que te trae en tiempo real los posts a medida que se publican. Para mirar a la cara a este websocket sin usar comandos de consola se pueden usar las siguientes herramientas web:
- https://tools.simonwillison.net/bluesky-firehose va agregando el JSON a un textarea gigante.
- https://firehose3d.theo.io/ es un visualizador que se volvió viral hace unas semanas por lo gráfico que es a la hora de mostrar lo que está escribiendo ahora mismo la gente.
Cuando encuentre un post en el websocket que concuerde con el criterio de mi algoritmo, puedo anotar su ID para poder devolverlo más adelante cuando se pidan datos del feed. Se recomienda también descartar los posts más antiguos para no devolver un feed muy grande.
Siguientes puntos
La siguiente pregunta que me queda por hacer es cómo funciona la autenticación, porque algunos feeds son anónimos ya que solo tienen que devolver datos del firehose público. Sin embargo, si quiero filtrar para que muestre información relacionada con mi cuenta (por ejemplo, posts de la gente que sigo o posts de la gente que me sigue), supongo que el feed necesita una forma de saber quién soy. Esto es algo que dejo para investigar otro día.