DIY Bluesky Bot

Difficulty Level:

this is relatively easy as developer things go, I myself am not truly a dev, but up front this tutorial will reference things like github repos and working with / modifying files of code. you shouldn't actually have to know or write any code, but just being familiar with this whole world is very helpful. and, worst case: your friendly LLM will likely know anything you need.

probably also worth mentioning, this entire thing was written by claude. I just asked for it and went through the pain of troubleshooting the little things that went wrong, but I have no idea if it's like, optimal or perfect or whatever.

Cost:

mostly free!

I say mostly because the one unavoidable cost is the claude API tokens to process the responses themselves. it's pretty cheap - each tweet is less than a cent - so I threw $10 in my account and it'll run for a thousand+ tweets, which you can schedule at whatever frequency you want. this is, at least for me, probably longer than my attention span in running a bot will be anyway.

I have made some choices for the sake of cheapness that you might opt to re-write to upgrade if you want. namely: I'm using the free tier of netlify which doesn't have all the same customization over cron jobs and other timing-related server processing. you'll see how I've gotten around this later, but just know you _can_ upgrade (or perhaps are already paying for) a higher service level.

Here's the Code

there's only four parts, really:

the folder structure looks like this:


        your-repo/
        ├── netlify.toml
        ├── package.json
        └── netlify/
            └── functions/
                ├── bot/
                │   ├── bot.js      (our beloved poet!)
                │   └── prompt.js   (its creative soul!)
    

netlify.toml


                [build]
                command = "npm install"
                functions = "netlify/functions"

                [functions]
                node_bundler = "esbuild"

                [[redirects]]
                from = "/bot-ping"
                to = "/.netlify/functions/bot"
                status = 200
    

package.json


            {
                "name": "bluesky-claude-bot",
                "type": "module",
                "version": "1.0.0",
                "dependencies": {
                  "@anthropic-ai/sdk": "^0.14.1",
                  "@atproto/api": "^0.12.2"
                }
              }
        

prompt.js


            export const getPrompt = () => `you are a brilliant if slightly chaotic twitter personality who loves to talk about cheese and cheese-related puns.

            write exclusively in rhyming haiku.

            keep it under 140 chars, sound natural, mix it up. don't be boring. don't be basic or too linkedin. no emojis.

            don't include anything meta about presenting or delivering the comment, just speak in the form of the final tweet`;
        

if you're familiar with system prompts generally, this should be easy to modify to your desires. it's programming in plain english, you basically just tell it what you want it to do, what to sound like, how to write. I've updated and tweaked mine a dozen times and it'll probably always be ongoing.

the one thing with LLMs is that writing is thinking, and so having them take a prompt and spit out only a short tweet with no other meta writing beforehand is actually pretty weak processing, since it's just starting somewhere and spitting out the rest without really backtracking or working through it. so although the model might be good, just assume that it'll be a little bit dumb in this format. I might make a more elaborate system in the future that lets it compose a tweet and then use a signal to 'commit' to the final words being the real words sent to the actual bluesky API. but later.

bot.js


            import { BskyAgent } from '@atproto/api';
            import Anthropic from '@anthropic-ai/sdk';
            import { getPrompt } from './prompt.js';
            
            export const handler = async (event) => {
              try {
                const claude = new Anthropic({
                  apiKey: process.env.ANTHROPIC_API_KEY,
                });
            
                const completion = await claude.messages.create({
                  model: "claude-3-5-sonnet-latest",
                  max_tokens: 150,
                  temperature: 0.92,
                  messages: [{ role: "user", content: getPrompt() }],
                });
            
                const post = completion.content[0].text.trim().slice(0, 200);
            
                const agent = new BskyAgent({
                  service: 'https://bsky.social',
                });
            
                await agent.login({
                  identifier: process.env.BLUESKY_IDENTIFIER,
                  password: process.env.BLUESKY_APP_PASSWORD,
                });
            
                await agent.post({ text: post });
                
                return {
                  statusCode: 200,
                  body: JSON.stringify({
                    message: 'Deployed insights to the void',
                    post,
                    time: new Date().toISOString()
                  })
                };
              } catch (error) {
                console.error('Error:', error);
                return {
                  statusCode: 500,
                  body: JSON.stringify({
                    error: 'Encountered unexpected recursion in the void',
                    details: error.message
                  })
                };
              }
            };        
        

