Complete guide to using the AT Protocol Agent API for building social applications
The Agent API is the primary interface for interacting with AT Protocol servers. It provides a comprehensive set of methods for creating posts, managing profiles, following users, and much more.
The Agent class extends the XRPC client with AT Protocol-specific functionality and Bluesky syntactic sugar. It handles authentication, request signing, and provides convenient methods for common operations.
import { Agent } from '@atproto/api'const agent = new Agent(session)
The agent requires a session manager, which can be:
A CredentialSession for app password authentication (deprecated)
An OAuthSession from the OAuth client (recommended)
A custom SessionManager implementation
App password authentication is deprecated. Use OAuth-based authentication for production applications. See the OAuth authentication guide.
Every agent is backed by a session manager that handles authentication state:
// Access the current sessionconst did = agent.sessionManager.did // 'did:plc:xyz...'// Or use the convenience propertyconst userDid = agent.did // Same as aboveconst accountDid = agent.accountDid // Throws if not authenticated
Labelers provide moderation labels for content. Configure which labelers the agent uses:
// Configure globally for all agentsAgent.configure({ appLabelers: ['did:plc:your-labeler']})// Configure for a specific agent instanceagent.configureLabelers(['did:plc:another-labeler'])
// Configure proxy for all subsequent requestsagent.configureProxy('did:plc:service#atproto_labeler')// Or create a new agent instance with proxyconst proxiedAgent = agent.withProxy('atproto_labeler', 'did:plc:service')
const { data } = await agent.getPostThread({ uri: 'at://did:plc:xyz/app.bsky.feed.post/abc123', depth: 10, // How deep to fetch replies parentHeight: 10 // How far up to fetch parent posts})console.log(data.thread.post.record.text)
const postUri = 'at://did:plc:alice/app.bsky.feed.post/123'const postCid = 'bafyreiabc...' // Get from post dataconst { uri } = await agent.like(postUri, postCid)console.log('Like record URI:', uri)
For methods not wrapped by convenience functions, use the namespaced API:
// Using the reverse-DNS styleconst { data } = await agent.com.atproto.repo.listRecords({ repo: agent.accountDid, collection: 'app.bsky.feed.post', limit: 100})// Or using the record-specific methodsconst { data: post } = await agent.app.bsky.feed.post.get({ repo: 'alice.bsky.social', rkey: 'abc123'})
Not handling mentions properly: Always call detectFacets() to resolve mentions to DIDs:
const rt = new RichText({ text: 'Hello @alice.bsky.social!' })await rt.detectFacets(agent) // This resolves @alice.bsky.social to a DIDawait agent.post({ text: rt.text, facets: rt.facets, createdAt: new Date().toISOString()})
Using handles instead of DIDs: For persistent references, always use DIDs, not handles (handles can change):
// Bad - handle might changeawait agent.follow('alice.bsky.social')// Good - DID is permanentawait agent.follow('did:plc:abc123')