diff --git a/docs/superpowers/plans/2026-07-13-lastfm-integration.md b/docs/superpowers/plans/2026-07-13-lastfm-integration.md
new file mode 100644
index 0000000..99f29e4
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-13-lastfm-integration.md
@@ -0,0 +1,93 @@
+# Last.fm Integration — Implementation Plan
+
+Approved via plan mode 2026-07-13. Built page-first in 3 shippable slices (each ships +
+verifies before the next), via subagent-driven-development (per-task review + Opus
+whole-branch review per slice), branch `feature/lastfm` → ff-merge `main` → deploy.
+
+## Context
+Lyra's Discovery surfaces new artists/albums via pluggable `SimilaritySource`s (today only
+ListenBrainz). Weave Last.fm in two ways: (1) a **second discovery source** (`artist.getSimilar`),
+and (2) a **"Last.fm" browse page** that pulls the user's own top artists/albums (listening
+history) into Artists/Albums tabs, with a hide-in-library toggle and the existing modal +
+Follow/Want flow for new items.
+
+## Confirmed decisions
+- **Auth = username + API key** (no OAuth). `user.getTopArtists`/`getTopAlbums`/`artist.getSimilar`
+ work with an API key + a public username.
+- **"Hide items in my library" = owned OR followed/wanted** (LibraryItem owned, WatchedArtist
+ followed, or MonitoredRelease monitored/wanted).
+- **Page-first, 3 slices**: (1) creds + Settings tab, (2) browse page, (3) discovery source.
+- **No Prisma migration** — everything on existing tables + Config keys.
+
+## Cross-cutting facts
+- Config is a flat KV table; the worker decrypts `secret` rows (`worker/lyra_worker/config.py`
+ `get_config` + `crypto.py`, a port of `web/src/lib/crypto.ts`). Store `lastfm.api_key`
+ **encrypted** (`secret:true`); both layers read it.
+- Pipeline is **MBID-native**; Last.fm returns names (mbid often empty). Resolve names→MB
+ identities before follow/want/own-state line up. Own-state / library-match is exact-string on
+ the MB canonical name/title.
+- MB rate limit is a shared global ~1 req/s (`musicbrainzngs` in worker; web MB client unthrottled
+ but used one-item-at-a-time). Resolve lazily on-demand + cache.
+- **Last.fm API:** base `https://ws.audioscrobbler.com/2.0/`, `?method=...&api_key=KEY&format=json`.
+ `user.gettopartists`/`user.gettopalbums` (`&user=&period=overall|7day|1month|3month|6month|12month&page=&limit=`);
+ `artist.getsimilar` (`&mbid=` or `&artist=`, `&limit=`). Errors return `{error,message}` at HTTP
+ 200 — parse the `error` field.
+
+## Slice 1 — credentials + Settings tab
+- `web/src/app/api/config/route.ts` — `FIELDS` += `lastfmApiKey→{key:"lastfm.api_key",secret:true}`,
+ `lastfmUsername→{key:"lastfm.username",secret:false}`. GET returns `lastfmUsername` (plain) +
+ `lastfmApiKeySet`. PUT/DELETE unchanged (iterate FIELDS; DELETE hasOwnProperty guard covers new keys).
+- `web/src/app/api/config/route.test.ts` — PUT encrypts key + stores username plain; GET masks key;
+ DELETE `?field=lastfmApiKey` clears.
+- `web/src/app/settings/settings-form.tsx` — new `"Last.fm"` tab (Tab type + TABS) mirroring the
+ Soulseek tab: secret API-key input (•••• stored + `[Clear]` via existing `clearField`) + plain
+ username input; load in the GET effect; add both to the `saveCredentials` PUT body; update eyebrow.
+- Reuse: FIELDS mechanism, `encryptSecret`, `clearField`, Qobuz/Soulseek tab markup, `*Set` masking.
+
+## Slice 2 — `/lastfm` browse page
+- New `web/src/lib/lastfm.ts` (server-side): `topArtists(user,apiKey,{period,page,limit})` →
+ `{artists:{name,playcount,mbid|null}[],totalPages}`; `topAlbums(...)` →
+ `{albums:{name,artist,playcount,mbid|null}[],totalPages}`; shared `lfmGet(method,params)` that
+ appends `api_key`/`format=json` and throws on the `error` field.
+- New routes: `GET /api/lastfm/top-artists?period=&page=` and `.../top-albums` — read
+ `lastfm.username`/`lastfm.api_key` (400 if unset), call the lib, annotate own-state from one cheap
+ DB read (artists: `followed`=WatchedArtist name/mbid, `owned`=LibraryItem.artist; albums:
+ `owned`=(artist,album) in LibraryItem, `wanted`=MonitoredRelease). `inLibrary = owned||followed||wanted`.
+ Name-based (no per-item MB calls). `GET /api/mb/release-group?artist=&album=` — thin wrapper over
+ `searchReleaseGroup` (`web/src/lib/musicbrainz.ts:61`) → `{rgMbid,artistMbid,artistName,...}` or 404.
+- New page `web/src/app/lastfm/page.tsx` → ``; add `{href:"/lastfm",label:"Last.fm"}`
+ to `_ui/contents-nav.tsx`.
+- `lastfm-client.tsx`: PageHead; `.tabs` Artists|Albums; period `