FXpress

AI Co-Pilot for Cross-Border Remittance

I designed an AI product that isn't allowed to predict anything. It shows you where today's exchange rate sits against the last 30 days, says it in plain language, and refuses to guess what happens next. Deployed and running on real rates.

Role

Product Design

Tools

Claude Haiku 4.5 · Cloudflare Workers · Frankfurter API · Netlify

Type

Self-initiated

Status

0 → 1

Timeline

Apr - Jul 2026

fxpress

The Problem

I spent fifteen years in institutional finance and I send money to Korea monthly, so I know this from both sides. A payment app shows 1,540 KRW to the dollar — and I have no way to tell if that's a good rate. The apps compete on speed and cost, not timing. Senders aren't short on rate information; they're short on a way to judge, in the moment, whether today is a good day to send money abroad.

13 months of USD→KRW. Best and worst day ran ~5% apart — and no app shows a sender where today sits.

The Challenge

How might I show a sender where today’s rate sits — honestly enough to earn trust — without ever pretending to know where it’s headed?

Research

I interviewed four people who send money abroad regularly. Small sample — directional, not conclusive — but the patterns held, and I was building for a problem I share with them.


Participants:

  • Marketing coordinator — monthly to Manila

  • Software engineer — to parents in Seoul (USD→KRW)

  • Warehouse supervisor — to family in El Salvador

  • Freelance designer — multi-corridor, LATAM & Africa

Two findings decided the architecture.


Recipient-amount certainty is non-negotiable. Everyone wanted the exact amount their recipient gets before sending — the real number, not an estimate.


Trust is visibility plus control. Same framing every time: show me the reasoning, and let me stop it at any moment.

"I'd need to see what the rate is, what my family's getting, and how long it'll take."

Perry — Warehouse Supervisor, Dallas TX

Exploration

I mapped five ways to solve the timing problem. Three were more ambitious than what I shipped — and each broke the rule the product exists to keep:

Forecast dashboard — the one everyone would ask for. I couldn't build it without predicting the rate, and nobody can do that honestly. Cut.


Auto-router — moving money makes you a licensed transmitter. A legal wall, not a design problem. Cut.


Chat assistant — a monthly send is a 30-second task. Typing a question is slower than reading the answer already on screen. Cut.


Threshold alerts — too thin alone, but useful. It shipped as Set Trigger.

Two of these I cut for reasons that had nothing to do with whether I could build them. I could've shipped a forecast in a weekend — it would've been confidently wrong in the direction that costs a sender money. Knowing which constraints are real was the judgment here.

Design Decisions

Code computes. Fexi narrates. The user authorizes.


Three decisions define the product. Each one gives the user less certainty than they asked for — because the honest version was worth more than the complete-looking one.

The AI narrates, it never decides. Meet Fexi. It reads the rate, the 30-day range, and the recipient amount, and says in plain language where today sits. It can't compute, forecast, or tell you whether to send — the system prompt forbids all three. It gets a finished conclusion as text and phrases it; it never sees the raw data. Nothing for it to be wrong about.

Fexi, Live AI state. The face is a status indicator, not decoration.

The fee I removed rather than invent. Everyone named the fee as part of certainty. But FXpress hands off to your provider — it has no fee of its own, and no view of theirs. The hi-fi showed a fee line; the shipped version doesn't, because the only way to fill it was to make one up. The canvas states the payout and names the gap in words: before your provider's fee.

Recipient certainty before authorization. The amount your recipient gets, in their currency at today's rate, is the biggest thing on the canvas — visible before the send button. Every participant named this, unprompted, as the precondition for trust.

How It Works

The app runs in two layers that never cross:


The deterministic engine (JavaScript, no AI) fetches the live rate and 30 days of central-bank history, counts how many days closed below today, and does the recipient math. Every number is real and survives if the AI drops.


The language engine (Claude Haiku 4.5, on a Cloudflare Worker) takes that finished signal as text and phrases it. It can't invent or forecast, because it never sees the raw data — and the API key lives only on the Worker. About 51 output tokens per call: a narrator can't fit market analysis into two sentences, by design.

The signal is finished before Claude sees it.

