I own a shelf of sealed Pokémon product. Sixty-odd items - booster boxes, elite trainer boxes, premium collections, a few Pokémon Center exclusives from Japan - and over the last couple of years the shelf has quietly become worth more than some of the furniture it stands near. Which means it belongs on the contents insurance, and contents insurance has a very specific idea of what “owning” something means.
An insurer doesn’t want your enthusiasm. They want, per item: what it is, what it’s worth now, and photographs. Notably absent: what you paid. Insurance is about replacement value, and a box that cost $60 at retail and sells for $300 today is a $300 box. A list and a folder of pictures. If the shelf burns, that folder is the claim.
What I actually had was an iPhone camera roll thousands of photos deep - a few hundred of them of the collection, mixed in with everything else a phone accumulates - a Collectr portfolio that knew the prices, and no link between the two. Closing that gap by hand is an afternoon of squinting at thumbnails, and it’s an afternoon you get to repeat every time you buy more cardboard. So I did it once with Claude Code driving, and the interesting part isn’t the automation. It’s what survived the chore: not a script, a skill - and the second run cost one sentence.
A camera roll is not an inventory
The raw material is a photo dump: IMG_8740.HEIC through IMG_8889.HEIC, one photo per physical item, straight off the phone. The convention that makes this workable is almost embarrassingly simple: two nearly identical photos means quantity two. You don’t annotate anything at photo time. You just photograph every box, including the duplicates, and let the count carry the information.
HEIC is the first small wall - nothing opens it by default - so step one is pillow_heif and a thumbnails/ folder of small JPGs for identification. Step two is the human-judgment part: looking at each thumbnail and deciding which product it is, which produces the only hand-maintained artifact in the whole pipeline, a mapping dict in a Python script:
mapping = {
"Prismatic Evolutions Booster Bundle": ["IMG_8745", "IMG_8747", "IMG_8748", ...],
"151 Alakazam ex Collection": ["IMG_8755", "IMG_8790"],
"Mega Battle Deck (Mega Gengar ex)": ["IMG_8803"],
...
}
organize.py does the rest: moves - not copies, so a photo can’t be claimed twice - each image into a folder named after the product and renames it Product Name_1.HEIC, Product Name_2.HEIC. The structure is deliberately flat. Collectors organize by set; insurance doesn’t care about sets.
The part that earns its keep is the report at the end. The script cross-references the folders against the Collectr inventory and prints four lists: products with no photos, products with fewer photos than the quantity says I own, products with more photos than the quantity, and photos mapped to nothing. Every line is a real question. A box with no photo isn’t evidenced; an extra photo means either a miscount in the tracker or a box I forgot to log. The photos audit the ledger and the ledger audits the photos, and the first run caught discrepancies in both directions.
(Filesystem lesson in passing: Collectr calls a product 151: Alakazam ex Collection and Windows will not give you a folder with a colon in it. Every name goes through a safe_name() scrub, and the CSV keeps the original so the join survives.)
The export button is a paid feature
Prices were supposed to be the easy half. The first run started from Collectr’s CSV export - product, quantity, cost paid, market price. By the second run, export had moved behind the paid plan, and there’s no public API. The data was all still visible, one page at a time, to a human with a mouse.
So the mouse became playwright-cli with a headed browser and a persistent profile - log in once by hand, and every subsequent run reuses the session. Three things come out of Collectr per product:
- The market price, as a screenshot. Currency switched to NZD first, because my insurer thinks in NZD and a valuation that needs a conversion step is a valuation someone gets to argue with. Then the product’s portfolio card - name, image, current market price - is screenshotted into the product’s folder as
price_info.png. A screenshot turns out to be exactly the right evidence format for an insurer: the number, the product, and the source, in one image nobody can fat-finger in a spreadsheet. - What I paid and when - not for the insurer, who only cares about replacement value, but because it’s my cost basis and the browser is already logged in. It lives three clicks deep behind an “Enter Product Details” dialog - one entry per purchase, so a product bought twice at two prices keeps both cost bases and both dates.
- A dated CSV per batch -
new_additions_2026-06-18.csv- tying each product to its quantity, cost, acquisition date, market price, photo paths and screenshot. That file plus the folders is the entire claim package.
The UI fights back
Everything above sounds clean because it’s the version after the bugs. Driving a React app that was never meant to be driven is where the actual time went, and the war stories are the part worth writing down.
The search box lies. Filling the portfolio search field with Playwright’s .fill() does nothing - the list only filters on a keystroke event or a URL query param. Navigating to portfolio/products?query=... sidesteps the whole question.
The selector that worked was a Bulbasaur. On one page, the quantity “+” button is reliably found via a div.select-none wrapper. On the product pages, div.select-none is a decorative Bulbasaur pet animation, and clicking it does nothing forever. The fix that stuck: find the one button with a stable accessibility label - aria-label="Open inventory options" - and locate the minus and plus buttons by position relative to it. Ugly, honest, works.
The calendar returns days from the wrong month. Setting an acquisition date means driving a React Day Picker, and paging it with the prev/next buttons hits a genuine trap: the grid shows adjacent-month days, so reading “the current month” off the first visible day button tells you May when you’re looking at June. The reliable path is the month and year <select> dropdowns inside the calendar, then clicking the day by its ordinal label - because "March 2" matches eleven elements (“March 2”, “March 20”, “March 21”…) and "March 2nd" matches exactly one.
You can’t move anything. Collectr has no “move between portfolios”, and I keep two - insured and not-yet-insured, which is a real distinction with a premium attached. Getting a product insured means recreating every entry in the insured portfolio, then zeroing it out in the source. Even zeroing has a lesson: clicking minus n times loses races against the button’s animation, while focusing the quantity input and typing 0 Enter works every time.
None of these is individually hard. Collectively they’re two hours of discovery, and they’re exactly the kind of knowledge that evaporates.
The artifact is the skill
The first pass at this was a normal Claude Code session: interactive, exploratory, full of dead ends like the Bulbasaur. The traditional fate of that knowledge is a chat transcript nobody rereads, page four of which contains the only correct explanation of the calendar bug.
Instead, everything above got distilled into a skill - a SKILL.md in ~/.claude/skills/pokemon-insurance/ that reads as a runbook: the steps in order, the working-directory layout, the code snippets that survived contact with the UI, and a Key gotchas list where every line is an hour someone doesn’t spend twice:
- Use py not python on this Windows system
- Cannot move items between Collectr portfolios - must recreate and zero the source
- div.select-none is the Bulbasaur pet animation overlay - do NOT use it to find the + button
- portfolio search requires Enter or URL query param - .fill() alone does not filter
- Play! Pokemon Prize Packs do NOT need photos (skip entirely)
A runbook written for an agent is better than one written for a future me, because it’s allowed to be pathologically specific. “The quantity input is two buttons before the element labeled Open inventory options” is a terrible sentence to hand a human and a perfect one to hand a machine. Future-me documentation drifts toward aspirational prose; agent documentation stays at the altitude where the bugs live.
The proof came in June. New boxes, photos into a staging folder, and the entire instruction was: update the insurance. Triage against the not-yet-insured portfolio, folders created, photos renamed, purchase entries recreated in the insured portfolio - calendar dropdowns, ordinals and all - source zeroed, prices screenshotted in NZD, dated CSV written. The chore that was an afternoon is now a sentence and a coffee.
Here’s the detail I enjoy most: while writing this post I went looking for the original chat sessions, and they’re gone - cleaned up months ago. It doesn’t matter at all. The transcript was never the right home for what those sessions produced. The skill is, and the skill is the one file that gets better every time the workflow runs, because every new gotcha has an obvious place to go.
The shelf, meanwhile, finally has the shape the insurer wanted all along: a folder per product, photos named after the thing they show, and a screenshot that says what it’s worth and where that number came from. The CSV also remembers what I paid - the number the insurer never asks for, and the one that tells me the shelf was a good idea. I hope the folder never gets opened for its real purpose. But it’s ready, and keeping it ready no longer costs me anything.