Join beta
protocol architecture

Middn Protocol Architecture.

A technical view of how Middn can protect peer-to-peer settlement. Vault-backed escrow, contract-gated instructions, and 2/3 Safe fallback explained layer by layer.

PROMISE Real liquidity. Reserved before payment.
PROMISE Relayers can sponsor gas. Middn does not own funds.
PROMISE The relayer executes; it does not decide.

Three layers, one boundary.

App orchestrates UX. Backend indexes & relays. Contracts hold the truth and the funds. The Safe arbitrates when humans must decide.

indexed events / data relayer transactions Safe arbitration

Available reserved.

A seller's vault holds two distinct balances. Available liquidity backs new offers and can be withdrawn at any time. Reserved liquidity is locked against an open trade and cannot be moved by the seller until the trade resolves.

availableBalance
7,500USDT
withdrawable · backs new offers
reservedBalance
2,500USDT
locked in 3 open deals

When a buyer opens a deal, reserveLiquidity() moves the trade amount from available to reserved. Only then can the app reveal payment instructions to the buyer.

Fee and gas reserve accounting.

The design separates the trade amount from protocol fees and any relayer reserve, so settlement accounting stays explicit instead of hidden inside the UI.

Worked example · 500 USDT trade
seller pays fee
Trade amountUSDT delivered to buyer
500.00 USDT
Middn platform fee0.4% · accrues to treasury
+ 2.00 USDT
Gas reservereimburses relayer for tx
+ 1.00 USDT
Total locked in escrow
503.00 USDT
mode 01 · seller pays

Seller funds amount + fee + gas

Seller deposits the trade amount, the platform fee, and the gas reserve. Buyer receives the full advertised amount. Cleanest UX for buyers.

mode 02 · buyer pays

Fee deducted from payout

Seller deposits amount + gas reserve only. Middn fee is deducted from the buyer's payout at release. Lower upfront capital requirement.

The architecture can support either mode; the chosen settlement model is encoded per offer instead of being implied by the front-end.

The relayed UX path

Middn can sponsor gas via a relayer wallet so users don't need native ETH/ARB/MATIC for every action. The user (or the trade rules) still authorize the action — the relayer only submits transactions.

The gas reserve refunds the relayer wallet after each settled trade. Unused gas reserve can be refunded to the seller per trade config.

Middn pays gas. Middn does not own funds.

Core functions. What lives where.

Filter by category. Each function is tagged on-chain or off-chain — anything that moves funds is always on-chain.

