Inspiration
Every fishing app asks you to trust the angler. Photos get reused, locations get invented, and the "record" is whatever someone typed in. I wanted the opposite: a log where the record itself carries evidence, so a catch is worth something because it can be checked — not because someone promised it was real.
I also wanted to find out how far two AI coding agents could be pushed on one production codebase before they started quietly destroying things. That question shaped the architecture as much as the fishing did.
What it does
You photograph a fish. Gemini identifies the species from a 2,941-species catalogue. You confirm it — nothing is written without a human decision. Before the record lands, the server runs three generations of fraud checks (EXIF consistency, perceptual-hash duplicate detection, and image forensics on what's actually in the frame). Confirmed catches earn Fish Token, an in-app point recorded in a hash-chained, append-only ledger with database triggers that reject UPDATE and DELETE outright.
Catches appear on a world map, blurred by default — Japanese angling culture treats an exact spot as private, and the app respects that. Around this sit a 2,941-species encyclopedia (1,294 of them illustrated twice: a clean plate and an in-habitat scene), tide/weather/sea-state analytics joined to your own history, and the usual social layer.
Two subscription tiers (Premium ¥480/mo, Pro ¥980/mo, 7-day free trial) sell viewing perks only, through the RevenueCat SDK.
How we built it
Flutter + Dart on the client, FastAPI + PostgreSQL/PostGIS on the server, deployed to Cloud Run, with a separate server-rendered admin console kept out of the user app on purpose (a compromised user build should not carry an admin surface).
The unusual part is the process. This is a solo project with one collaborator, and we each drive a different AI agent — Claude Code and ChatGPT Codex — against the same repository. They read different instruction files (CLAUDE.md and AGENTS.md), and a CI test asserts those two files stay consistent with each other, because the first time we duplicated content between them, one copy went stale within days.
On top of that sits a concept we call preservation anchors: 48 explicitly declared invariants ("do not delete this", "do not simplify this", "this must stay append-only"), each one backed by a test. The operating rule is written down: if a test fails, do not fix the test — first suspect that the change broke an anchor.
Challenges we ran into
AI-generated content fails in ways that pass every mechanical check. One batch of 200 illustrations was correct on dimensions, naming, count, and orientation — and 20 of them were the wrong species. Another time, 410 English common names were silently wrong because we'd taken GBIF's alphabetically-first synonym instead of the accepted name, and the AI-written descriptions had already been generated from those wrong names. We audited 53 flatfish illustrations one at a time for which eye faces up.
The lesson that cost the most: green is not done. A build succeeded with no SDK installed and handed us an 11-day-old artifact. A deploy reported success while the secret it needed was unreadable. Code that passed static analysis failed on a physical device. We now require evidence per change type, written into the pull request template.
Shipping from Japan as an individual was its own project: trademark filings, a law-firm retainer, a telecom-business notification question where two firms disagreed (we took the conservative side), and consumer-disclosure obligations that attach the moment a purchase becomes possible.
Accomplishments that we're proud of
- A paywall that refuses to sell the most valuable thing. Exact coordinates are not for sale at any price. Subscription-derived precision is capped in
db.get_viewer_precision, and regression tests fail if anyone widens it. Fish Token and Angler's Point are not for sale either. - Refunds cannot corrupt the ledger. Entitlement state lives entirely in
entitlements.statusandexpires_at; the append-only point ledgers are never touched by billing. An AST-level test enforces that billing code doesn't reach into them. - Server-authoritative receipts. Validation never runs on the client, so an old app build still gets the right entitlements.
- 1,218 localization keys in Japanese and English, zero missing — verified in CI, not by hand.
- ~590 merged pull requests, in public, as one person plus one collaborator.
What we learned
Give an AI agent a list of things it must never delete, and put a test behind each one. Without that, "refactoring" quietly removes attribution notices, safety disclaimers, and the exact mechanisms you built to be trustworthy — and it does it while every check stays green.
The second lesson is about honesty in measurement. When we counted the reach of our own build-in-public posts, every one was in double digits. We wrote that down instead of finding a flattering way to present it, because a project whose entire premise is verifiable records cannot start inflating its own numbers.
What we learned since launch
We released 1.0.0 on Google Play on 2 September and on the App Store on 22 September. The weeks after taught us more than the weeks before.
Launch did not move usage. Weekly active recorders have been zero for five complete weeks. Of 106 accounts, 85 are excluded from our own metrics as throwaway or one-day sign-ups, and none has yet earned enough points for Silver, the first tier above free. We set those thresholds before we had data.
We misread our own log, then corrected it. We first reported that new users were being refused tides. The code showed the home screen requests tides by itself seconds after sign-up, so the log was counting the app, not people. We said so publicly.
Reviewers made the app better. An App Store reviewer could not find the subscription: the only path was four taps deep. There are now four entry points, the nearest one tap from home. A promo-code field was removed on iOS, and an open tester's report of a notice that would not dismiss led to a fix that CI now enforces.
Next: activation before acquisition.
What's next
Version 1.0.0 went live on Google Play on 2 September 2026, after a lawyer-reviewed Terms of Use and Privacy Policy replaced the drafts (every user re-consents once). iOS 1.0 passed App Store review on 22 September 2026 with both subscriptions approved alongside it, so the app is now officially available on both stores — same account, same records, on iPhone and Android. Longer term: opening the verified-catch dataset as a citizen-science resource for marine biodiversity, which is the reason the ledger was made tamper-evident in the first place.
Built With
- dart
- fastapi
- firebase
- flutter
- gemini
- google-cloud-run
- google-maps
- postgis
- postgresql
- python
- revenuecat
- vercel

Log in or sign up for Devpost to join the conversation.