Inspiration
Two people want to buy something together online. Neither wants to send money first. One goes first and gets scammed — or nobody moves and the deal dies. Middlemen charge fees, require identity checks, and add a third party you still have to trust.
I kept thinking: this is exactly the kind of problem cryptography was supposed to solve. Two parties, one locked outcome, no human in the middle. When I discovered SimplicityHL and the Liquid Network, I realized the tools to build this properly finally existed. DuoPay is the result.
What It Does
DuoPay is a 2-of-2 co-buyer escrow contract written in SimplicityHL on the Liquid Network.
Both buyers lock their share of funds into the contract on-chain. The funds have exactly two possible outcomes — no more, no less:
Release path: Both buyers sign with their BIP-340 keys confirming delivery. The contract verifies both signatures via jet::bip_0340_verify and releases the full combined amount to the seller in Output[0].
Timeout path: If the deal collapses, both buyers wait for param::EXPIRY_TIME block height to pass. The contract enforces the timelock via jet::check_lock_time, then splits funds back — Output[0] to Buyer A, Output[1] to Buyer B.
No arbitrator. No admin key. No middleman. The chain enforces it.
How I Built It
I wrote duopay.simf in real SimplicityHL syntax, following the patterns from the Blockstream simplicity-contracts repository — specifically option_offer.simf.
The contract uses:
witness::PATH+Either<Left, Right>+matchfor two-path branchingparam::namespace for compile-time parameters (public keys, amounts, expiry)witness::SIG_Aandwitness::SIG_Bpassed at spend time via.witfilejet::bip_0340_verifyfor BIP-340 signature verificationjet::check_lock_timefor timelock enforcementjet::sig_all_hashfor the transaction sighashjet::add_64andjet::eq_64for amount arithmetic and verification
The Rust witness builder in main.rs handles constructing the .wit files for both spend paths using the LWK SDK.
Challenges I Ran Into
SimplicityHL is a new language with limited documentation and almost no Stack Overflow to fall back on. Every pattern had to be worked out from reading the existing contracts in the simplicity-contracts repo directly.
Getting the amount verification right was the trickiest part — unwrapping the explicit Liquid asset amount from jet::output_amount requires understanding the (Asset1, Amount1) pair type and using unwrap_right to extract the u64 value cleanly.
The two-path match structure also required careful thinking about what each witness carries and how param:: values interact with spend-time witnesses. There is no runtime debugging — you reason through it statically or it does not compile.
Accomplishments I'm Proud Of
The contract is written in real SimplicityHL syntax — not pseudocode, not a design sketch. Every jet, every namespace, every pattern matches what is actually in the Blockstream simplicity-contracts repo.
More importantly: the logic is correct. Exactly two terminal states. No unauthorized spend path. No runtime surprises. Both buyers can verify this before depositing a single satoshi — which is the entire point.
This is also the first co-buyer escrow design built natively for Simplicity's UTXO model. Most escrow solutions on-chain are single buyer ↔ single seller, rely on a human arbitrator, or inherit Ethereum's reentrancy risks. DuoPay has none of those problems.
What I Learned
Simplicity's "no loops, no recursion" constraint is not a limitation — it is a feature. Being forced to think in finite, bounded execution paths makes the contract logic cleaner and easier to reason about than any Solidity escrow I have seen.
I also learned how Liquid's explicit asset and amount model works at the jet level — how jet::output_amount returns a typed pair, and how to correctly verify that the right amount of L-BTC is going to the right output index.
Most of all: formal verifiability is not just a talking point. When you cannot rely on a debugger or gas estimation, you think much more carefully about what the code actually guarantees. That discipline is what makes Simplicity contracts worth building.
What's Next
- Compile
duopay.simfvia the Simplex toolchain and deploy to Liquid Testnet - Build and test
.witwitness files for both spend paths end-to-end - Add a Deal Link web interface using LWK SDK — create and fund contracts without touching the CLI
- Client/Freelancer mode: single funder, single approver, same contract architecture
- M-of-N variants: 3+ co-buyers using Simplicity's multi-input UTXO support
- Optional dispute oracle: a third agreed-upon arbitrator signature as a third spend path
Built With
- liquid-network
- lwk-sdk
- rust
- simplex
- simplicityhl

Log in or sign up for Devpost to join the conversation.