fundVault(token, amount) on-chain
Seller deposits liquidity into their personal vault. Increases availableBalance[token].
withdrawAvailable(token, amount) on-chain
Seller pulls funds back to their wallet — only from the unreserved portion. Reserved funds cannot be withdrawn.
availableBalance(seller, token) view
Public view of unreserved liquidity. Anyone can verify backing before taking an offer.
reservedBalance(seller, token) view
Public view of liquidity locked in active deals across all of the seller's open positions.
createOffer(params) on-chain
Seller publishes an offer with price, payment method, and trade rules. Must reference a vault with sufficient availableBalance.
openDeal(offerId, buyer, amount) on-chain
Buyer commits. The contract creates an escrow position and immediately calls reserveLiquidity().
reserveLiquidity(offerId, dealId, amount) internal
Moves the trade amount from availableBalance to reservedBalance. Atomic; reverts if insufficient.
cancelUnreservedOffer(offerId) on-chain
Seller can retract an offer at any time, as long as no deal has reserved against it.
unlockPaymentInstructions(dealId) app-side
Reveals seller's IBAN & reference to the buyer in the app. Gated by on-chain confirmation that reservation has settled.
markPaid(dealId) app-side
Buyer signals payment was sent. Off-chain in the early product, optional on-chain attestation later.
release(dealId) on-chain
Seller confirms receipt of fiat. Contract transfers crypto to buyer atomically. Position closes as RELEASED.
releaseWithSellerSignature(dealId, sig) on-chain · gasless
Relayer pays gas; contract verifies the seller's EIP-712 signature before settling. Same effect, gasless UX for seller.
refundExpired(dealId) on-chain · permissionless
Anyone can call after the deadline elapses with no payment marked. Reserved funds return to seller's availableBalance.
claim(dealId) on-chain
User pulls funds owed to them after a terminal outcome. Always available, even if the API is down.
addExtraFunds(dealId, amount) on-chain
Seller adds more crypto to the same escrow (e.g. buyer overpaid fiat or rate adjustment).
proposeAdjustment(dealId, newAmount, reason) on-chain
Either party proposes a revised final settlement amount. Recorded on-chain pending acceptance.
acceptAdjustment(dealId) on-chain
Other party accepts the proposed adjustment. Contract updates the escrow then proceeds to settlement.
splitSettlement(dealId, buyer, sellerRefund, fee, gas) on-chain
Settles with custom distribution: buyer gets X, seller gets Y back, fee and gas accounted for. Used in agreed adjustments or arbitration outcomes.
openDispute(dealId) on-chain
Either party freezes the escrow. State transitions to DISPUTED. No automatic release until resolved.
resolveDisputeBySafe(dealId, outcome) Safe-only
Only the arbitration Safe can call. Outcome is RELEASE or REFUND. Single keypair cannot execute.
resolveSplitBySafe(dealId, b, s, fee, gas) Safe-only
Safe resolves with custom split distribution. Useful when fault is shared or buyer overpaid/underpaid fiat.
requestMoreEvidence(dealId) app-side
Arbiter or moderator requests additional evidence from a party. App-level action, does not affect on-chain state.
pause() Pause Safe
Emergency stop on risky actions. Does not lock or seize funds — only prevents new ones.
unpause() Pause Safe
Resume protocol operations after the cause of the pause is resolved.
setArbitrationSafe(newSafe) Admin Safe + timelock
Update the arbitration Safe address. Subject to 7d timelock so users can exit before change applies.
setFeeTreasury(newTreasury) Admin Safe + timelock
Update the address that receives platform fees. Timelocked. Treasury cannot reach into escrows.
setRelayer(address, allowed) Admin Safe
Allow or revoke a relayer address. Relayers can submit transactions but cannot select payout destinations.
cancelBeforePayment(dealId) on-chain
Both parties can cancel by mutual signal before payment is marked. Reserved funds return to seller's vault.

Every trade walks the same path.

Twelve canonical states. Every transition is gated by either a user-signed action, a contract condition, or a Safe-approved decision.

OFFER_CREATED
Seller publishes terms. Vault must reference available liquidity.
VAULT_FUNDED
Seller's vault holds the liquidity the offer claims to back.
DEAL_OPENED
Buyer commits to a specific offer at a specific amount.
RESERVED
Amount moves from available → reserved. Seller cannot move it.
PAYMENT_INSTRUCTIONS_UNLOCKED
App reveals IBAN & reference to the buyer. Gated by on-chain reserve.
PAYMENT_MARKED_SENT
Buyer signals payment was made. Off-chain in the early product.
RELEASED
Seller confirms; buyer receives crypto atomically. Position closes.
REFUNDED
Funds return to seller's availableBalance after timeout or cancel.
DISPUTED
Funds frozen pending Safe arbitration outcome.
SPLIT_SETTLED
Funds distributed per agreed adjustment or Safe ruling.
EXPIRED
Deadline elapsed without resolution. Anyone can trigger refund.
CANCELLED
Closed before reservation/payment by mutual consent or rule.

Four outcomes, four paths.

Every escrow ends in one of four ways. Click through to see exactly which contract calls run, and in what order.

