corridor · failure modes, phase 5

Everything that
goes wrong.

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.

corridor USD → TRY chain Base Sepolia verified Aug 2026
disclosure, first

What is not real here

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 → TRY payoutdoes not exist

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.

Bridge → USD on-rampsimulated

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.

TRY payout partnersimulated

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.

Identity verificationreal flow, fixture verdict

Sequencing, retry, reason-code mapping and the resubmit path are production-shaped. The vendor verdict is a fixture. No documents are captured or transmitted.

The walletsimulated

@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.

The tx hashreal · verified

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.

FX rate feedfixed

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.

register

The full list

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.

FailureDetectionUser seesMitigationResidual
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
settlement risk

The 60-day problem

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.

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;
the payout leg

What Türkiye cannot tell you

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.

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.

event delivery

Three clocks, one story

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.

known gaps

What this build does not do

Distinct from the simulated rails above: these are gaps in the artifact itself, not in the design.

GapWhyCost 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
at scale

What breaks first

If this corridor took real volume tomorrow, in the order I would expect things to hurt:

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.