PayFX
Cross-border contractor payouts
Why I built this
I designed the timesheet side of a time-tracking product, where approved hours get exported for payroll and then leave the system. I wanted to see what happens once those hours become money, so I designed the next step: the run that pays them across six countries.
The finding that shaped everything: the money isn't lost. It arrives fast and then sits inside the receiving bank. Nobody tells the worker that.
Concept project. No client, no users, no shipped code.
What I found
Operator side. Failed payments often come from bad recipient data, so the checks belong before release rather than after a bounce. Exchange rates need capturing per payout with the rate stored against it. And an approver has to be able to hold one item without stalling the whole batch.
Worker side. Public reviews of Deel, Remote, Papaya, Wise and Payoneer. The complaint isn't that the product is clunky, it's "I can't get my money." A payout marked sent that sits in compliance for a week, with support that goes quiet exactly when a paycheck is on the line.
The most useful thing was in the payment rail itself. Every SWIFT payment carries a UETR, a tracking reference, and banks report status against it at each hop. SWIFT's own figures from October 2024: 90% of payments reach the destination bank within an hour, only 43% reach the end customer's account within an hour.
So the money isn't lost in transit. It arrived quickly and is sitting inside the receiving bank. Nobody tells the worker that, which is why six days feels like it vanished. That gap is what the design is about.
Operator side: releasing the run
Local amount and sending amount sit side by side because those are two different numbers to two different people. Totals break down by currency before the grand total, since FX exposure is per corridor.
The rates split into two groups. EUR and PLN are locked with a countdown. NPR, PHP, BRL and KES are marked indicative, because restricted currencies can't be locked offshore and convert at the local bank's rate on settlement day. Promising a rate you can't hold is how a payout becomes a support ticket.
One row is flagged: approved hours exceed the contract cap.
Opening it shows where the hours came from week by week, what the overage costs, and three ways out. Pay capped, pay in full with a logged reason, or hold this one and pay the other eleven on schedule. That third option exists because an exception should never stall a batch.
Confirm answers what rate, locked when, expiring when, and which corridors it actually holds for, alongside what each corridor sends and what the person receives. Below that are the checks that have to pass: bank details validated per country format, tax forms on file, sanctions screening timestamped, and the one payout that differs from its contract.
Approval and release are separate. Nothing leaves the account until a second person releases it.
After release the statuses are the ones the rail reports: in transit, under review, paid. Bea shows as capped rather than flagged, because a run-level exception and a payment status are different things.
Worker side: where the money actually is
An app for contractors is the wrong answer. Someone paid monthly won't install one, and asking them to create an account to find out where their money is adds friction at the worst possible moment.
So the mechanism is email. On release each contractor gets the amount in their currency, the rate used, and the date it should land. For Bea it has to explain why she's receiving the equivalent of $4,480 against a $5,502 invoice.
But email stops working exactly where the reviews say it does. A release email is a snapshot taken at 14:31. The hold starts on Thursday.
Employees paid through an EOR already have a login with their employer, so the live view doesn't need to be a new product. It goes inside the HRIS they already use. The host is drawn as a wireframe here. Only the Pay module is designed, because only the Pay module is PayFX.
No stage ever says only "processing". Every stage names what is happening, who is acting, and whether the person needs to do anything, including "nothing, this is normal". The stage that matters most is the split between arrived at your bank and your bank is crediting it, because that is where the six days actually go.
Where the currency is restricted the amount reads "you'll receive about", not an exact figure. Wise does the same: when a rate isn't guaranteed, they won't let you name an exact recipient amount, because nobody yet knows it.
The held state is the one I'd point at. The rail has a code for documents needed. It has no code for which documents. My first version of this screen listed three the bank supposedly wanted, which was a confident wrong answer that would send someone to their bank asking for the wrong paper. So the screen separates what we know from what we don't: the money reached BPI at 09:04, the bank asked for a document, it hasn't said which one. Then what we're doing about it, with a case reference and an automatic escalation if the bank hasn't replied in two days. Then likely documents, labelled as not confirmed.
Contractors invoicing directly have no HRIS account, and they're the population most affected by held payouts. For them the fallback is a tokenised link in the email. Same screen, two entry points. That's what I'd design next.
The system
Tokens plus five components: Status, Button, Table cell, Payout row, Sidebar, on top of an icon set. The line I drew was between what a component decides and what the person placing it decides. Anything that changes its shape is built in so it stays consistent wherever it appears. Anything that changes its content stays open so one component covers many uses. An icon-only button is not a second component, it is the same one with the label switched off. Components are assembled from each other rather than redrawn, so the table cell's action column holds a Button instead of its own chevron.
Nothing here has been in front of a user. The three things I'd test first: whether operators scan a run by currency or by person, since the totals row assumes currency; whether a cap breach is the exception that matters most or just the easiest to draw; and whether showing an estimated received amount creates more support tickets than it prevents.
See it in Figma
Walk the operator flow in the clickable prototype.
Other projects