Four states, one status. If the Worker drops, a templated voice takes over and a badge names who's speaking. The widget never breaks.

The judgment is also code. Where today sits in the range is decided in JavaScript before Fexi sees it. So Fexi can't reach a different answer than the code — and the trust guarantee holds even if the model misbehaves.

States & Send Flow

What Fexi says, and what you can do about it

The range read has three positions plus two honest edge states. Each is a different sentence and a different chip.

One indicator, three honest reads. Never color alone — each state is a different sentence and a different chip.

What Fexi says nothing.

The honest states are the ones that refuse to answer. With fewer than 10 days of history, Fexi makes no claim about where the rate sits — it says so plainly instead of guessing. If the rate feed lags, it goes quiet and tells you, rather than narrating a stale number. Same discipline as the missing fee: silence beats a confident wrong answer.

The Send Flow. Three actions: Send Now filled, Set Trigger outlined — so they never read as the same choice

Testing & Iteration

Five participants, time-boxed tasks on the live prototype.


What held. Five of five completed the send path. Five of five read the recipient's amount first, across every corridor — ₱11,200 via GCash, 950,000 KRW, 1,350 CAD, M-Pesa. Nobody asked "what will my recipient get?"


What didn't. Four of five completed the trigger path. One misclicked Send Now while setting a trigger — the two CTAs looked identical — then recovered. One stalled at an unlabeled threshold field and asked: "1,380 — is that dollars, won, or shillings?"


What changed. Both were comprehension problems. The CTAs got distinct weights (Send Now filled, Set Trigger outline); the threshold field got a currency label and helper text.

Two of five hesitated when both CTAs carried equal weight. Filling the primary action resolved it.

Outcomes

FXpress is deployed and live across 18 corridors on real exchange-rate data. Every number the user sees was computed in code before the model saw it — and anyone can open the URL and check the math against a free public API. That's more than most case studies can offer, and the point of building instead of mocking.


The research backed the direction. All four described the timing problem, and none rejected the narrate-don't-decide model when they saw it. Directional evidence, not proof of adoption.

The fallback is built and tested but never fired — I report it as designed, not proven. And I can't prove strangers used this: cookie-free analytics I chose specifically so I couldn't track anyone. No funnel worth publishing, and I'd rather say so than dress up a number that's mostly me.

What's Real, What's Out of Scope

LAYER

REAL

OUT OF SCOPE

FX Rates

30-day ∙ Frankfurter-API

--

Conversion Math

Deterministic JavaScript

--

AI Narration

Claude Haiku 4.5 · Cloudflare Worker

.

Triggers + Status

Status vs. Live Rate

--

Provider Hand-off

8 deep-links · Wise, Xoom + 6

--

Provider Fee

-

Not shown on purpose

Usage Analytics

Umami

--

Reflection

The research got the direction right — people struggle with timing, and they need the exact recipient amount. Both held. But the decision that mattered most didn't come from interviews. It came from running the thing on live rates for two months. You can't interview your way to a problem that only exists in real market data.


The build corrected the design twice. The first version compared today's rate to what the user saw earlier that session — so on a first visit it had nothing, and recommended sending anyway. I replaced it with a rolling 30-day window.


That exposed a deeper problem, and only live data could've surfaced it. The indicator tells you where today sits against the past 30 days. That only means something in a market that moves within a range. USD/KRW didn't — it hit a 15-year low in June 2026, then fell for two straight months.

44 trading days. The rate fell 10.8%; the indicator had one answer the whole time.

Worse — misleading. "Bottom of the range" sounds like a bargain, so a sender reads it as good rate, lock it in. But bottom means the fewest won per dollar in a month — the worst recent day, not the best. The label points a lay user the wrong way. I know, because I built it and it still caught me.


The widget now states the position and says plainly it doesn't imply the rate will bounce back. That's a copy patch on a structural problem. The real fix is a trend indicator — "low, and still falling."


What I'd carry forward is where the AI sits. The band was computed in code, so when it went uninformative, it did so in the logs — where I could count it. A forecasting model would've produced confident sentences and no record. Keeping the AI out of the math didn't just limit what it could get wrong. It's what made the failure legible.