Bluesky API

v1.0.3

Read, search, post, and monitor Bluesky (AT Protocol) accounts

0· 74·0 current·0 all-time
MIT-0
Download zip
LicenseMIT-0 · Free to use, modify, and redistribute. No attribution required.
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Benign
high confidence
Purpose & Capability
The skill's name/description match the provided scripts and SKILL.md: public reads/search use public.api.bsky.app and authenticated posting uses bsky.social with an app password. Minor metadata inconsistency: registry metadata lists no required binaries/env vars, but SKILL.md and scripts require curl and jq and optionally accept BSKY_HANDLE/BSKY_APP_PASSWORD.
Instruction Scope
SKILL.md and the three included scripts limit actions to Bluesky API endpoints, input validation is present (limit numeric checks, length checks), and the post flow recommends using app passwords and not exposing credentials. There are no instructions to read unrelated files or send data to unexpected endpoints.
Install Mechanism
No install spec is provided (instruction-only operation with included scripts). Nothing is downloaded or written during install, which minimizes risk.
Credentials
The skill does not require any platform-wide credentials; it optionally uses BSKY_HANDLE and BSKY_APP_PASSWORD for posting, which is proportional to the documented functionality. Confirming the registry omitted these optional env vars in metadata is advisable.
Persistence & Privilege
The skill is not marked always:true and does not request persistent system privileges or modify other skills. It can be invoked by the agent (default behavior), which is expected for an integration that can post on behalf of a user.
Assessment
This skill appears coherent and limited to Bluesky API usage. Before installing: (1) Verify you trust the skill source (it’s marked 'source: unknown'). (2) If you enable posting, create and use an app password (the SKILL.md correctly advises this); avoid supplying your main account password. (3) The scripts require curl and jq and optionally BSKY_HANDLE/BSKY_APP_PASSWORD—make sure those tools are available and understand that providing the app password allows the skill to create posts on your behalf. (4) Note the small metadata mismatch: the registry doesn’t list required binaries/env vars even though SKILL.md and scripts expect them; consider asking the publisher to correct the metadata. If you need stricter control, do not provide BSKY_APP_PASSWORD or restrict the skill from autonomous invocation.

Like a lobster shell, security has layers — review code before you run it.

atprotovk97d138n1ykab6j9zpgzm164kn83j7d1blueskyvk97d138n1ykab6j9zpgzm164kn83j7d1latestvk97btnp9p4gr29hrqy315ye3gd83k55vsocialvk97d138n1ykab6j9zpgzm164kn83j7d1

License

MIT-0
Free to use, modify, and redistribute. No attribution required.

Runtime requirements

🦋 Clawdis

SKILL.md

Bluesky Skill

Interact with Bluesky via the AT Protocol API. Supports public reads, search, authenticated posting, and profile monitoring.

Configuration

Credentials can come from openclaw config or environment variables:

SourceHandleApp Password
Configchannels.bluesky.handlechannels.bluesky.appPassword
Env varBSKY_HANDLEBSKY_APP_PASSWORD

App passwords are created at: https://bsky.app/settings/app-passwords

Never use a main account password. Always use an app password.


1. Read — Fetch Recent Posts from a Profile

No auth required. Uses the public API.

API Endpoint

GET https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=<handle>&limit=<n>
  • actor — Bluesky handle (e.g. alice.bsky.social) or DID
  • limit — number of posts to return (1–100, default 50)

curl Example

curl -s "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=alice.bsky.social&limit=5" | jq '.feed[] | {text: .post.record.text, createdAt: .post.record.createdAt, uri: .post.uri}'

Helper Script

./scripts/bsky-read.sh alice.bsky.social 5

Response Structure

The response JSON has a feed array. Each entry contains:

  • .post.record.text — the post text
  • .post.record.createdAt — ISO 8601 timestamp
  • .post.uri — AT URI of the post
  • .post.author.handle — author handle
  • .post.author.displayName — author display name
  • .post.likeCount, .post.repostCount, .post.replyCount — engagement counts

2. Search — Find Posts by Keyword

No auth required. Uses the public API.

API Endpoint

GET https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=<query>&limit=<n>
  • q — search query (keywords, hashtags, phrases)
  • limit — number of results (1–100, default 25)

curl Example

curl -s "https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=openclaw&limit=10" | jq '.posts[] | {text: .record.text, author: .author.handle, createdAt: .record.createdAt}'

Helper Script

./scripts/bsky-search.sh "openclaw" 10

Response Structure

The response JSON has a posts array. Each entry contains:

  • .record.text — the post text
  • .record.createdAt — ISO 8601 timestamp
  • .author.handle — author handle
  • .author.displayName — author display name
  • .uri — AT URI of the post

