Bluesky profile update automation

Screenshot showing my bsky profile with automated profile content

Automating Your Bluesky Profile Updates with Next.js

If you're an active Bluesky user, you might have noticed profiles that automatically update with information like the current date, day of the year, or year progress percentage. In this post, I'll walk you through creating your own automated Bluesky profile updater using Next.js and deploying it to Vercel.

The complete code is available in this GitHub Gist.

What We're Building

Our goal is to create an API endpoint that:

  1. Logs into your Bluesky account
  2. Calculates up-to-date information (day of year, percentage of year complete, etc.)
  3. Updates your profile description with this information
  4. Optionally updates your avatar and banner images
  5. Can be triggered by a scheduled cron job

Project Setup

Create a Next.js API route by placing the code in the following location:

/app/api/update-profile/route.ts

This will make your endpoint available at https://your-domain.com/api/update-profile once deployed.

Breaking Down the Code

Let's walk through the key components of our profile updater.

Setup and Imports

We start by importing the necessary dependencies and initializing our Bluesky agent:

export const dynamic = 'force-dynamic' // Ensures the route is not cached

import { NextResponse } from 'next/server'
import { BskyAgent } from '@atproto/api'
import { retry } from '@/utils/retry'

// Initialize the Bluesky agent
const agent = new BskyAgent({ service: 'https://bsky.social' })

The dynamic = 'force-dynamic' directive ensures our route isn't cached, which is important for an API endpoint that provides real-time updates.

Image Upload Helper

To update profile and banner images, we need a function to handle image uploads:

async function uploadImage(imageUrl: string) {
  try {
    // Fetch the image from the provided URL
    const response = await fetch(imageUrl);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    // Convert the image to a format Bluesky can accept
    const arrayBuffer = await response.arrayBuffer();
    const file = new Uint8Array(arrayBuffer);

    // Upload the image to Bluesky
    const upload = await agent.uploadBlob(file, {
      encoding: 'image/jpeg',
    });

    return upload.data.blob;
  } catch (error) {
    console.error('Error uploading image:', error);
    return null;
  }
}

This function fetches an image from a URL, converts it to a format Bluesky can accept, and uploads it using the Bluesky API.

Main API Handler

Our GET handler performs the core functionality:

export async function GET(request: Request) {
  console.log('Profile update process started')
  try {
    // Verify environment variables are set
    if (!process.env.BLUESKY_USERNAME || !process.env.BLUESKY_PASSWORD) {
      throw new Error('Bluesky credentials not configured')
    }

    // Log in to Bluesky with retry for reliability
    await retry(() => agent.login({
      identifier: process.env.BLUESKY_USERNAME!,
      password: process.env.BLUESKY_PASSWORD!,
    }))
    
    // ... rest of the code
  }
}

Notice that we're using environment variables for the Bluesky credentials, which is a security best practice. You'll need to set these in your Vercel project settings.

Calculating Time-Based Information

A key feature of our updater is calculating various time-based metrics:

// Get current date information
const today = new Date()
const dayOfWeek = today.toLocaleString('en-US', { weekday: 'long' })
const date = today.toISOString().split('T')[0] // YYYY-MM-DD format
const hours = today.getHours().toString().padStart(2, '0')
const minutes = today.getMinutes().toString().padStart(2, '0')
const time = `${hours}:${minutes}`

// Calculate percentage of year completed
const startOfYear = new Date(today.getFullYear(), 0, 1) // January 1st of current year
const millisecondsInYear = new Date(today.getFullYear() + 1, 0, 1).getTime() - startOfYear.getTime()
const millisecondsSoFar = today.getTime() - startOfYear.getTime()
const percentComplete = ((millisecondsSoFar / millisecondsInYear) * 100).toFixed(1)

// Calculate day of year
const dayOfYear = Math.floor((today.getTime() - startOfYear.getTime()) / (1000 * 60 * 60 * 24)) + 1

These calculations provide the current date, time, day of year, and percentage of the year completed.

Building the Profile Description

Next, we combine our static profile text with the dynamic time information:

// Core profile text that remains constant
const coreDescription = `Place your core profile text here.`

// Combine all information into the full profile description
const fullDescription = `${coreDescription}

Day ${dayOfYear} of 365.
${percentComplete}% through ${today.getFullYear()}. 
${dayOfWeek}, ${date} ${time}`

Updating the Profile

With all our data prepared, we can update the profile:

// Update the profile with all information
const updateResult = await retry(() => agent.com.atproto.repo.putRecord({
  repo: did,
  collection: 'app.bsky.actor.profile',
  rkey: 'self',
  record: {
    $type: 'app.bsky.actor.profile',
    displayName: "Your Name", // Replace with your actual name
    description: fullDescription,
    avatar: avatarBlob,
    banner: bannerBlob,
  },
}))

Notice we're using the retry utility to ensure reliability. This helps handle temporary network issues or API rate limits.

Error Handling and Response

We've implemented comprehensive error handling and proper HTTP responses:

// Return success response with cache headers to prevent caching
console.log('Profile updated successfully')
const response = NextResponse.json({ 
  success: true, 
  message: `Profile updated for ${dayOfWeek}, ${date} ${time}` 
})

// Set cache control headers to prevent caching
setCacheControlHeaders(response)
return response

For errors, we provide detailed information to help with debugging:

// Handle and log any errors
console.error('Error updating profile:', error)
let errorMessage = error.message || 'Failed to update profile'
let errorDetails = null

// Include error status if available
if (error.status) {
  errorMessage += ` (Status: ${error.status})`
}

// Include response data if available
if (error.response) {
  try {
    errorDetails = JSON.stringify(error.response.data, null, 2)
  } catch {
    errorDetails = 'Unable to parse error response data'
  }
}

// Return error response
const response = NextResponse.json(
  { 
    success: false, 
    error: errorMessage,
    details: errorDetails,
    stack: error.stack
  }, 
  { status: 500 }
)

Retry Utility

Our code uses a retry utility to handle temporary failures. Create this in /utils/retry.ts:

export async function retry<T>(fn: () => Promise<T>, maxAttempts = 3, delay = 1000): Promise<T> {
  let lastError: any;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      console.log(`Attempt ${attempt} failed. ${attempt < maxAttempts ? 'Retrying...' : 'No more retries.'}`);
      lastError= error;
      
      if (attempt < maxAttempts) {
        await new Promise(resolve=> setTimeout(resolve, delay));
      }
    }
  }
  
  throw lastError;
}

This utility will retry failed operations up to a specified number of times with a delay between attempts.

Deployment and Scheduling

After deploying to Vercel, you'll need to:

  1. Set up BLUESKY_USERNAME and BLUESKY_PASSWORD in your Vercel environment variables
  2. Configure a recurring task to call your endpoint

You can use Vercel Cron or an external service like cron-job.org to call https://your-domain.com/api/update-profile at your desired interval.

Customization Ideas

Here are some ways you could extend this basic implementation:

Security Considerations

The code uses environment variables for sensitive information, but remember:

Conclusion

With this automated profile updater, your Bluesky profile will always display fresh, up-to-date information without any manual intervention. The full code is available in this GitHub Gist for you to use and modify.

Happy coding!