01 · OPEN
Buyer calls openDeal()reserveLiquidity() runs internally
02 · UNLOCK
App reveals IBANunlockPaymentInstructions() · app-side
03 · MARK
Buyer marks paidmarkPaid() · app-side
04 · RELEASE
Seller signs releaserelease() or releaseWithSellerSignature()
01 · RESERVED
Funds lockedbuyer hasn't acted
02 · TIMEOUT
Deadline expiresblock.timestamp > expiresAt
03 · CALL
Any caller triggersrefundExpired(dealId) · permissionless
04 · DONE
Seller vault restoredreserved → available
01 · RESERVED
Lockedtrade in progress
02 · PROPOSE
Adjustment proposedproposeAdjustment()
03 · ACCEPT
Other party acceptsacceptAdjustment()
04 · SPLIT
Custom distributionsplitSettlement(buyer, sellerRefund, fee, gas)
01 · FREEZE
Dispute openedopenDispute() · funds locked
02 · EVIDENCE
Both sides submitapp-side · receipts, statements
03 · VOTE
Arbiters review2-of-3 signature off-chain
04 · EXECUTE
Safe calls contractresolveDisputeBySafe() or resolveSplitBySafe()

Disputes reviewed off-chain. Settled on-chain.

Smart contracts cannot verify bank transfers, screenshots or RWA conditions. So Middn separates the two layers: human review off-chain, Safe-executed settlement on-chain.

01Buyer marks paid; seller refuses release or contests.
02Either party calls openDispute(). Funds freeze.
03Both sides upload evidence to the dispute room (off-chain).
04Normal trades: Ops Safe reviews. High-value: 3 arbiters required.
052-of-3 majority reached → Safe calls resolveDisputeBySafe().
06Contract executes the decision: release, refund, or split.

Who can do what.

Permissions are scoped narrowly. Anyone who can move funds is a Safe, not a single key. Anyone who can decide an outcome is many people, not one.

Support / Moderator
read
Read tickets & trade context.
Request more evidence.
Cannot move funds.
Arbiter
vote
Review evidence.
Vote on outcome.
Cannot execute alone.
Ops Safe
resolve · capped
Resolve small disputes.
Subject to max amount cap.
Cannot touch high-value escrows.
Senior Safe
resolve · high
Resolves high-value disputes.
Stricter threshold (e.g. 3-of-5).
Cannot upgrade contracts.
Protocol Admin Safe
upgrade
Manage upgrades + parameters.
Subject to 7d timelock.
Cannot touch user vaults.
Relayer
execute
Pay gas, submit transactions.
Verified per-call signature.
Cannot choose payout destination.
Pause Safe
emergency
Pause risky actions in emergency.
Lower threshold for speed.
Cannot withdraw any funds.
Treasury
receive
Receive platform fees.
Address is publicly verifiable.
Cannot reach into escrows.
User / Owner
custody
Deposit, withdraw available.
Sign release / refund / cancel.
No-one else controls their vault.

If the API fails.

The app is the premium UX but never the only path. If Middn's API goes down — temporarily or permanently — users can still recover their funds directly from the contracts.

View on-chain deals. Recovery UI reads deal state directly from EscrowEngine.
Refund expired trades. Permissionless call to refundExpired(dealId) after timeout.
Claim resolved payouts. claim(dealId) always pulls funds owed to the caller.
Verify escrow state. All escrow positions are public; anyone can query via block explorer.
Verify the Safe. Arbitration Safe address is on-chain & auditable. Active disputes may wait for resolution.
Pause new trades. Pause Safe halts new openings. Existing escrows still resolve via their normal paths.

Architecture, in one breath.

Middn is a settlement primitive. The app is UX. The backend is plumbing. The contracts are the source of truth. The Safe is the only entity that can override defaults. Funds never leave user custody without an on-chain rule allowing it.

Real liquidity. Reserved before payment.
Relayers can sponsor gas. Middn does not own funds.
The relayer executes; it does not decide.
Available is withdrawable. Reserved is not.
Disputes reviewed off-chain, settled on-chain.
The Safe resolves; one key cannot.
No dead-end escrow. Every trade has an exit path.
Payment details unlock only after reserve.