Skip to main content
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.

Understanding the Agent

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.

Core Concepts

Session Management

Every agent is backed by a session manager that handles authentication state:
// Access the current session
const did = agent.sessionManager.did // 'did:plc:xyz...'

// Or use the convenience property
const userDid = agent.did // Same as above
const accountDid = agent.accountDid // Throws if not authenticated

Labelers Configuration

Labelers provide moderation labels for content. Configure which labelers the agent uses:
// Configure globally for all agents
Agent.configure({
  appLabelers: ['did:plc:your-labeler']
})

// Configure for a specific agent instance
agent.configureLabelers(['did:plc:another-labeler'])

Proxy Configuration

Route requests through a proxy service:
// Configure proxy for all subsequent requests
agent.configureProxy('did:plc:service#atproto_labeler')

// Or create a new agent instance with proxy
const proxiedAgent = agent.withProxy('atproto_labeler', 'did:plc:service')

Working with Posts

Creating Posts

Create posts using the post() method:
await agent.post({
  text: 'Hello, AT Protocol!',
  createdAt: new Date().toISOString()
})
The createdAt field is required and must be an ISO 8601 timestamp.

Posts with Rich Text

Use the RichText class to handle mentions, links, and other facets:
import { RichText } from '@atproto/api'

const rt = new RichText({
  text: 'Hello @alice.bsky.social, check out https://example.com!'
})
await rt.detectFacets(agent) // Resolves mentions and detects links

await agent.post({
  text: rt.text,
  facets: rt.facets,
  createdAt: new Date().toISOString()
})
See the Rich Text guide for more details.

Posts with Media

Upload images and embed them in posts:
// Upload image blob
const { data } = await agent.uploadBlob(imageFile, {
  encoding: 'image/jpeg'
})

// Create post with embedded image
await agent.post({
  text: 'Check out this image!',
  embed: {
    $type: 'app.bsky.embed.images',
    images: [
      {
        alt: 'A beautiful sunset',
        image: data.blob,
        aspectRatio: {
          width: 1000,
          height: 750
        }
      }
    ]
  },
  createdAt: new Date().toISOString()
})

Deleting Posts

Delete a post using its URI:
const postUri = 'at://did:plc:xyz/app.bsky.feed.post/abc123'
await agent.deletePost(postUri)

Reading Feeds and Posts

Getting the Timeline

Retrieve the authenticated user’s timeline:
const { data } = await agent.getTimeline({
  limit: 50,
  cursor: undefined // For pagination
})

for (const item of data.feed) {
  console.log(item.post.author.handle, ':', item.post.record.text)
}

// Use the cursor for pagination
if (data.cursor) {
  const { data: nextPage } = await agent.getTimeline({
    limit: 50,
    cursor: data.cursor
  })
}

Getting an Author’s Feed

Retrieve posts from a specific user:
const { data } = await agent.getAuthorFeed({
  actor: 'alice.bsky.social',
  limit: 30
})

Getting Post Threads

Retrieve a post and its replies:
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)

Getting Multiple Posts

Fetch multiple posts by URI:
const { data } = await agent.getPosts({
  uris: [
    'at://did:plc:alice/app.bsky.feed.post/123',
    'at://did:plc:bob/app.bsky.feed.post/456'
  ]
})

Social Graph Operations

Following Users

Follow a user:
const { uri } = await agent.follow('did:plc:alice')
console.log('Follow record URI:', uri)

Unfollowing Users

const followUri = 'at://did:plc:me/app.bsky.graph.follow/xyz'
await agent.deleteFollow(followUri)

Getting Followers

const { data } = await agent.getFollowers({
  actor: 'alice.bsky.social',
  limit: 100
})

for (const follower of data.followers) {
  console.log(follower.handle)
}

Getting Follows

const { data } = await agent.getFollows({
  actor: 'alice.bsky.social',
  limit: 100
})

for (const follow of data.follows) {
  console.log(follow.handle)
}

Engagement Actions

Liking Posts

const postUri = 'at://did:plc:alice/app.bsky.feed.post/123'
const postCid = 'bafyreiabc...' // Get from post data

const { uri } = await agent.like(postUri, postCid)
console.log('Like record URI:', uri)

Unliking Posts

const likeUri = 'at://did:plc:me/app.bsky.feed.like/xyz'
await agent.deleteLike(likeUri)

Reposting

const { uri } = await agent.repost(postUri, postCid)
console.log('Repost record URI:', uri)

Deleting Reposts

const repostUri = 'at://did:plc:me/app.bsky.feed.repost/xyz'
await agent.deleteRepost(repostUri)

Profile Management

Getting Profiles