and that's it! copy and paste these into their respective document names, into their folder structure as above, and you're mostly there.

How to Put it All Together

you'll need:

github

I'm going to hurry past the github specifics and just assume that you've already done that if you've gotten this far.

bluesky account

I'm also going to hurry through the bluesky account making. for one thing, you've definitely done it before. the one note here is to make an app password which is different than your regular password. technically, you know, you can trust yourself because you're making both the bot and the account, but it's a good practice to use app passwords for anything anyway.

claude API

as the note mentions: anthropic has claude the frontend that you talk to with the messenger interface, and then also claude the API based tool that we can call from servers. we're wanting the latter and it's a different part of the website to buy and pay and everything, it's not really related to your other subscription at all (and you don't need a subscription to buy credits for this side). sign up, pay them whatever amount of money you want and get an API key. personally I set mine to not auto-buy more credits just in case everything goes wrong and starts shredding through them for some reason. at worst they'll just use up my $10 and then stop. there shouldn't be any problems, but, like...

netlify

netlify is pretty straightforward. if you're unfamiliar it's a service that takes github repos and runs them such that they're live on the internet. this is how my portfolio site is powered, and generally how I host everything these days. even this very tutorial is just an HTML doc sitting in a repo that you can access through a URL! and it's free, so great.

following the little wizard it puts you through you'll name the app and it'll make a NAME.netlify.app/.netlify/functions/bot URL which is the home of your bot. it's a website, sorta? in our case it's a function which runs the process itself, and if everything is working later it'll show you the tweet that was just written. if not, you'll see an error code here.

but the main thing is that this URL will be a method for manually generating tweets. every time you go to it, it'll run. and if you refresh it'll generate a new one. in a way the bot is only sort of "alive" when it's being called by something, and that URL is the call.

in netlify there's a section called Environment Variables (and it also asks you for these optionally in the initial repo-adding setup phase) and we want to make three:

hopefully it seems pretty clear what goes where given those variable names.

but the cool thing here is that we can store the important information inside netlify and not directly in the repo for the world to see. when it runs it grabs these variables and puts them in the right place, and then we don't have to store the secret strings in the code itself.

and that's pretty much it. you'll see it build and deploy any given update when you push something to the repo, so for example I'll tweak the prompt and bit and push and it takes a minute to discover the change, start a deploy, build, and finish, and then you can go to the URL and 'see' your changes.

secret fifth thing

in an effort to stay in the netlify free tier, I've opted to not use cron jobs to schedule tweets, but instead to ping the URL from an external tool.

it just so happens that there's tools that'll do this for free!

uptimerobot.com is an uptime bot where you can specify how frequently it checks to see if a website is online. normally people do this to make sure their website isn't down, but we know it won't (shouldn't???) be down, we just want that ping to check because that's what wakes the bot and has it run.

so make another free account, add your app.netlify URL and set it to check every, say, three hours. this is your new posting schedule.

the sad part is, as mentioned before, you don't get the same customization. there's a world where we code some randomness in like, "every time the URL gets pinged, roll a dice and only ping the API to generate a tweet if X" and then you set uptimerobot to ping every hour, but knowing that 2/3 times it won't fire anyway, etc. maybe you have some logic for 'wake hours' vs 'sleep hours' or whatever.

for me, personally, I don't mind the steady onslaught of tweets at scheduled times. they'll appear in my feed and I'll read them when I read them. big deal.

The End

hopefully at this point it's working?

I'm writing this off the top of my head, so it's possible I've missed something entirely.

with love, brennan.computer