Inspiration
Most people keep a few coins they never spend. The one from a trip you won't repeat, the one a grandparent left you, the one you got as change in a city whose name you no longer quite remember. They end up in a drawer — and a phone photo of them flattens exactly what makes a coin a coin: the relief, the shine of the metal, the edge.
I wanted to give those coins a place that does them justice. Not a price catalogue, but a personal archive: something between a collector's album and a travel journal, where every coin keeps its detail and remembers where it came from.
What it does
Cuño turns a physical coin into a 3D model from two photos.
- Mint a coin. Photograph the obverse, flip the coin, photograph the reverse — or pick both from your photo library. Cuño reconstructs it in 3D in a few minutes on the server, so you can close the app while it works; a push notification tells you when it's ready.
- Keep it in an album. Every coin takes a slot in a private, paged album. Tap one and it lifts into a full-screen viewer where you spin it with a flick and inspect the relief up close.
- Place it on a map. Each coin remembers where it was minted, so over time the map turns into a record of where you've been.
- Credits. Minting a coin costs one credit. New accounts get one free, so the whole flow can be tried without paying; more come as one-time in-app purchases. If a mint fails, its credit is refunded automatically.
It's private by design: no feed, no public profiles, no ads and no tracking, and you can delete your account and all your data from inside the app. It's available in Spanish and English, in light and dark themes, and every animation respects Reduce Motion.
How we built it
The app. Expo SDK 55 on React Native 0.83 with the New Architecture, Expo Router and TypeScript. Motion runs on Reanimated 4 and Gesture Handler, capture on VisionCamera, and the map on react-native-maps.
The 3D. Coins render with three.js on expo-gl, lit by a studio environment prefiltered through PMREM, with the PBR materials tuned so metal reads as metal. A phone can't afford a live WebGL context per album tile, so tiles show a die-cut image generated on the server; coins that predate it render once, snapshot themselves to a cached image and release their GL context, with never more than two live at a time.
The reconstruction. Photos go to Tripo3D's multiview-to-model (H3.1). Before Tripo sees them, a Cloud Function isolates the coin from its background with sharp. The resulting GLB is capped at 40k faces so phones can load it and rehosted to Cloud Storage, and Tripo's preview render is cut out into the die-cut sticker the album shows.
The backend. Firebase Auth (email, Google and Sign in with Apple), Firestore, Storage and Cloud Functions on Node 22. Minting is a job queue: a Firestore trigger claims each job under a lease, a scheduled sweeper picks up any job whose worker died, credits are debited on claim and refunded on failure, and every step is idempotent. Clients can never write their own credit balance — the security rules forbid it.
Payments. RevenueCat handles the in-app purchases. Its webhook lands on an Expo Router API route on EAS Hosting, which authenticates it and writes the event to Firestore, where a trigger grants the credits.
Shipping. EAS Build for the iOS binary, EAS Update for over-the-air fixes with fingerprint-based runtime versions, and an Icon Composer icon for iOS 26's Liquid Glass.
Challenges we ran into
Coins that came back as pebbles. Multiview reconstruction only ever sees a coin's two faces, never its edge, so it has to guess the thickness — and it guessed badly. When I measured real models, nearly half came back far thicker than any real coin, and a few were close to spheres. The faces were reliably round, though, so I fixed it at render time: any model thicker than a real coin's proportions gets flattened along its thin axis. That repaired every coin already sitting in people's albums, too.
A bug that only existed in release builds. Album tiles washed out to pure white in TestFlight while looking perfect in development. A "cheap" lighting environment only ran in release, and its HDR values blew straight through the tone mapping. The fix was deleting the shortcut. The lesson was bigger: never trust a code path I haven't seen running on a real device, in a release build.
Many 3D objects on one screen. A page of live WebGL coins stuttered, and it turned out to be two unrelated problems: a cold path, where lighting bakes and texture work piled up on the first fill, and a warm path, where turning the page was slow because of heavy UI chrome — not WebGL at all. Caching snapshots fixed the first; stripping layers out of the album fixed the second.
Money and concurrency. An early version could, under the right race between the trigger and the sweeper, deliver a coin and refund its credit. Leases with ownership checks closed it: a job can only be failed by the worker that holds it, or once its lease has expired.
A payment webhook with nowhere to land. An organization policy on the Google Cloud project blocked both public Cloud Run endpoints and service-account keys, so neither a public function nor the Admin SDK at the edge was an option. The workaround: the API route authenticates RevenueCat and writes the raw event over REST into a locked-down Firestore collection, tagged with a secret token, and a trigger running inside Google Cloud validates that token before granting any credits.
Accomplishments that we're proud of
- It feels like holding the coin. Spin it and it carries momentum; drag the detail view down and it follows your finger, with momentum projection and rubber-banding modeled on Apple's fluid interfaces.
- Waiting doesn't feel like waiting. While a coin loads, a hand-drawn hammer strikes a coin with a haptic tap on each blow, instead of a spinner.
- An economy that can't be gamed. Credits are server-authoritative, grants are idempotent, and failed mints refund themselves.
- Closing the app loses nothing. A mint keeps running on the server and a push notification brings you back.
- It's a real, shippable app, with in-app purchases, account deletion, a privacy manifest, and privacy labels checked against what the code actually stores.
What we learned
- Test the release build, on a device. The worst bug in the project was invisible everywhere else.
- Perceived speed beats raw speed. The biggest "this feels slow" wins came from what people see while they wait, not from shaving milliseconds.
- Start from the tool's defaults. Turning every reconstruction setting to maximum made coins slower and less faithful. Matching Tripo Studio's defaults and cleaning up the input photos instead did better on both.
- Anything that touches money needs leases and idempotency from day one, not after the first race condition.
- Derive what you can instead of pinning it. A hand-set runtime version once let an over-the-air update target a binary built with different native code. Deriving it from a fingerprint of the native project rules that out.
What's next for Cuño
- Better edges. The thickness fix is a render-time patch. The real fix is giving reconstruction the information it's missing, by capturing the coin's edge as part of the flow.
- Android. The codebase is already cross-platform and the Play Store app is set up in RevenueCat; what's left is a dedicated polish and QA pass.
- See it life-size. Coins are already 3D models, so placing them on your desk in AR is a natural next step.
- More about each coin. Country, year and denomination, so the album becomes an archive you can browse, not just scroll.
Built With
- cloud-firestore
- cloud-functions
- eas
- expo-gl
- expo.io
- firebase
- firebase-authentication
- firebase-storage
- react-native
- react-native-maps
- reanimated
- revenuecat
- sharp
- three.js
- tripo3d
- typescript
- vision-camera
Log in or sign up for Devpost to join the conversation.