Project

Apple Wallet Support Tracker: A Dataset That Maintains Itself

I didn't want to manually track which brands support Apple Wallet, so I built an open dataset that researches and corrects itself: a monthly cron, a Claude orchestrator, and a fleet of parallel research agents.

11 min read03.06.2026Justin LanfermannSource
An autonomous research agent verifying Apple Wallet support across many brands and opening pull requests

While building NeatPass, I kept running into the same boring question: does this airline, transit operator, or loyalty program actually hand you a native Apple Wallet pass? There is no canonical list. The answer changes constantly, it is buried in support pages and forum threads, and any spreadsheet you build rots within a month.

So I built the Apple Wallet Support Tracker: an open dataset of which brands ship native .pkpass support. The interesting part is not the data, it is the machinery. I did not want to maintain it by hand, so I made it maintain itself. A scheduled job spawns AI research agents that fact-check every brand, cite their sources, and open a pull request for me to review. It is the dataset behind the live NeatPass Wallet Support Tracker, and this post is about how the autonomous part actually works.

The dataset

Each brand is one folder on disk: a data.json with the structured row, and a research.md that records which pages were reviewed and a chronological history of every change. A generated index.json is the fast path for consumers. Per brand, a row captures:

  • Native pkpass support, as full, partial, or none.
  • Whether the iOS app exposes a Live Activity, and whether passes sync to Apple Watch.
  • Known issues observed in the wild.
  • A list of cited sources, each tagged by type: official, support, press, or community.

That last field is the whole point. Every fact has to be backed by a URL, and every URL has a priority. Once facts must cite their evidence, you can let a machine update them without the dataset quietly turning into fiction.

Letting a cron job do my research

The core loop is a GitHub Actions workflow that runs at 04:00 UTC on the first of every month. It checks out the repo, then hands a prompt to Claude Code running as an agent. No human is in the loop until a PR shows up.

The agent it starts is not a single worker grinding through 54 brands one at a time. It is an orchestrator. Its only job is to read the brand index, split it into roughly six batches, and dispatch a subagent per batch using the Task tool, all in a single message so they run concurrently. Then it waits, collates the results, regenerates the index, and validates.

Why bother with the fan-out? Verifying 54 brands sequentially takes 30 to 45 minutes of web searches. In parallel batches it drops to under ten. Each subagent gets its own focused context window, so they never bleed one brand's facts into another. The orchestrator is explicitly forbidden from doing research itself: never verify brands yourself, always dispatch via subagents.

The deterministic shell around a non-deterministic agent

The agent is the smart, unpredictable part. Everything around it is deliberately dumb. The workflow is a fixed sequence of shell steps, and the agent is exactly one of them. Before it runs, the job resolves the current month, finds or creates a milestone for it, and writes the sweep's scope to .agent/sweep-config.json. That file is how the workflow talks to the agent: scope and a dry-run flag handed over as data, not stuffed into the prompt. Then it checks the token exists, loads prompts/sweep.md from disk into an env var, and only then hands control to Claude.

The agent step itself runs with continue-on-error, so a flaky model run never hard-fails the job. Afterwards the workflow re-runs npm run validate on its own, reads the outcome, and branches: success opens a PR, failure opens a type:bug issue filed against that month's milestone so a bad sweep can never just vanish.

That split is the whole design. The agent never opens its own PR, never edits the index, never gets to decide whether its own work is valid. It researches and edits files; the workflow does the bookkeeping, the validation, and the git. If the model loses the plot, the worst case is a no-op run and an auto-filed bug, not a corrupted dataset on main.

Teaching agents to be good researchers

An agent let loose on the open web will happily cite a random Reddit comment as gospel. The fix is not a smarter model, it is a stricter prompt. Each subagent runs a short, opinionated procedure: read the existing row, run at most three searches, and stop at the first trustworthy source that confirms or contradicts it. Trustworthiness is ranked, and the ranking is in the prompt:

Community sources can only ever be supporting evidence, never the sole basis for a fact. The agent bumps lastChecked, appends a citation, caps the source list at five, and logs a one-line entry to research.md. It is also told never to run reindex itself, because the orchestrator does that exactly once after all batches land. Small rule, but it is what keeps six agents from fighting over the same generated file.

The cost discipline is just as deliberate. A brand checked in the last 30 days gets a quick refresh, not a full re-investigation. The whole run is capped at a 120-turn budget across the orchestrator and all subagents, on medium effort. A monthly sweep ends up costing a few dollars, which is the difference between an automation I actually leave running and one I switch off after the first bill.

Two backends, and not an API key in sight

The prompts/ directory is the entire agent definition: three Markdown files. sweep.md is the orchestrator, sweep-batch.md is the subagent it spawns, and issue-fix.md is the reactive handler. Swapping the brain behind those prompts is a dropdown. Both workflows take an agent input, claude or codex, and the cron always uses Claude. Claude runs through anthropics/claude-code-action; Codex runs through codex exec --full-auto on gpt-5.5. Same prompt file, different model.

Neither bills against a metered API key. They authenticate with subscription tokens, CLAUDE_CODE_OAUTH_TOKEN or CODEX_AUTH_JSON, and the workflow hard-fails with a clear error if the secret is missing rather than silently falling back to a pay-per-token key. A runaway loop can burn its turn budget, but it cannot quietly run up an API bill.