Get a single profile:
const { data } = await agent.getProfile({
  actor: 'alice.bsky.social'
})

console.log(data.displayName)
console.log(data.description)
console.log(data.followersCount)
Get multiple profiles:
const { data } = await agent.getProfiles({
  actors: ['alice.bsky.social', 'bob.bsky.social']
})

Updating Your Profile

Use upsertProfile to safely update your profile:
await agent.upsertProfile((existing) => {
  return {
    displayName: 'Alice Smith',
    description: 'Software engineer and coffee enthusiast',
    avatar: existing?.avatar // Keep existing avatar
  }
})
The upsertProfile method:
  • Fetches the current profile
  • Passes it to your callback function
  • Creates or updates the profile record
  • Handles CAS (Compare-And-Swap) conflicts automatically
upsertProfile automatically retries on conflicts, but will fail after too many attempts to prevent infinite loops.

Notifications

Listing Notifications

const { data } = await agent.listNotifications({
  limit: 50
})

for (const notif of data.notifications) {
  console.log(notif.reason, ':', notif.author.handle)
}

Counting Unread Notifications

const { data } = await agent.countUnreadNotifications()
console.log('Unread count:', data.count)

Marking Notifications as Seen

await agent.updateSeenNotifications()

Searching for Users

const { data } = await agent.searchActors({
  q: 'alice',
  limit: 25
})

for (const actor of data.actors) {
  console.log(actor.handle, ':', actor.displayName)
}
For autocomplete functionality:
const { data } = await agent.searchActorsTypeahead({
  q: 'ali',
  limit: 10
})

Moderation

Muting Users

await agent.mute('did:plc:alice')

Unmuting Users

await agent.unmute('did:plc:alice')

Blocking Users

Blocking is done by creating a block record:
const { uri } = await agent.app.bsky.graph.block.create(
  { repo: agent.accountDid },
  {
    subject: 'did:plc:alice',
    createdAt: new Date().toISOString()
  }
)

Advanced Usage

Direct XRPC Calls

For methods not wrapped by convenience functions, use the namespaced API:
// Using the reverse-DNS style
const { data } = await agent.com.atproto.repo.listRecords({
  repo: agent.accountDid,
  collection: 'app.bsky.feed.post',
  limit: 100
})

// Or using the record-specific methods
const { data: post } = await agent.app.bsky.feed.post.get({
  repo: 'alice.bsky.social',
  rkey: 'abc123'
})

Custom Headers

Set custom headers for requests:
agent.setHeader('X-Custom-Header', 'value')

// Clear all custom headers
agent.clearHeaders()

Cloning Agents

Create a copy of an agent with the same configuration:
const agentCopy = agent.clone()

// Clones preserve labelers, proxy, and headers
agentCopy.labelers === agent.labelers // true

Error Handling

Handle errors from API calls:
import { XRPCError } from '@atproto/xrpc'

try {
  await agent.post({ text: 'Hello!' })
} catch (error) {
  if (error instanceof XRPCError) {
    console.error('XRPC Error:', error.status, error.error)
    
    // Check specific error types
    if (error.status === 401) {
      console.error('Authentication failed')
    }
  }
}

Best Practices

1

Use OAuth authentication

Always use OAuth-based sessions in production rather than app passwords for better security and token management.
2

Handle pagination

Use cursors properly when fetching large datasets to avoid missing items.
let cursor: string | undefined
const allPosts: Post[] = []

do {
  const { data } = await agent.getAuthorFeed({
    actor: 'alice.bsky.social',
    limit: 100,
    cursor
  })
  allPosts.push(...data.feed)
  cursor = data.cursor
} while (cursor)
3

Validate input

Validate user input before creating records, especially for rich text.
const rt = new RichText({ text: userInput })

if (rt.graphemeLength > 300) {
  throw new Error('Post is too long')
}
4

Handle rate limits

Implement exponential backoff when hitting rate limits.
import { retry } from '@atproto/common-web'

await retry(3, async () => {
  return await agent.post({ text: 'Hello!' })
})

Common Pitfalls

Forgetting createdAt: All records require a createdAt timestamp. Always include it:
// Bad
await agent.post({ text: 'Hello!' })

// Good
await agent.post({
  text: 'Hello!',
  createdAt: new Date().toISOString()
})
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 DID

await 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 change
await agent.follow('alice.bsky.social')

// Good - DID is permanent
await agent.follow('did:plc:abc123')

Next Steps

OAuth Authentication

Implement secure OAuth-based authentication

Rich Text

Work with mentions, links, and formatted text

Moderation

Implement content moderation in your app

API Reference

Explore the complete API reference

Build docs developers (and LLMs) love