And the one thing no design can fix.
A remittance corridor is a chain of four systems that fail independently, on different timescales, and mostly after the user has stopped looking. This document names each failure, says how Corridor detects it, what the user sees, and, where the honest answer is "nothing", says that instead.
It opens with what is simulated in this build, because a writeup that buried that would have already failed its own test.
Four of the five rails in this build are simulated. Each is disclosed on-screen at the moment it matters, and the badge text is read from the provider registry rather than written into the screens, so a screen cannot claim a leg is real while a fixture is bound.
Bridge publishes USD (ACH/FedNow/Wire), EUR (SEPA), MXN (SPEI), BRL (Pix), GBP (FPS) and COP. There is no TRY route in either direction. This is not a KYB gate that could be opened; the product is not sold. Phases 1 and 2 named Bridge as the TRY payout provider and were simply wrong; phase 3 caught it against the live route table.
A real Bridge product, but sandbox access is not self-serve: a Developer account must be
granted by Bridge support before an sk-test key can be generated. Modelled on
Bridge's documented transfer shape and real ACH timing.
No USDC→TRY bank settlement is available self-serve. The adapter is written against a documented partner's API shape, owns the IBAN validation, and is bound per corridor so a real partner replaces it in one file.
Sequencing, retry, reason-code mapping and the resubmit path are production-shaped. The vendor verdict is a fixture. No documents are captured or transmitted.
@privy-io/expo requires native modules and therefore a development build, so the
in-app wallet is a fixture and the repo stays runnable with npx expo start. The
real Privy adapter binds in the same slot.
Discharged. 0x551c···ba10 is a genuine ERC-20 transfer of 1.00 USDC on
Base Sepolia, block 45038683, against Circle's canonical contract, produced by
scripts/proof.mts and checkable on BaseScan without trusting this repo.
The amount differs from the app's 500.00 demo figures because Circle's testnet faucet
caps it; the receipt screen states that on the screen itself.
Pinned at 52.84 TRY/USD. The rate machinery is real (rationals, signed
quotes, server-side expiry), but no live feed is attached, so rate-movement failures are
modelled rather than observed.
the honest summary
No money moves in this build, and no blockchain transaction exists. What is real is the design, the domain logic, the provider seam, and the failure handling, all of which are exercised on every run because the simulated adapters go through the same ingress, signature check and state fold that a real vendor would.
Every failure mode the corridor produces, with what the user sees and what residual risk remains after mitigation. Residual is the column that matters: a register where everything is fully mitigated is a register that has not been thought about.
| Failure | Detection | User sees | Mitigation | Residual |
|---|---|---|---|---|
ACH return R01/R08 |
Bridge webhook, ~2 banking days | Transfer parks at funding, then refunds |
Payout held until ACH settles (funded_held) |
none |
ACH return R10 unauthorised |
Bridge webhook, up to 60 days | Nothing; lira already delivered | Priced into fee; velocity limits until settled history | unclosable |
| Rate lock expires mid-flow | Server clock at submit | 11a · old vs new rate, delta in lira | Explicit re-accept, fresh lock, never silent reprice | none |
| Payout name mismatch | Partner webhook, minutes to hours | 11c · "Your money is safe", funds returned | Warned at entry; USDC returns to balance; fee refunded | 2-3 day delay |
| IBAN typo / transposition | Local mod-97, on keystroke | Inline rejection before any network call | ISO 7064 + TR length + bank-code echo | none |
| Valid IBAN, wrong person | Not detectable; no TR name-check service | 11c after the fact | Cannot prevent; made cheap and reversible | unclosable |
| Chain reorg on settlement leg | Confirmation count regresses | Step stays live, never regresses visually |
Resubmit under stable {id}:chain idempotency key |
low on Base |
| Webhook dropped | Reconciler backoff poll | Step expectation copy, then stall escalation | Push is fast path, polling is the floor | latency only |
| Webhook duplicated / out of order | seq assigned on ingress |
Nothing; fold is idempotent | Sort by seq, drop duplicates, monotonic steps | none |
| Unknown vendor event type | parseWebhook returns null |
Nothing | Logged and dropped; ingress stays total | none |
| Quote tampered client-side | Signature verify at submit | Submission refused | Server-signed quote; client submits quoteId only |
none |
| Double submit on flaky network | Idempotency key minted at screen mount | Second attempt returns the first result | Per-leg keys, stable across process restart | none |
| KYC provider outage | Session creation fails | Verification unavailable, transfer parks | Hard stop. No bypass, no degraded mode | funnel loss |
| KYC document rejected | Vendor reasonCode |
11b · scoped, specific, retake one item | Ledger of passed items preserved; quote parked | none |
| Turkish bank holiday / closed | Partner queues payout | Step expectation, not an error | Delivered means credited, never merely submitted | delay |
| Sanctions / compliance hold | Screening at recipient add and submit | Transfer blocked, support route | Fail closed | false positives |
This is the failure that shaped the product. Everything else on this page is engineering; this one is a balance sheet wearing an engineering costume.
R01 insufficient funds and R08 stop payment surface within about two banking days. Waiting for settlement before releasing the payout eliminates both, which is exactly why screen 09 shows "USD received" as a discrete, timestamped step rather than hiding a two-day wait inside a spinner.R10. An unauthorised-debit return can arrive up to 60 days later. By then the recipient has spent the lira. No holding period a customer would tolerate closes this window.stated plainly
Anyone operating this for real needs a funded reserve against R10 returns and a written policy
for the clawback state. The state machine models the event; it does not pay for
it. A design document that implied otherwise would be lying about the only number that could
close the business.
// The distinction the fold has to make, and why `clawback` exists. // An ACH return before value leaves is a clean refund. // The same return after payout is a loss event. case 'onramp_returned': achReturn = e.code; state = state === 'funding' ? 'refunded' : 'clawback'; break;
Local IBAN validation catches typos, transpositions, wrong-country pastes and bad lengths, all before a network call, on every keystroke. It cannot catch a structurally perfect IBAN belonging to the wrong person, and Türkiye has no name-check service to resolve that.
verified and default to them; make the returned-payout path non-catastrophic.the general rule this is an instance of
When you cannot prevent a failure, make it cheap. Most of the design work on this corridor went into the second half of that sentence.
Bridge, the payout partner and the chain all report independently. They will deliver late, twice, and out of order. None of that is exceptional; a design that only handles the happy path stalls silently in production.
seq is assigned by Corridor on ingress, never taken from the provider. A vendor's ordering guarantees are that vendor's business.seq; reordering is corrected by sorting before folding. Both are covered by tests.parseWebhook returns null rather than throwing on unknown or badly-signed payloads. A provider shipping a feature you have not heard of should never be an outage.Distinct from the simulated rails above: these are gaps in the artifact itself, not in the design.
| Gap | Why | Cost to close |
|---|---|---|
| Artifact amount ≠ demo amount | The on-chain transaction is 1.00 USDC; the app's demo figures are 500.00. Circle's testnet faucet sets the ceiling, not the design. Same contract, same decimals, same signing path, different amount. Stated on the receipt screen itself, not only here, because a receipt implying a 500.00 settlement over a 1.00 transaction would be the exact failure this document exists to avoid. | Accepted and disclosed |
| Wallet does not sign in-app |
The verified transfer was produced by scripts/proof.mts using the same
contract and chain the app targets, not by tapping through the app itself, because
wiring Privy would force every reviewer through a development build.
The transaction is real; the path to it was a script.
|
Privy adapter + EAS dev build |
| Privy not wired | @privy-io/expo requires native modules and therefore a development build; a reviewer could not npx expo start the repo. The wallet sits behind WalletProvider so the real adapter binds in the same slot. |
One adapter + an EAS dev build |
| No web target | Privy's Expo SDK is iOS/Android only. There is no browser fallback demo, so the app cannot be linked as a live URL the way the documents can. | Not closable with this stack |
| Six screens visually unverified | 04, 06, 08, 09, 10 and the edge states need in-memory flow state that a deep link cannot construct, and the simulator could not be tapped programmatically from the build environment. | One manual pass, or accessibility permissions |
| Brand fonts not bundled | Bricolage Grotesque, Inter and IBM Plex Mono are specified in theme/type.ts but the app renders with system faces. The type scale and hierarchy are correct; the faces are not. |
expo-font + three families |
| Quote signing is not HMAC | services/signing.ts is a keyed FNV-1a standing in for HMAC-SHA256, labelled as such. The shape is right (signed at issue, verified before a transfer is created), but it is not a security boundary. |
One dependency, server-side |
| No persistence | State is in-memory and resets on reload. The ledger is modelled correctly as an event log; it is simply not written anywhere. | Any store that appends |
If this corridor took real volume tomorrow, in the order I would expect things to hurt:
PayoutProvider seam makes a second one cheap to add, which is most of the argument for having built it.what this document is claiming
Not that Corridor handles all of the above. It is a case study, and it moves no money. The claim
is narrower and, I think, more useful: that the failures were enumerated before the happy path
was designed, and that the design visibly changed because of them: funded_held
exists because of ACH returns, the PayoutProvider seam exists because Bridge has no
TRY route, and screen 11c leads with the location of the money because that is what a frightened
user needs first.