Tool grants are scoped per role. The sweep orchestrator gets the Task tool so it can spawn subagents. The issue handler deliberately does not, because one issue should touch exactly one brand and never fan out. Both get WebSearch, WebFetch, Read, Edit, and Bash, and nothing else. The agent only ever holds the tools its job needs.

The internet is hostile, so the issues are too

The monthly sweep keeps the dataset fresh, but the fastest corrections come from people who spot something wrong. So there is a second workflow: file an issue labelled type:correction or type:new-brand, and an agent picks it up, verifies the claim, and opens a PR. The catch is that an issue body is untrusted input from a stranger, and that input goes straight into a prompt. That is a textbook prompt-injection setup. The workflow wraps the issue in <untrusted-input> tags, and the prompt draws a hard boundary around them:

The rest of the handler is built to fail safe. If the cited URL returns a 4xx, if a community source has no higher-priority backing, or if the request is ambiguous, the agent changes nothing and writes a summary instead, and the workflow labels the issue needs-human. An idempotency guard skips the run entirely if a branch for that issue already exists, and corrections from outside contributors require manual approval before the agent ever starts. One issue touches exactly one brand.

Trusting a robot with my data

The thing that makes this comfortable to run is that the AI is the researcher, not the committer. It never writes to the published dataset directly. Every path, sweep or correction, ends at a pull request that I review. The guardrails around that are boring on purpose:

  • PRs, never pushes. The agent works on stage and opens a PR. Releases are a separate, manual fast-forward to main.
  • Validation gates every change. A schema check, slug and index consistency, brand uniqueness, and live URL reachability probes all have to pass in CI.
  • The prompts are protected. The prompt files, schema, and workflows are CODEOWNERS-guarded, and the agent is explicitly told it may never edit them. The thing being automated cannot rewrite its own instructions.
  • Bounded blast radius. Turn budgets, concurrency caps, and the one-issue-one-brand rule mean a confused run is cheap and contained, not catastrophic.

None of this is clever. It is the same instinct as treating a junior contributor's PR with friendly suspicion: let them do the legwork, but read the diff before it ships.

The same gates apply to everything, not just the agent's PRs. A test workflow runs npm run validate on every push and pull request. A conventions check rejects any PR missing exactly one type:* label and at least one area:* label, and a labeler applies the area tags automatically from the changed paths. Releases are a manual, semver-checked fast-forward from stage to main, guarded by a branch ruleset that even the release job needs a scoped token to bypass. Agent or human, the path to main is identical.

Stopping strangers from draining my account

Here is a hole I caught early, one of the first things I locked down. The issue handler triggers on labelled issues, and the issue templates auto-apply type:correction and type:new-brand for whoever files them. Put those two facts together and anyone on the internet can open an issue, watch it get auto-labelled, and kick off an agent run that spends my Claude or Codex subscription. A few hundred of those is a cheap denial-of-wallet attack.

The fix is a GitHub Environment used as an approval gate, chosen dynamically per run. If the trigger is the monthly cron or the actor is me, the job runs in agent-auto, which has no protection rules. Everyone else lands in agent-approval-required, which lists me as a required reviewer. The job pauses there before any model starts, so a stranger's issue waits in a queue for one approving click instead of quietly burning tokens.

There is a second, quieter guard for cost. Before doing any work, the handler checks the remote for an agent/issue-<number>-<agent> branch. If one already exists, a previous run is either in flight or sitting in review, so it skips. Re-labelling the same issue, or a double-fire from GitHub, cannot fan out into duplicate agent runs that each cost real money.

Neither trick is glamorous. But the moment you wire a paid model to a public trigger, abuse stops being hypothetical. The cheapest insurance is making sure nothing expensive ever runs without either a schedule or a human standing behind it.

Closing the loop

The dataset is only useful if something consumes it. The NeatPass Wallet Support Tracker pulls the repo's raw JSON at build time, validates it, and code-generates a typed module that the page renders as a filterable table. Pinning to a release tag means a bad upstream day can never break a deploy.

And the loop closes on itself. Every row on that page has a report a correction link that opens a pre-filled issue against the repo, which the issue-handler agent then picks up, verifies, and turns into a PR. A reader spotting a stale fact becomes the trigger for the next autonomous fix. The system gets a little more correct every time someone uses it.

What I would not pretend works perfectly

This is automation I trust, not magic. A few honest edges:

  • An LLM can still cite a confident, well-written source that happens to be wrong. The source-priority rules reduce this, they do not eliminate it. The human review step is load-bearing, not ceremonial.
  • Monthly cadence is a freshness-versus-cost tradeoff. A brand that changes on the 2nd waits 29 days for the sweep, unless someone files an issue first.
  • Facts about Apple Wallet decay fast. The dataset's own disclaimer says it plainly: verify against the cited sources before acting on a row. The tracker tells you when something was last checked for exactly this reason.

Resources

The full source, dataset, prompts, and workflows are on GitHub, and you can see the data in action on the NeatPass Wallet Support Tracker. If you spot a brand that is wrong or missing, the correction link on that page is the fastest way to feed it back into the loop.