Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.rocksky.app/llms.txt

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

@rocksky/sdk is a zero-dependency TypeScript client for the Rocksky XRPC API.
  • Type-safe — every endpoint and parameter is statically typed.
  • Builder-friendly — fluent RockskyClient.builder() for ergonomic setup.
  • Pipe-friendly — composable async operators (pipe, withRetry, withTimeout, map, tap, withFallback, catchError).
  • Zero-dependency — only the platform fetch and the standard library.

Install

bun add @rocksky/sdk

Quick start

import { createClient } from "@rocksky/sdk";

const client = createClient();

const profile = await client.actor.getProfile({
  did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
});
const topTracks = await client.charts.getTopTracks({ limit: 5 });

Authentication

import { createClient } from "@rocksky/sdk";

const client = createClient({ auth: process.env.ROCKSKY_TOKEN });

// or refresh on every call
const client2 = createClient({
  auth: async () => loadTokenFromKeychain(),
});
Endpoints that require auth throw RockskyAuthError when no token is configured.

Builder

import { RockskyClient } from "@rocksky/sdk";

const client = RockskyClient.builder()
  .baseUrl("https://api.rocksky.app")
  .bearer(process.env.ROCKSKY_TOKEN!)
  .userAgent("my-app/1.0")
  .timeout(10_000)
  .retries(3)
  .retryDelay(200)
  .header("x-trace-id", crypto.randomUUID())
  .build();
withAuth(token) and withBaseUrl(url) return a new client without mutating the original.

Pipe-style composition

import {
  createClient,
  map,
  pipe,
  tap,
  withFallback,
  withRetry,
  withTimeout,
} from "@rocksky/sdk";

const client = createClient();

const handle = await pipe(
  () => client.actor.getProfile({ did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr" }),
  withRetry(3, { delayMs: 200 }),
  withTimeout(5_000),
  tap((p) => console.log("loaded", p.handle)),
  map((p) => p.displayName ?? p.handle),
  withFallback("anonymous"),
);
OperatorDescription
map(fn)Transform the resolved value.
tap(fn)Run a side-effect; pass the value through.
withRetry(n, { delayMs, factor, shouldRetry })Retry on rejection with exponential backoff.
withTimeout(ms)Reject with RockskyTimeoutError after ms.
withFallback(value | fn)Recover from any error with a default.
catchError(fn)Map a thrown error to a value.

Namespaces

client.actor        getProfile, getActorAlbums, getActorArtists, getActorSongs,
                    getActorScrobbles, getActorLovedSongs, getActorPlaylists,
                    getActorNeighbours, getActorCompatibility
client.album        getAlbum, getAlbums, getAlbumTracks
client.apikey       getApikeys, createApikey, updateApikey, removeApikey
client.artist       getArtist, getArtists, getArtistAlbums, getArtistTracks,
                    getArtistListeners, getArtistRecentListeners
client.charts       getScrobblesChart, getTopArtists, getTopTracks
client.dropbox      getFiles, getMetadata, getTemporaryLink, downloadFile
client.feed         search, getFeed, getFeedGenerators, getFeedGenerator,
                    describeFeedGenerator, getFeedSkeleton,
                    getRecommendations, getArtistRecommendations,
                    getAlbumRecommendations, getStories
client.googledrive  getFile, getFiles, downloadFile
client.graph        followAccount, unfollowAccount, getFollowers, getFollows,
                    getKnownFollowers
client.like         likeSong, dislikeSong, likeShout, dislikeShout
client.mirror       getMirrorSources, putMirrorSource
client.player       getCurrentlyPlaying, getPlaybackQueue, play, pause, next,
                    previous, seek, playFile, playDirectory, addItemsToQueue,
                    addDirectoryToQueue
client.playlist     getPlaylists, getPlaylist, createPlaylist, removePlaylist,
                    startPlaylist, insertDirectory, insertFiles, removeTrack
client.scrobble     createScrobble, getScrobble, getScrobbles
client.shout        createShout, replyShout, reportShout, removeShout,
                    getShoutReplies, getProfileShouts, getTrackShouts,
                    getArtistShouts, getAlbumShouts
client.song         getSong, getSongs, getSongRecentListeners, matchSong,
                    createSong
client.spotify      getCurrentlyPlaying, play, pause, next, previous, seek
client.stats        getStats, getWrapped
Every method takes a typed parameter object and returns Promise<T>. Pass a generic to narrow the response type:
type Profile = { handle: string; did: string; displayName?: string };
const me = await client.actor.getProfile<Profile>({ did: "alice.bsky.social" });

Escape hatch

For endpoints not yet wrapped:
const result = await client.xrpc<MyType>(
  "app.rocksky.something.notWrappedYet",
  "GET",
  { params: { foo: "bar" } },
);

Error handling

import { RockskyHttpError } from "@rocksky/sdk";

try {
  await client.scrobble.getScrobble({ uri: "at://x" });
} catch (err) {
  if (err instanceof RockskyHttpError && err.status === 404) {
    console.log("not found");
  } else {
    throw err;
  }
}
ErrorMeaning
RockskyHttpErrorNon-2xx response. .status, .url, .body
RockskyTimeoutErrorExceeded timeoutMs
RockskyAuthErrorEndpoint requires auth but no token configured
RockskyErrorBase class

Pagination

import { createClient, paginate } from "@rocksky/sdk";

const client = createClient();

// Offset / limit
for await (const s of paginate({
  fetch: ({ limit, offset }) =>
    client.actor
      .getActorScrobbles({ did, limit, offset })
      .then((p) => p.scrobbles ?? []),
  pageSize: 50,
  maxItems: 200,
})) {
  console.log(s.track.title);
}

// Cursor
const followers = await client
  .paginate({
    fetch: async ({ limit, cursor }) => {
      const page = await client.graph.getFollowers({ actor, limit, cursor });
      return { items: page.followers, cursor: page.cursor };
    },
    pageSize: 100,
  })
  .toArray();

Realtime (WebSocket)

import { RealtimeClient, createClient } from "@rocksky/sdk";

const rt = RealtimeClient.builder()
  .baseUrl("https://api.rocksky.app")
  .token(process.env.ROCKSKY_TOKEN!)
  .clientName("my-app")
  .pingInterval(20_000)
  .reconnect({ backoffMs: 1000, maxBackoffMs: 60_000 })
  .build();

rt.on("registered", ({ deviceId }) => console.log("device id:", deviceId));
rt.on("message", ({ data, device_id }) => console.log(device_id, data));
rt.on("control", (c) => console.log("control:", c));

await rt.connect();

await rt.sendMessage({
  type: "track",
  title: "Heart of Glass",
  artist: "Blondie",
});

await rt.sendControl({ action: "play", target: "device-id-123" });

await rt.close();
Events: open, close, error, registered, deviceRegistered, message, control, raw.

Types

Public model types are derived from the Rocksky lexicons and live in src/generated/types.ts. They are regenerated from apps/api/lexicons/**/*.json by running bun run lexgen:types at the repo root.

License

MIT © Tsiry Sandratraina. Source: sdk/typescript.