Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt

Use this file to discover all available pages before exploring further.

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