Skip to content
Adam Probert

For myself · AI / Web · 2025

Grimoire ML

Natural-language Magic: The Gathering search, as a small fleet of services.

What it was

The project

An AI-assisted Magic: The Gathering card discovery and deckbuilding app. A mono-repo of four services: a FastAPI backend, a query microservice that turns natural language into an intermediate representation and then into Scryfall queries, a Redis-cached card image proxy, and a Next.js frontend, with Prometheus and Grafana wired in from the start.

The Grimoire ML mark: a burning spellbook with ML in the flames.Grimoire ML
Grimoire ML Logo
Grimoire ML Search Results
Grimoire ML Search Bar
Build

How it fits together

PROMETHEUS · /metricsIR CACHEREDISPOSTGRESBROWSERWEBAPP:3000 · NEXT.JSQUERY:8080 · NL → IRCARD-DB:8081 · IMAGESBACKEND:8000 · DECKSOPENAIGPT-5-MINISCRYFALLSEARCH + IMAGES
  • A query service that goes natural language, then intermediate representation, then Scryfall query, with Redis caching between stages.
  • A dedicated card image proxy with its own cache and metrics, keeping image traffic off the main backend.
  • A Next.js frontend in TypeScript with Tailwind and Mantine.
  • Prometheus metrics and Grafana dashboards, plus a Docker Compose stack wiring Postgres, Redis and every service together for local development.
Key decisions

The trade-offs that mattered

  • An intermediate representation between the model and the API

    Going via a structured IR makes the model's output inspectable and lets the query layer be tested without the model at all.

  • Cache aggressively at every boundary

    Card data barely changes and model calls cost money and latency. Redis between each stage made the app feel instant on repeat queries and kept costs low when testing.

  • Observability from day one, on a side project

    Wiring Prometheus and Grafana in early cost a couple of hours afternoon but paid for itself time and time again when Claude gave crappy responses to my prompts. Gaining deep insights to the internals of this microservice structure app was invaluable.

Lesson learned

What I took from it

Asking a language model to emit search syntax directly is brittle and impossible to debug. Putting a structured intermediate representation in between turned an unpredictable component into a testable one - I could exercise the whole query layer with no model involved at all. That indirection is what made the results trustworthy, and it is the bit I would do first next time rather than last.

Contact

Say hello.

Whether it’s work, something you’re stuck on, or a project you’re thinking about starting, I’m happy to chat through it.

Get in touch
  • Something you're building, or thinking about building
  • A problem you're stuck on and want another pair of eyes on
  • Anything on this site you want to know more about
  • A role or piece of work worth a conversation