Swap, send, receive, buy, sell, trade gift cards, and pay bills — by typing or speaking a sentence. No network dropdowns. No token contract addresses. No "which chain is this again?"
ai.integration.mp4
Crypto wallets are built by people who already understand crypto. A first-time user opening a swap screen faces a network selector, a token selector, a slippage field, a gas estimate, and a warning that a wrong address means permanent loss of funds. Most of them close the app.
Pucket removes the form. The user says what they want, in their own words, and the assistant does the translating — inferring the network, normalizing the token, filling in what's missing, and reading the intent back in plain English before anything is committed.
User: "send 50 tether to 0x71C7…9F3b on bep20" Pucket: "Just confirm, you want to send 50 USDT to 0x71C7…9F3b on BSC, right?" User: "yeah go for it" → Validated, typed action payload dispatched.
Note what happened there: the user said "tether" (→ USDT), said "bep20" (→ Binance Smart Chain, chainId 56), and confirmed with "yeah go for it" rather than "yes". All three are handled.
The assistant is not a chatbot bolted onto a wallet. It is a constrained intent-extraction pipeline whose only permitted output is either (a) a clarifying question, (b) a confirmation sentence in a fixed grammar, or (c) a typed JSON action payload. Everything below was designed and built for this project.
Speech goes through a three-stage pipeline before the model ever sees it:
🎤 expo-av recording (HIGH_QUALITY preset)
↓
OpenAI Whisper (whisper-1, multipart upload, plain-text response)
↓
Domain-aware typo correction ← custom, no dependencies
↓
Chat input, pre-filled and editable
Why the third stage exists: general-purpose ASR has never heard of "BEP20" and reliably mangles crypto vocabulary — "swap" → "swamp", "USDT" → "you SDT", "0x" → "zero X". A wrong token name in a wallet is not a typo, it's a failed transaction.
So the transcript is passed through a Levenshtein-distance corrector built specifically for blockchain vocabulary:
- Word-by-word fuzzy matching against curated dictionaries of actions, token symbols, and chain names
- Similarity scored as
1 − distance / maxLength, with per-dictionary thresholds (0.65 for prose terms, 0.60 for short token symbols where a single character is a larger proportion of the word) - Casing-aware: an all-caps word ≤ 5 characters is preferentially matched against the token-symbol dictionary, so
USDTstays a symbol and doesn't get lowercased into prose - Address phonetics: spoken forms like "zero x", "oh x", "letter o" are rewritten to their literal characters before EVM (
0x+ 40 hex) and Solana (base58) patterns are evaluated - Punctuation is stripped for matching but unmatched words pass through untouched — the corrector is conservative by design and never invents vocabulary
The assistant's behaviour is governed by a ~200-line system prompt that reads less like instructions and more like a specification. Its notable design decisions:
| Rule | Rationale |
|---|---|
| Never emit jargon the user didn't use first — no "ERC20"/"BEP20" unless they said it | Jargon is the #1 abandonment trigger for beginners |
| Aliases collapse to one concept — "BNB Chain" = "Binance Smart Chain" = "BEP20" = "BSC" | Users have inconsistent mental models; the assistant absorbs the inconsistency |
| Native-token auto-routing — BNB→BSC, MATIC→Polygon, SOL→Solana, BTC→Bitcoin | Never ask a question whose answer is already determined |
| Ambiguity-only clarification — ETH (Ethereum/Arbitrum/Base) and stablecoins do prompt for a network | Asking only where it genuinely matters keeps the conversation short |
| Per-action required-parameter sets — swap/send/buy/sell/gift-card/bill-pay each declare their own slots | Turns free-form chat into deterministic slot-filling |
| Receive skips confirmation entirely | Showing an address is non-destructive; a confirmation step there is friction for nothing |
| JSON is forbidden until after explicit confirmation | Structurally prevents the model from triggering an action mid-conversation |
Chain identity is resolved out of the model's hands: a local normalizer maps free text to canonical names and then to numeric chain IDs (Ethereum 1, BSC 56, Polygon 137, Arbitrum 42161, Base 8453; Solana and Bitcoin explicitly null). The LLM proposes; the client decides.
LLMs do not follow output formats perfectly, so intent is captured two independent ways and whichever fires first wins:
- Path A — structured: the response is scanned for an embedded JSON object with a valid
actionfield. Found → the raw JSON is never rendered to the user; it's routed straight into the action handler. - Path B — grammatical: if the response is a confirmation sentence, a per-action regex family parses the parameters back out of the natural-language sentence itself. The confirmation line is the data structure.
Path B is the interesting one. Because the prompt pins the confirmation to an exact grammar per action, a sentence like "Just confirm, you want to swap 1.5 ETH for USDT on Ethereum, right?" is machine-parseable — including addresses, decimal amounts, fiat amounts with currency symbols, and optional country fields for gift-card and bill-pay flows. The app stays in sync with the conversation even when the model narrates instead of emitting JSON.
Real users don't answer "yes". They answer "yeah", "sure do it", "sounds good", "go ahead", "actually wait", "no make it 100 instead".
Rather than keyword-matching that space, a second, separate model call acts as a dedicated confirmation judge — a low-temperature (0.1) classifier given the pending confirmation, the user's reply, and a 4-message context window, constrained to return {"agrees": boolean}. Ambiguity, questions, and mid-flight parameter changes all resolve to disagree, which safely returns control to the main conversation instead of firing an action.
Two models, two jobs: one converses, one adjudicates. The judge is deliberately narrow — it cannot start an action, only permit one.
Confirmed intent is not trusted straight from the model. It's rebuilt locally through a discriminated-union payload builder, one branch per action, with TypeScript variants for swap, send, sell, buy, giftCardTrade, and billPay. Each branch re-normalizes the chain, re-derives the chain ID, and returns null unless every required field for that action is present. A missing slot cannot become a transaction — it becomes a graceful fallback message instead.
Small details, deliberately built:
- Character-by-character typewriter rendering (18 ms/char) with interval cleanup on unmount, so replies feel authored rather than pasted
- Three-dot thinking indicator — staggered
Animated.loopsequences (0/120/240 ms offsets), native-driver, running while the model is in flight - Pulsing ring around the mic during capture, an in-place spinner during Whisper upload, and a disabled state that makes double-submission impossible
- In-chat progress states — an action renders "Swap in progress" with an inline spinner, then resolves in place
- Keyboard-aware footer that tracks real keyboard height per-platform, and auto-scroll on every new message
- Recording is torn down and the temp audio file deleted on unmount, so an interrupted session leaves nothing behind
50 screens · ~20,700 lines of screen code · ~29,000 lines of TypeScript · 90 hand-built SVG icon components · zero UI kit.
Every component is written from scratch on React Native primitives — no NativeBase, no Tamagui, no Gluestack. The visual language is entirely bespoke.
File-based routing via Expo Router 6 with typed routes enabled, organized into route groups that model the app's actual lifecycle rather than its file tree:
app/
├── index · onboarding · unlock-wallet ← cold-start & lock gate
├── (wallet)/ create-seed · confirm-seed · import-wallet
│ setup-security · wallet-address · wallet-choice
├── (auth)/
│ ├── (sign-up)/ lets-get-started · select-id-type · camera
│ │ confirm-photo · kyc-form · verification-success
│ ├── (recovery)/ recovery-intro · recovery-phrase · recovery-verify
│ │ choose-password · backup-complete
│ └── (update)/ update-password
├── (app)/
│ ├── home · chat · exchange · top-up
│ ├── (actions)/ send · receive · buy · sell · transactions
│ │ gift-card · pay-bill (+ electricity/TV/data/airtime/betting)
│ ├── (exchange)/ swap · bridge
│ └── (config)/ account · edit-profile · profile-details · font-size
│ biometric-settings · language · help
└── (process)/ scan-qr-code · top-up-amount · top-up-confirmation · withdraw
Route groups keep URLs clean while letting each phase own its own stack and layout — onboarding can't leak into the authenticated stack, and the wallet-creation flow is isolated from the auth flow.
- Semantic
ColorSchemeinterface (16 tokens:primary,card,textSecondary,tabIconSelected,success/error/warning, …) rather than raw hex scattered through components - Light and dark palettes defined side by side from shared brand constants, with dark-mode-specific tuning — success/error/warning are brightened independently for contrast on dark surfaces, rather than reused
- Three-state preference —
light/dark/system— persisted toAsyncStorageand reconciled against the live OS color scheme, so "system" stays genuinely reactive - Bridged into React Navigation's own theme provider so native navigation chrome matches, with a keyed remount to force a clean re-theme
- Platform-aware typography stack (SF Pro on iOS, Roboto on Android, Inter with system fallbacks on web)
The requirement: a user-adjustable text size (0.8×–1.4×) that scales every piece of text in a 50-screen app, including hundreds of StyleSheet.create styles with hard-coded fontSize values.
Why the easy answers fail: Text.defaultProps only affects text with no explicit fontSize — it's overridden by every styles.title. Threading a scaleFontSize() call through ~29k lines is unmaintainable and one missed call is a visible bug.
The solution: an accessibility layer that intercepts element creation itself. At module scope, before any screen mounts, the app wraps all three React element-creation paths — the legacy React.createElement, the modern react/jsx-runtime (jsx/jsxs), and react/jsx-dev-runtime (jsxDEV) — so it works identically under dev and production transforms.
Each wrapper intercepts only Text and TextInput, flattens the incoming style (resolving StyleSheet registry IDs and nested arrays into a plain object), multiplies any numeric fontSize by the current scale, and forwards everything else untouched. The scale is read from a global at render time rather than captured in a closure, so changing it re-scales immediately without stale values. Idempotency flags on each wrapper prevent double-wrapping under Fast Refresh.
The result: one setting scales the entire app, and no screen contains a single line of code aware that font scaling exists.
- Swipeable token cards on the home screen, built directly on
PanResponder— gesture-driven reveal actions without a gesture library - Pull-to-refresh portfolio sync with themed
RefreshControl - Custom SVG donut chart for portfolio allocation, with a real aggregation model: percentage-of-total computed per holding, sorted by value, and positions under 5% automatically collapsed into a labelled "Others" slice so the chart stays readable regardless of how many tokens are held
- Custom numeric keypads on swap, bridge, exchange, and withdraw — no OS keyboard, with decimal-point guarding, backspace, and Min/Max shortcuts
- Full-screen modals for token selection, transaction preview, and slippage tolerance configuration
LinearGradientused as a consistent surface treatment across 20+ screens- Live-computed conversion previews with minimum-received-after-slippage math, plus balance and non-zero validation before any preview can open
- 6-digit PIN modal — a reusable, promise-shaped component (
onSuccess/onCancel) that verifies against the wallet's stored PIN and can gate any sensitive action - BIP39 seed-phrase flow — generation, a confirm-the-phrase quiz screen, and separate import paths for mnemonic and raw private key
- Biometric settings screen backed by
expo-local-authentication(Face ID / Touch ID) - QR everywhere —
react-native-qrcode-svgfor receive addresses,expo-camerafor scanning,expo-clipboardfor copy-to-clipboard - Wallet lock gate on cold start
A complete document-verification funnel: ID-type selection → in-app camera capture → photo confirmation with retake → structured KYC form → success state. Image capture runs through expo-image-picker and expo-image-manipulator for compression before upload.
The app-wide KYCOverlay component renders distinct states for not_started / pending / approved / rejected — including surfacing the backend's rejection reason — and gates protected surfaces behind verification. A companion polling hook checks status on a 30-second interval and self-terminates on any terminal state, with cleanup on unmount so no interval leaks between screens.
Font scaling (above), a language-selection screen with country flag icons (US / UK / Canada / Nigeria), and localized bill-pay and gift-card providers per market.
flowchart TB
subgraph UI["React Native · Expo Router"]
A["🎤 Voice input"]
B["⌨️ Text input"]
C["50 screens · 90 SVG icons"]
end
subgraph AI["AI orchestration"]
D["Whisper STT"]
E["Levenshtein<br/>typo correction"]
F["GPT-4o-mini<br/>wallet assistant"]
G["Confirmation judge<br/>(second model)"]
H["JSON extractor +<br/>grammar parser"]
I["Typed payload builder<br/>& validation gate"]
end
subgraph Core["Client core"]
J["WalletManager<br/>BIP39 · EVM · Solana"]
K["Theme / FontSize<br/>contexts"]
L["AsyncStorage<br/>persistence"]
end
subgraph BE["Node.js WebSocket backend"]
M["Auth · Biometric"]
N["KYC pipeline"]
O["Wallet sync"]
P["OTP / password reset"]
end
A --> D --> E --> B
B --> F --> H --> I
F -.pending confirmation.-> G --> I
I --> J
C --> K
J --> L
J <-->|"WSS"| O
C <-->|"WSS"| M & N & P
Design principle throughout: the model is an interface, not an authority. It interprets language and asks good questions. It never decides a chain ID, never bypasses a confirmation, and never produces a payload the client hasn't independently validated.
| Layer | Technology |
|---|---|
| Framework | React Native 0.81 · React 19 · Expo SDK 54 (New Architecture, React Compiler) |
| Language | TypeScript 5.9, strict: true |
| Navigation | Expo Router 6 (file-based, typed routes) · React Navigation 7 |
| AI | OpenAI GPT-4o-mini (assistant + confirmation judge) · Whisper whisper-1 |
| Voice | expo-av recording · custom Levenshtein correction layer |
| Graphics | react-native-svg · expo-linear-gradient · 90 bespoke icon components |
| Animation | React Native Animated (native driver) · Reanimated 4 |
| Crypto | BIP39 mnemonics · expo-crypto · EVM + Solana address derivation |
| Security | expo-local-authentication (Face ID / Touch ID) · 6-digit PIN · encrypted local storage |
| Device | expo-camera · expo-image-picker · expo-image-manipulator · expo-clipboard |
| State | React Context + hooks · AsyncStorage |
| Realtime | Custom WebSocket client — auto-reconnect with backoff, offline message queue, availability detection |
| Backend | Node.js WebSocket server (ws) · JWT · OTP email · 23 message types |
| Tooling | ESLint (eslint-config-expo) · path aliases · typed routes |
Chains — Ethereum · BSC · Polygon · Arbitrum · Base · Solana · Bitcoin
Actions — Swap · Send · Receive · Buy · Sell · Bridge · Gift-card trade · Bill pay (electricity, TV, data, airtime, betting) · Top-up · Withdraw
Platforms — iOS · Android · Web (via react-native-web)