ewan's projects — docs

Malachite

February 24, 2026

# malachite# atproto# lastfm# spotify# tools

Malachite is a CLI tool for importing your Last.fm and Spotify listening history to the AT Protocol network as fm.teal.alpha.feed.play records. It's designed to be safe, resumable, and smart about rate limits — so you don't accidentally hammer your PDS.

The name is a deliberate nod to the teal lexicon it publishes to: malachite is a greenish-blue copper mineral associated with preservation and transformation, sitting squarely in that teal/green colour range.

Quick Start

# Install dependencies and build
pnpm install
pnpm build

# Run in interactive mode (recommended for first-time use)
pnpm start

# Or with command line arguments
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y

Interactive mode walks you through everything: choosing a mode, entering credentials, picking files, and setting optional flags.

Import Modes

Mode Flag Description
Last.fm -m lastfm (default) Import a Last.fm CSV export
Spotify -m spotify Import Spotify Extended Streaming History JSON
Combined -m combined Merge both sources with deduplication
Sync -m sync Skip records that already exist on Teal
Deduplicate -m deduplicate Remove duplicate records already on Teal

Command Line Options

Required

Option Short Description
--input <path> -i Path to Last.fm CSV or Spotify JSON file/directory
--handle <handle> -h Your ATProto handle or DID
--password <pass> -p Your ATProto app password (not your main password)

Common Options

Option Short Description
--mode <mode> -m Import mode (see table above)
--spotify-input <path> Spotify export path (for combined mode)
--reverse -r Process newest tracks first
--yes -y Skip confirmation prompts
--dry-run Preview records without publishing
--verbose -v Debug-level logging
--quiet -q Warnings and errors only
--dev Verbose + file logging + smaller batches
--pds <url> Skip identity resolution and use a known PDS URL directly

Getting Your Data

Last.fm: Export your scrobbles from lastfm.ghan.nl/export as a CSV.

Spotify: Go to Spotify Privacy Settings, request your "Extended streaming history" (takes up to 30 days), then use either a single Streaming_History_Audio_*.json file or the whole extracted directory. Malachite automatically filters out podcasts and non-music content.

Duplicate Prevention

Malachite has two layers of protection against duplicates:

Input deduplication — before publishing anything, it removes entries within your source file that share the same track name, artist, and timestamp.

Teal comparison — it fetches all your existing Teal records and skips anything already imported. This uses adaptive batch sizing (starting at 25, scaling up to 100 based on network performance) and shows real-time progress. This runs automatically; no special mode needed, but credentials are required even for dry runs.

Rate Limiting

Bluesky's AppView enforces rate limits on PDS instances. Exceeding 10K records per day can rate-limit your entire PDS — affecting all users on it, not just your account.

Malachite handles this automatically:

  • Monitors your rate limit quota in real-time from response headers
  • Dynamically adjusts batch size between 1 and 200 records
  • Maintains a 15% headroom buffer so the quota is never fully exhausted
  • Hard daily cap of 7,500 records (75% safety margin)
  • Pauses 24 hours between days for large imports
  • Scales immediately back to maximum speed after a quota reset

You can also check your current rate limit status at any time:

npm run check-limits

File Storage

All data is stored in ~/.malachite/:

~/.malachite/
├── cache/            # Cached Teal records (24-hour TTL)
├── state/            # Import state for resume support
├── logs/             # Logs when --dev is active
└── credentials.json  # AES-256-GCM encrypted credentials (optional)

Credentials are encrypted using a key derived from your hostname and username, making them machine-specific. You can clear them with pnpm start --clear-credentials.

Record Format

Each scrobble is published as an fm.teal.alpha.feed.play record. Required fields are trackName, artists, playedTime, submissionClientAgent, and musicServiceBaseDomain. Last.fm imports also include MusicBrainz IDs when available.

Example Last.fm record:

{
  "$type": "fm.teal.alpha.feed.play",
  "trackName": "Paint My Masterpiece",
  "artists": [{ "artistName": "Cjbeards", "artistMbId": "c8d4f4bf-..." }],
  "releaseName": "Masquerade",
  "playedTime": "2026-11-13T23:49:36Z",
  "originUrl": "https://www.last.fm/music/Cjbeards/_/Paint+My+Masterpiece",
  "submissionClientAgent": "malachite/v0.9.3",
  "musicServiceBaseDomain": "last.fm"
}

Development

pnpm run type-check   # Type checking
pnpm run build        # Build
pnpm run dev          # Rebuild and run
pnpm run test         # Run tests
pnpm run clean        # Clean build artifacts

Lexicon

Malachite publishes to the fm.teal.alpha lexicon. The schema definitions live in /lexicons/fm.teal.alpha/ in the repository.

License

AGPL-3.0-only.


← all docs