3. Post — Create a New Post

Requires auth. Uses app password authentication.

Step 1: Authenticate

POST https://bsky.social/xrpc/com.atproto.server.createSession
Content-Type: application/json

{"identifier": "<handle>", "password": "<app_password>"}

This returns a session with accessJwt and did.

curl Example

# Always use env vars — never interpolate credentials directly into shell strings
SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg h "$BSKY_HANDLE" --arg p "$BSKY_APP_PASSWORD" '{identifier: $h, password: $p}')")

ACCESS_TOKEN=$(echo "$SESSION" | jq -r '.accessJwt')
DID=$(echo "$SESSION" | jq -r '.did')

Step 2: Create the Post

POST https://bsky.social/xrpc/com.atproto.repo.createRecord
Authorization: Bearer <accessJwt>
Content-Type: application/json

{
  "repo": "<did>",
  "collection": "app.bsky.feed.post",
  "record": {
    "$type": "app.bsky.feed.post",
    "text": "<post text>",
    "createdAt": "<ISO 8601 timestamp>"
  }
}

curl Example

curl -s -X POST "https://bsky.social/xrpc/com.atproto.repo.createRecord" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"repo\": \"${DID}\",
    \"collection\": \"app.bsky.feed.post\",
    \"record\": {
      \"\$type\": \"app.bsky.feed.post\",
      \"text\": \"Hello from OpenClaw!\",
      \"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\"
    }
  }"

Helper Script

# Pass app password via env var — never as a CLI argument (visible in ps/shell history)
BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx ./scripts/bsky-post.sh alice.bsky.social "Hello from OpenClaw!"

Post Length Limit

Bluesky posts have a 300-character limit (grapheme count). Check length before posting.


4. Monitor — Check for New Posts Since Last Check

Use the read endpoint with a timestamp comparison to detect new posts. This is useful for heartbeat monitoring.

Approach

  1. Fetch recent posts from the target profile.
  2. Compare .post.record.createdAt against the last-known timestamp.
  3. Any post with a createdAt newer than the stored timestamp is new.

curl Example

# Fetch the 10 most recent posts
FEED=$(curl -s "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=alice.bsky.social&limit=10")

# Filter posts newer than a given timestamp
SINCE="2026-03-24T00:00:00.000Z"
echo "$FEED" | jq --arg since "$SINCE" '.feed[] | select(.post.record.createdAt > $since) | {text: .post.record.text, createdAt: .post.record.createdAt}'

Monitoring Loop Pattern

For heartbeat use, store the latest seen timestamp and compare on each check:

# Use a user-owned state dir, not world-readable /tmp
LAST_SEEN_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/bsky-monitor-${HANDLE}.txt"
mkdir -p "$(dirname "$LAST_SEEN_FILE")"
HANDLE="alice.bsky.social"

# Read last seen timestamp (or default to 24h ago)
if [ -f "$LAST_SEEN_FILE" ]; then
  SINCE=$(cat "$LAST_SEEN_FILE")
else
  SINCE=$(date -u -v-24H +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S.000Z)
fi

FEED=$(curl -s "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${HANDLE}&limit=20")
NEW_POSTS=$(echo "$FEED" | jq --arg since "$SINCE" '[.feed[] | select(.post.record.createdAt > $since)]')
COUNT=$(echo "$NEW_POSTS" | jq 'length')

if [ "$COUNT" -gt 0 ]; then
  echo "Found $COUNT new post(s) from $HANDLE since $SINCE"
  echo "$NEW_POSTS" | jq '.[] | {text: .post.record.text, createdAt: .post.record.createdAt}'
  # Update last seen to the most recent post timestamp
  echo "$NEW_POSTS" | jq -r '.[0].post.record.createdAt' > "$LAST_SEEN_FILE"
else
  echo "No new posts from $HANDLE since $SINCE"
fi

Error Handling

HTTP StatusMeaningAction
200SuccessParse response
400Bad requestCheck parameters
401UnauthorizedRe-authenticate (token may be expired)
404Not foundCheck handle/DID exists
429Rate limitedBack off and retry after delay

Auth Token Expiry

Access tokens expire. If you get a 401, re-authenticate by calling createSession again. Do not cache tokens for more than a few minutes.


Quick Reference

OperationAuth?Script
Read profile feedNo./scripts/bsky-read.sh <handle> [limit]
Search postsNo./scripts/bsky-search.sh <query> [limit]
Create postYes./scripts/bsky-post.sh <handle> <app_password> <text>
Monitor new postsNoUse read + timestamp filter (see section 4)

Files

4 total
Select a file
Select a file to preview.

Comments

Loading comments…