QuaiRelay
QuaiRelay solves one narrow problem: some in-app wallets can call a contract but cannot make
a plain value transfer (on Quai this typically surfaces to the user as could not coalesce error). QUAI sitting in such a wallet is otherwise stuck. QuaiRelay turns a transfer into a
contract call — the wallet signs sendTo(recipient) with value attached, and the relay forwards
that exact value to recipient in the same transaction.
wallet ──sendTo(recipient) + value──▶ QuaiRelay ──same transaction──▶ recipientArchitecture
QuaiRelay is a contract, not a service. There is no backend, no database, no indexer and no owner/operator key. The two front ends that call it are:
- quai-relay — the standalone page.
index.html+app.js+relay-core.js, no build step, no dependencies, served statically (including from IPFS). Connects to the wallet's injected provider, reads balance/gas from the public Quai RPC, and submits one transaction. - /blipsend on HartiiLabs (hartii-labs, a separate product — see its own docs). A production page aimed at Blippay in-app-wallet users, offering the same plain-transfer-first / contract-fallback pattern, wired to the same relay contract. It is documented here only because it is QuaiRelay's main real-world caller — see How /blipsend uses the relay below.
Data flow for a relay send:
- The page reads the connected wallet's balance and the current gas price directly from
https://rpc.quai.network/cyprus1(quai_getBalance,quai_gasPrice). - It validates the destination client-side (Cyprus-1 prefix, not a Qi-ledger address, checksum,
not self, not the relay), takes the gas limit from a live
quai_estimateGasof a send to a never-used recipient plus 20% (130,000 if the node does not answer), and computes the maximum sendable amount (balance minus that gas cost with 25% headroom). - The wallet signs a transaction:
to= the relay contract,value= the amount,data= ABI-encodedsendTo(recipient). - The relay contract forwards
msg.valuetorecipientin the same transaction, or reverts the whole thing — there is no partial-failure state.
Live contract
| Network | Address |
|---|---|
| Quai mainnet, Cyprus-1 (chain 9) — v2, the only one to use | 0x003D2EC4C1Ff5e98898fEa6C6D1AAaA48FaDc11A |
| Retired v1 — no new sends | 0x0031B09e63592B713768761cdDACC603974c10bD |
Verified live: quai_getCode against the v2 address on https://rpc.quai.network/cyprus1
returns 674 bytes of runtime bytecode, identical to the build. Deployed 2026-11-02, solc 0.8.20,
EVM paris, after a rehearsal on Orchard.
Why v2. v1 (deployed 2026-09-19) checked the zone but not the ledger. A Qi-ledger address
also starts 0x00, and on Quai a value call to one runs out of gas instead of reverting, so a send
to it burned the payer's whole gas limit (nothing else was lost: the transaction reverted). v2
refuses such a recipient by name ("Recipient is a Qi address"), so the gas estimate fails with a
reason before the wallet opens. Every Hartii page now sends through v2 only. HPAY's history and
receipt tools still read v1's Relayed events, so payments made through it keep verifying.
Source. The contract, the standalone page and their tests are open source: github.com/IboTheSecond/quai-relay (MIT).
See contracts for the full interface and trust model, and integrate for runnable examples.
How /blipsend uses the relay
HartiiLabs' /blipsend page (hartii-labs/src/pages/BlipSend.jsx) offers two ways to move QUAI
out of a stuck wallet, both to the same address the visitor typed:
- Plain transfer — an ordinary
signer.sendTransaction({ to, value }). Works for any wallet that can send. - Contract route (QuaiRelay) — for wallets that can't:
contract.sendTo(destination, { value })against the relay address above, using the ABI insrc/abi/quaiRelay.js.
The page's own comment explains why it uses the general-purpose relay rather than a
fixed-destination forwarder contract (hartii-labs also has one, QuaiForwarder, used for a single
private payee elsewhere on the site):
In other words: sendTo's lack of a fixed destination isn't a limitation, it's the reason this
contract is safe to expose on a public page at all.
How PayLink uses the relay
PayLink — the shareable-payment-link feature on Gallery and
Biome (where it is now called HPAY) — is the relay's other real-world caller, via the same
sendTo(recipient) pattern /blipsend uses: a plain native transfer for wallets that support one
(Pelagus), QuaiRelay for wallets that can't (Blip, and anything unrecognized). The shared,
byte-identical payLinkPay module (Gallery's src/utils/paylink/payLinkPay.js, Biome's
apps/biome/public/payLinkPay.mjs) picks the route with shouldUseRelay(kind) from
detectWalletEnvironment, never guessing mid-payment.
Every PayLink payment carries an explicit gas limit and the live gas price, fixed 2026-09-30
after a funding bug: a transaction with no gas fields lets the wallet reserve its own default, and
Blip's "Fund & continue" sheet asked a payer for a 75 QUAI "preparation reserve" on a 10 QUAI
payment (roughly 1.3M gas at the live ~56k gwei, when the real cost is far smaller). Measured on
mainnet: quai_estimateGas for QuaiRelay.sendTo to a never-used recipient is about 98,000 gas
(98,233 on v2, 2026-11-02), to a recipient that already exists about 44,000; mined sends have used
32,722 to 73,616. A plain value transfer is 55,236.
estimatePaymentGas() fixes this:
- Calls
quai_estimateGaswithout afrom— the estimate has to work while the payer's wallet is still unfunded, which is exactly the moment the top-up sheet appears, and afromon an empty wallet gets rejected with "insufficient funds." - Adds 20% headroom to the estimate, and reads the live
quai_gasPricealongside it. - Falls back to measured constants (130,000 gas for the relay path, 70,000 for a plain transfer) if the node is unreachable, so the payment still goes out on the first tap.
The Pay page shows the same number back to the payer as a "Network fee ≈ X QUAI" line under the Pay button, rounded up — so the funding sheet the wallet shows and the fee line the page shows agree, instead of the wallet inventing its own much larger number.