···11+# Activity Overview
22+33+## What We Built
44+55+### 1. Basic Posting (01a-hello-world.js)
66+- Connects to Bluesky using app password
77+- Posts a simple message with #ATProtoChicago
88+- Shows the basics of authentication and posting
99+1010+### 2. Creative Posts (01b-emoji-art.js)
1111+- Generates random emoji art grids
1212+- Demonstrates creative uses of the posting API
1313+- Fun visual output that's shareable
1414+1515+### 3. Rich Text Features (02-rich-text.js)
1616+- Auto-detects links and converts them to clickable links
1717+- Auto-detects hashtags for discoverability
1818+- Shows how Bluesky enhances plain text
1919+2020+### 4. Real-time Firehose (03-feed-generator.js)
2121+- Connects to Bluesky's Jetstream service
2222+- Filters the entire network for #ATProtoChicago posts
2323+- Shows posts in real-time with clean JSON format
2424+- Demonstrates how feed generators work
2525+2626+### 5. Timeline Filtering (05-post-filter.js)
2727+- Fetches your home timeline
2828+- Filters for Chicago/ATProto mentions
2929+- Shows engagement metrics
3030+- Local processing example
3131+3232+### 6. Profile Updates (04-profile-updater.js)
3333+- Programmatically updates your profile
3434+- Adds timestamp to description
3535+- Shows how to modify account data
3636+3737+## Key Concepts Demonstrated
3838+3939+- **Authentication**: Using app passwords safely
4040+- **CRUD Operations**: Create posts, read timeline, update profile
4141+- **Real-time Data**: Consuming the firehose
4242+- **Data Processing**: Filtering and analyzing posts
4343+- **Rich Content**: Links, mentions, hashtags
4444+- **API Patterns**: Consistent error handling and async/await
4545+4646+## Architecture Notes
4747+4848+- All examples use the official `@atproto/api` SDK
4949+- Jetstream provides JSON instead of binary CAR/CBOR
5050+- Credentials stored in `.env` for security
5151+- Bun runtime for fast execution and built-in TypeScript support
+26-5
README.md
···771. **Create a Bluesky account** at https://bsky.app
882. **Generate an app password**: Settings → App Passwords → Add
993. **Install Bun** (if needed): `curl -fsSL https://bun.sh/install | bash`
1010-4. **Clone this repo** and create `.env` file:
1010+4. **Clone this repo** and install dependencies:
1111+ ```bash
1212+ git clone <this-repo>
1313+ cd atproto-meetup
1414+ bun install
1515+ ```
1616+5. **Create `.env` file** with your credentials:
1117 ```
1218 BSKY_USERNAME=your-handle.bsky.social
1319 BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx
···3339```
3440Demonstrates auto-detection of links and hashtags in posts.
35413636-### Activity 3: Feed Generator Metadata (15 minutes)
4242+### Activity 3: Real-time Firehose with Jetstream (15 minutes)
3743```bash
3844bun activities/03-feed-generator.js
3945```
4040-Creates metadata for a custom feed (note: actual feed requires a server).
4646+Connect to Bluesky's Jetstream service to see real-time posts with #ATProtoChicago. Uses simple JSON instead of complex binary formats!
4747+4848+### Activity 4: Local Post Filter (15 minutes)
4949+```bash
5050+bun activities/05-post-filter.js
5151+```
5252+Build a local feed algorithm that filters your timeline for Chicago/ATProto posts.
41534242-### Activity 4: Profile Updater (10 minutes)
5454+### Activity 5: Profile Updater (10 minutes)
4355```bash
4456bun activities/04-profile-updater.js
4557```
···6072- Use `#ATProtoChicago` to find other meetup participants!
61736274## Common Issues
7575+7676+- **"Invalid identifier or password"**: Make sure you're using an app password, not your main password
6377- **Rate limits**: The API has rate limits, wait a minute if you hit them
6464-- **Module not found**: Run the scripts from the repo root directory7878+- **Module not found**: Run the scripts from the repo root directory
7979+- **WebSocket errors**: The firehose can be flaky, just restart if it disconnects
8080+8181+## What's Next?
8282+8383+- Build a full feed generator: https://github.com/bluesky-social/feed-generator
8484+- Explore the AT Protocol specs: https://atproto.com/specs
8585+- Join the Bluesky developer community: https://github.com/bluesky-social/atproto/discussions
+3-3
activities/01a-hello-world.js
···11#!/usr/bin/env bun
22-import { BskyAgent } from '@atproto/api'
22+import { AtpAgent } from '@atproto/api'
3344// Read credentials from .env file
55const username = process.env.BSKY_USERNAME
···1010 process.exit(1)
1111}
12121313-const agent = new BskyAgent({ service: 'https://bsky.social' })
1313+const agent = new AtpAgent({ service: 'https://bsky.social' })
14141515console.log('Logging in...')
1616await agent.login({
···2121console.log('Connected! Your DID:', agent.session?.did)
22222323const post = await agent.post({
2424- text: 'Hello from the Chicago ATProto meetup! 🚀'
2424+ text: 'Hello from the #ATProtoChicago meetup! 🚀'
2525})
26262727console.log('Posted successfully!')
+2-2
activities/01b-emoji-art.js
···11#!/usr/bin/env bun
22-import { BskyAgent } from '@atproto/api'
22+import { AtpAgent } from '@atproto/api'
3344// Read credentials from .env
55const username = process.env.BSKY_USERNAME
···1010 process.exit(1)
1111}
12121313-const agent = new BskyAgent({ service: 'https://bsky.social' })
1313+const agent = new AtpAgent({ service: 'https://bsky.social' })
14141515console.log('Logging in...')
1616await agent.login({
+2-2
activities/02-rich-text.js
···11#!/usr/bin/env bun
22-import { BskyAgent, RichText } from '@atproto/api'
22+import { AtpAgent, RichText } from '@atproto/api'
3344// Read credentials from .env
55const username = process.env.BSKY_USERNAME
···1010 process.exit(1)
1111}
12121313-const agent = new BskyAgent({ service: 'https://bsky.social' })
1313+const agent = new AtpAgent({ service: 'https://bsky.social' })
14141515console.log('Logging in...')
1616await agent.login({
+110-45
activities/03-feed-generator.js
···11#!/usr/bin/env bun
22-import { BskyAgent } from '@atproto/api'
22+import { AtpAgent } from '@atproto/api'
33+import { WebSocket } from 'ws'
3445// Read credentials from .env
56const username = process.env.BSKY_USERNAME
···1011 process.exit(1)
1112}
12131313-const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+const agent = new AtpAgent({ service: 'https://bsky.social' })
14151516console.log('Logging in...')
1617await agent.login({
···20212122console.log('Connected! Your DID:', agent.session?.did)
22232323-// Create a simple feed generator record
2424-// Note: This creates the metadata for a feed, but you'd need a server to actually generate the feed content
2525-const feedRecord = {
2626- did: `did:plc:${agent.session.did.split(':')[2]}`, // Use your actual DID
2727- displayName: 'Chicago ATProto Meetup',
2828- description: 'Posts from our Chicago ATProto meetup! Tag your posts with #ATProtoChicago',
2929- avatar: undefined,
3030- createdAt: new Date().toISOString()
3131-}
2424+// Store found posts
2525+let postCount = 0
2626+const foundPosts = []
2727+2828+console.log('\n🚀 Starting Feed Generator with Jetstream')
2929+console.log('==========================================')
3030+console.log('This connects to Bluesky\'s Jetstream service which provides:')
3131+console.log('- Simple JSON format (not binary CAR/CBOR)')
3232+console.log('- Filtered streams (just posts, not all events)')
3333+console.log('- Clean, readable data')
3434+console.log('\nLooking for #ATProtoChicago posts...')
3535+console.log('Press Ctrl+C to stop\n')
3636+3737+// Connect to Jetstream - only get posts
3838+const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post')
32393333-const rkey = 'chicago-meetup-feed'
4040+ws.on('open', () => {
4141+ console.log('✅ Connected to Jetstream!')
4242+ console.log('Listening for posts...\n')
4343+ console.log('═'.repeat(60))
4444+})
34453535-console.log('\nCreating feed generator record...')
3636-console.log('Feed details:', JSON.stringify(feedRecord, null, 2))
4646+ws.on('message', async (data) => {
4747+ try {
4848+ const event = JSON.parse(data.toString())
4949+5050+ // Check if this is a post event
5151+ if (event.commit && event.commit.record && event.commit.record.text) {
5252+ const text = event.commit.record.text.toLowerCase()
5353+5454+ // Check for our hashtag
5555+ if (text.includes('#atprotochicago') || (text.includes('atproto') && text.includes('chicago'))) {
5656+ postCount++
5757+ const timestamp = new Date().toLocaleTimeString()
5858+5959+ console.log(`\n🎯 [${timestamp}] Found post #${postCount} with #ATProtoChicago!`)
6060+ console.log('─'.repeat(60))
6161+6262+ // Get author info from the event
6363+ const did = event.did
6464+ let authorHandle = did
6565+6666+ try {
6767+ const profile = await agent.getProfile({ actor: did })
6868+ authorHandle = profile.data.handle
6969+ if (profile.data.displayName) {
7070+ authorHandle = `${profile.data.displayName} (@${profile.data.handle})`
7171+ }
7272+ } catch {
7373+ // If profile lookup fails, just show DID
7474+ authorHandle = did.substring(0, 20) + '...'
7575+ }
7676+7777+ console.log(`👤 Author: ${authorHandle}`)
7878+ console.log(`📝 Post:`)
7979+ console.log('')
8080+8181+ // Display the post text
8282+ const lines = event.commit.record.text.split('\n')
8383+ lines.forEach(line => {
8484+ console.log(` ${line}`)
8585+ })
8686+8787+ // Show metadata
8888+ console.log('')
8989+ console.log(`🔗 URI: at://${did}/app.bsky.feed.post/${event.commit.rkey}`)
9090+ console.log(`🕐 Created: ${new Date(event.commit.record.createdAt).toLocaleString()}`)
9191+9292+ // Show engagement if available
9393+ if (event.commit.record.reply) {
9494+ console.log(`💬 This is a reply`)
9595+ }
9696+9797+ console.log('─'.repeat(60))
9898+ console.log(`\n📊 Total posts found: ${postCount}`)
9999+ console.log('═'.repeat(60))
100100+101101+ foundPosts.push({
102102+ timestamp: new Date(),
103103+ author: authorHandle,
104104+ text: event.commit.record.text,
105105+ uri: `at://${did}/app.bsky.feed.post/${event.commit.rkey}`,
106106+ did: did
107107+ })
108108+ }
109109+ }
110110+ } catch (error) {
111111+ // Ignore parsing errors
112112+ }
113113+})
371143838-try {
3939- // First check if it already exists
4040- const existing = await agent.com.atproto.repo.getRecord({
4141- repo: agent.session.did,
4242- collection: 'app.bsky.feed.generator',
4343- rkey: rkey
4444- }).catch(() => null)
115115+ws.on('error', (error) => {
116116+ console.error('\n❌ WebSocket error:', error.message)
117117+})
451184646- if (existing) {
4747- console.log('\nFeed already exists! Updating...')
4848- await agent.com.atproto.repo.putRecord({
4949- repo: agent.session.did,
5050- collection: 'app.bsky.feed.generator',
5151- rkey: rkey,
5252- record: feedRecord,
5353- swapRecord: existing.data.cid
5454- })
5555- } else {
5656- await agent.com.atproto.repo.putRecord({
5757- repo: agent.session.did,
5858- collection: 'app.bsky.feed.generator',
5959- rkey: rkey,
6060- record: feedRecord
119119+ws.on('close', () => {
120120+ console.log('\n\n👋 Disconnected from Jetstream')
121121+ console.log(`Found ${postCount} posts with #ATProtoChicago`)
122122+123123+ if (foundPosts.length > 0) {
124124+ console.log('\n📋 Summary of posts found:')
125125+ console.log('─'.repeat(60))
126126+ foundPosts.forEach((post, i) => {
127127+ console.log(`${i + 1}. ${post.author}: "${post.text.substring(0, 50)}${post.text.length > 50 ? '...' : ''}"`)
61128 })
62129 }
130130+})
631316464- console.log('\nFeed generator record created successfully!')
6565- console.log(`\nNote: This creates the feed metadata. To make it functional, you would need:`)
6666- console.log('1. A server running the feed generation logic')
6767- console.log('2. The feed to be published and indexed by Bluesky')
6868- console.log(`\nYour feed URI: at://${agent.session.did}/app.bsky.feed.generator/${rkey}`)
6969-7070-} catch (error) {
7171- console.error('Error creating feed:', error.message)
7272-}132132+// Graceful shutdown
133133+process.on('SIGINT', () => {
134134+ console.log('\n\nShutting down gracefully...')
135135+ ws.close()
136136+ process.exit(0)
137137+})
+2-2
activities/04-profile-updater.js
···11#!/usr/bin/env bun
22-import { BskyAgent } from '@atproto/api'
22+import { AtpAgent } from '@atproto/api'
3344// Read credentials from .env
55const username = process.env.BSKY_USERNAME
···1010 process.exit(1)
1111}
12121313-const agent = new BskyAgent({ service: 'https://bsky.social' })
1313+const agent = new AtpAgent({ service: 'https://bsky.social' })
14141515console.log('Logging in...')
1616await agent.login({