Professional
Cantina Royale Tools
A public explorer for a live blockchain game's 20,413 NFTs, built on an offline snapshot compiled into SQLite at build time, so the data is reviewed like code rather than fetched at request time.
The public explorer for Cantina Royale: five NFT collections, their traits and stats, the gameplay balance tables behind them, and current market activity. It is the player-facing surface of the data work I did as the game’s data engineer.
- Scope
- 5 collections · 20,413 NFTs · 48 gameplay tables
- Chain
- MultiversX, with XOXNO marketplace data
- Serving
- SQLite compiled at build time, queried server-side
- Role
- Data engineer
- Repository
- VincenzoImp/cantinaroyale.tools
The problem
A game’s data does not live in one place. Ownership and traits are on chain, item metadata sits behind the studio’s own endpoints, listings live on a marketplace, and the numbers that decide what an item actually does – damage curves, upgrade costs, reward pools – are balance tables that ship with the game and change when designers change them.
An explorer has to join all of that, and it has to do it without either hammering four external services on every page view or shipping the whole corpus to the browser.
What I built
The data is not fetched at request time. A Python pipeline runs offline against the chain API, the metadata hosts and the marketplace, and writes a JSON snapshot that is committed to the repository. At build time that snapshot and 48 balance CSVs are compiled into a SQLite database, which the application then queries server-side and returns as small paginated pages.
That choice has a consequence I like: a data refresh is a pull request. The snapshot diff is reviewed, and validation plus an asset-size budget run before it merges. Bad data cannot arrive silently, because arriving is a reviewed event.
Hard parts
Getting the payload off the client. The naive version of this ships the collection to the browser and filters it there, which is fine at a hundred items and unusable at twenty thousand. Moving the query into SQLite on the server means a collection view returns one page rather than the whole corpus, and the trait-heavy detail data is loaded only when one item is opened.
Making the build atomic. The database is written to a temporary file and only swapped over the live one after a successful build, so a failed compile leaves the previous database in place rather than a half-written one. Deploys are all-or-nothing by construction.
Retiring the robot that committed to main. The refresh used to run in a container that woke
at midnight, cloned the repository with a token, ran the pipeline, and pushed a commit called
Auto-update to main. It worked, and it meant unreviewed data could land in production
overnight. Replacing it with a manual, reviewed refresh cost freshness and bought the ability to
see what changed.