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 — 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 — it now moves $300M+ a month.

I wrote down the user before I drew a screen

Where it started

The first artifact wasn’t a flow — it was 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 — pre-decided almost every call that followed.

Image placeholderPersona comparison — A vs BPersona A: crypto-comfortable, settles in stablecoins, biggest fear is a frozen wallet. Persona B: fiat-native, de-risked from traditional banks for being a dealer, distrusts crypto and us. Shared truth: won't learn crypto vocabulary.

A pipe, not a wallet

The bet

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.

Image placeholderThe two-port pipeMoney in (deposit, receive) → a single dollar balance → money out (send, withdraw). Person-to-person transfer is negligible. Show the in → balance → out flow, not a social feed.

Crypto does the work, never shows its face

The hard part

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 where crypto’s reality genuinely leaks — a cross-chain send that reads “Sent” the instant it leaves but takes minutes to actually arrive — 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 — Home, Activity, Me — and nothing more. 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.

Image placeholderInformation architectureThree tabs: Home, Activity, Me. The four money actions (Send, Receive, Deposit, Withdraw) launch from Home as full-screen modals that hide the tab bar. Activity = transaction history + in/out chart. Diagram the tab structure and how the action flows branch off Home.

Trust is the product

Designing for the anxious first-timer

Persona B is watching 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

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.

Image placeholderUser-flow graph — SendPick a counterparty (saved, or paste/scan an address) → choose network + token → enter the amount on the shared keypad → confirm → success. A new counterparty triggers a one-time, reversible 'save this person?' branch. Graph the steps and that branch.

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

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

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 — userbase strategy, IA, interaction, copy, motion, and the React Native that ships it. The throughline: 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