One Reach backend, per-participant frontends for cross-chain wagers
Use a single Reach backend to describe a two-party protocol and let the compiler generate per-participant contract code and JavaScript clients for Algorand or Ethereum, keeping on-chain logic separate from UI and I/O.
25 Jun 2026, 03:52 UTC

Write the protocol once, generate per-participant code for each chain
The practical problem is maintaining a two-party payment protocol on multiple consensus networks without duplicating on-chain logic. Reach solves this by letting you describe the interaction as a single backend. The compiler then produces per-participant contract artifacts and a JavaScript client library that targets the chosen connector, such as Algorand or Ethereum.
The useful takeaway is separation of concerns: on-chain logic stays in Reach, user I/O stays in plain JavaScript interact implementations, and the same backend compiles to different chains.
How the single backend maps to participants
A Reach program declares participants, their roles, and the ordered steps of the protocol. Publish makes a value public on-chain, pay moves funds into the contract, and transfer moves funds out. The compiler verifies properties like funds not being locked forever across all execution paths.
Participant interaction is declared explicitly via an interact object. The backend references methods like interact.getHand or interact.informTimeout, but does not implement them. The frontend supplies plain JavaScript implementations for each participant, keeping UI and network I/O off-chain.
Worked configuration: a two-party wager
The canonical example is a wager where Alice sets the amount, Bob accepts, and the balance is paid to a winner. The backend expresses the protocol, not the UI.
// wager.rsh - backend protocol sketch (simplified)
export const main = Reach.App(() => {
const Alice = Participant('Alice', {
wager: UInt,
...hasConsoleLog,
});
const Bob = Participant('Bob', {
acceptWager: Fun([UInt], Null),
});
init();
Alice.only(() => {
const wager = declassify(interact.wager);
});
Alice.publish(wager).pay(wager);
commit();
Bob.only(() => {
interact.acceptWager(wager);
});
Bob.pay(wager)
.timeout(relativeTime(1000), () => closeTo(Alice, informTimeout));
// winner decided by application rule, e.g. commit-reveal outcome
transfer(balance()).to(winner);
commit();
});In this sketch Alice publishes a wager amount and pays it, Bob accepts and pays the same amount, and the contract transfers the combined balance to the winner. The interact object is defined per participant: for Alice it might expose a method to choose a hand or confirm the amount; for Bob it exposes acceptance and a choice. The frontends implement these as ordinary JavaScript functions.
Commit-reveal is required for any secret choice. Publishing a choice directly lets the counterparty front-run it. A fair pattern is to have each participant publish a commitment to a secret, then reveal the secret later, with the contract verifying the commitment matches via makeCommitment and checkCommitment.
Timeouts are expressed with relativeTime or absoluteTime. If a participant disappears, the timeout branch allows the other party to reclaim funds. Omitting a timeout branch is a common beginner mistake and the compiler will warn about potentially locked funds.
Limits and common mistakes
Reach abstracts the chain but not its costs. Gas or fee models, block times, and token standards still differ between connectors. Every consensus step costs fees on the target chain, so minimizing the number of on-chain transfers is a key optimization decision.
Compiler verification covers what the language can express, such as balanced transfers and reachable termination. It does not catch application-level logic errors. A wrong winner condition or a bad randomness source will still compile and verify.
Non-network tokens and linear state are supported via the standard library, but the mapping to each chain's asset model is connector-specific. Confirm the toolchain is actively maintained for your target network before committing to production.
A practical way to check the result is to compile the backend and inspect the generated per-participant artifacts and client library. Run the compiled DApp against a local devnet for the chosen connector and verify that both participants' balances change as the protocol dictates when the happy path and timeout branches are exercised.
Common mistakes to avoid:
- Publishing a secret choice directly instead of using commit-reveal.
- Omitting a timeout branch, which can leave funds stuck if a participant stops responding.
- Assuming fee equivalence across chains and designing a protocol that is too chatty for Ethereum.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.