KettlePay App

KettlePay App — Home screen on a device

Design by

Kyoung Hoon Kim

Role

Founding Design Engineer

Duration

2025 - Present

What is the KettlePay App?

Overview

KettlePay is a settlement app for high-volume watch and diamond dealers. These are people closing six-figure deals who’ve been de-risked by their banks and burned by crypto’s sharp edges. As founding design engineer I owned it end to end: defining who the user is, the information architecture, every interaction and line of copy, the motion, and shipping the React Native that runs it. Through its early months it compounded at roughly 300% month over month, and it now moves $300M+ a month.

I wrote down the user before I drew a screen

Where it started

Before any of it, the first artifact wasn’t a flow but a document describing exactly who this is for. Most users are 20-to-60-year-old dealers who have never used a fintech app, let alone a crypto one. Two of them sit at every table: one is comfortable with crypto but lives in fear of a frozen wallet; the other is fiat-native, has been pushed out of traditional banking for being a dealer, and distrusts crypto and us in equal measure. The one thing they share: they will not learn crypto vocabulary. That pre-decided almost every call that followed.

A pipe, not a wallet

The bet

Design for those two and the obvious build is a crypto Venmo. But person-to-person transfers are a rounding error here; the real job is money in, money out: a two-port pipe. So KettlePay isn’t a wallet you live inside, it’s a settlement tool you open to close a deal and leave. Balances are held as dollars, the chain stays invisible, and the whole app orbits the in-and-out flows instead of a social feed.

Crypto does the work, never shows its face

The hard part

Keeping the chain invisible is a hundred small refusals. Dollars, not tokens. “Bitcoin”, not “BTC”. No gas, no seed-phrase theatre, no wallet addresses standing in for people where it can be avoided. And crypto’s reality does leak in places: a cross-chain send reads “Sent” the instant it leaves but takes minutes to actually arrive, so the interface tells the truth about delivery instead of pretending it’s instant.

Three tabs, and everything else is a flow

Architecture

The app is three tabs and nothing more: Home, Activity, Me. The money actions aren’t destinations you navigate to; they’re full-screen flows that launch from Home and drop the tab bar so you always know you’re mid-action. Activity is the ledger and the in/out chart. Keeping the surface this small is what lets a first-time user trust it.

Trust is the product

Designing for the anxious first-timer

That first-time trust has a face. Persona B watches their first six-figure transfer like a hawk, so the app is built to be trusted at a glance: it should feel like an account, not a wallet; there are no surprise states; the balance is always legible; and there’s always a plain-English read on what just happened and what comes next. For this user, confidence, not features, is what makes them stay.

Learn the app once

One interaction grammar

Confidence like that is built by repetition. Every money flow shares one grammar: the same keypad, the same confirm, the same success screen. Send, receive, withdraw, all the same shape. Action screens drop the tab bar so the mode is unmistakable. Send to someone new and it asks, once and reversibly, whether to save them; nothing is filed away silently. And numbers are sized so a seven-figure balance never overflows.

Send

Crypto out, to a saved counterparty or a pasted address.

Receive

A QR and address; whatever lands is converted to USD on arrival.

Withdraw

The USD balance back out to a linked bank account.

Pull-to-refresh: a rendering problem, not an animation one

Engineering deep-dive

Underneath that one grammar, the engineering isn’t always where you’d expect. Matching Home’s branded pull-to-refresh on Activity looked trivial and wasn’t: the problem was what I was animating. Three tries to get it right:

Attempt 1 · The naive build

Animate the gap open

To open the ~52px gap I animated the height of a spacer at the top of the list. Every frame that changed the scroll view’s contentSize, so React Native re-ran Yoga layout across the whole list on the JS thread, and the indicator was a state-driven component re-rendering alongside it. JS collapsed to ~31fps and the pull visibly stuttered.

Attempt 2 · Off layout, off the JS thread

translateY, not height

I replaced the height animation with a translateY transform on an overflow-hidden wrapper, GPU-composited, so contentSize never changes and nothing re-lays-out. The gesture and the arrow→spinner morph run in a Reanimated worklet off shared values, so the indicator never re-renders. Then React.memo on the row plus a stable keyExtractor/renderItem stopped all ~60 rows re-rendering when the refreshing flag flips. JS holds 120fps; only the final data-commit blips.

Attempt 3 · Shipped

One hook, one grammar

Converged into a single usePullRefresh hook + PullIndicator, shared by Home and Activity: a 52px reveal, a trigger threshold at 7% of screen height, 280ms timing, a haptic latch when you cross it, a re-entrancy guard, and a minimum spinner duration so it never flashes. The same branded arrow→spinner everywhere.

Attempt 1 · The naive build

Animate the gap open

To open the ~52px gap I animated the height of a spacer at the top of the list. Every frame that changed the scroll view’s contentSize, so React Native re-ran Yoga layout across the whole list on the JS thread, and the indicator was a state-driven component re-rendering alongside it. JS collapsed to ~31fps and the pull visibly stuttered.

Attempt 2 · Off layout, off the JS thread

translateY, not height

I replaced the height animation with a translateY transform on an overflow-hidden wrapper, GPU-composited, so contentSize never changes and nothing re-lays-out. The gesture and the arrow→spinner morph run in a Reanimated worklet off shared values, so the indicator never re-renders. Then React.memo on the row plus a stable keyExtractor/renderItem stopped all ~60 rows re-rendering when the refreshing flag flips. JS holds 120fps; only the final data-commit blips.

Attempt 3 · Shipped

One hook, one grammar

Converged into a single usePullRefresh hook + PullIndicator, shared by Home and Activity: a 52px reveal, a trigger threshold at 7% of screen height, 280ms timing, a haptic latch when you cross it, a re-entrancy guard, and a minimum spinner duration so it never flashes. The same branded arrow→spinner everywhere.

UI first, features later

How it was built

That pull-to-refresh story is the whole app in miniature: I built the interface fully before the features filled in behind it, not a pile of features but a UI built whole, with the engineering arriving into a shape that was already proven. Designing the entire surface first surfaced the real grammar and the edge cases early, instead of negotiating the UI screen-by-screen against a backend.

The throughline

Role & philosophy

Founding design engineer means owning the whole line, from userbase strategy and IA to interaction, copy, motion, and the React Native that ships it. The throughline across all of it: make crypto invisible while it’s load-bearing, let the button tell the truth, one concept per screen and one grammar across them, and document the reasoning, not just the verdict.

Works