A board where peers post intents and other peers fill them. Nobody in the middle.
An intent says what you give and what you want. settle names how it settles. Anyone who
understands that method can fill it — no allowlist, no registration, no operator. Today one method
works (two ERC-20 legs on Base, both moved in a single transaction). Bitcoin is the next one, and
adding it does not change the board, the page, or anybody else's client.
flowchart LR
A[Peer A<br/>has 0.002 USDC] -->|1. signs an intent| B[(Public Nostr relays<br/>run by strangers)]
B -->|2. reads it| C[Peer B<br/>has 0.0008 EURC]
C -->|3. checks + simulates<br/>free, before any gas| C
C -->|4. executes| S[Seaport 1.6 on Base<br/>audited, not ours]
S -->|one transaction<br/>both legs or neither| A
S -->|one transaction<br/>both legs or neither| C
N[Nuri] -.->|runs nothing| B
style S fill:#ddffdd
style N fill:#ddddff
The money never leaves a wallet until both sides move at once. There is no step where funds sit somewhere waiting — so there is nothing to recover, no escrow, no recovery key.
sequenceDiagram
participant A as Peer A
participant R as Public relays
participant B as Peer B
participant S as Seaport 1.6
A->>R: publishes signed intent
B->>R: fetches intents it can settle
B->>B: verifies signature, balance,<br/>allowance, simulates fill
B->>S: fulfillOrder
S->>A: 0.0008 EURC
S->>B: 0.002 USDC
Note over A,B: One transaction. Both legs, or neither.
flowchart TD
X[Something goes wrong] --> Q{What happened?}
Q -->|Maker spent the money| R1[Fill reverts.<br/>Taker loses nothing but a free check.]
Q -->|Nobody takes the intent| R2[It expires.<br/>Maker's money never moved.]
Q -->|Relays censor it| R3[Send it as a file instead.<br/>Same format, no relay needed.]
Q -->|Nuri disappears| R4[Nothing changes.<br/>We run no part of this.]
Q -->|Our service is down| R5[There is no our service.<br/>See above.]
There is a real, fillable offer on the public board as you read this (it expires 24h after posting — an empty board is normal, offers only exist while somebody makes one):
- Maker
0x42EA1bd1DdAfd2911095BEe9284A400BE9889318gives 0.0002 USDC, wants 0.00018 EURC - Intent
0x202c157aeebe1d92fe63f519196613feb342c6895e7dd7f791933e8d0c5646f0 - Find it: open
index.html, click “Look for offers” — or ask any relay for kind38383with tagm = seaport-1.6
It is dust on purpose. It proves the loop: sign → publish → discover → check → fill.
The Bitcoin half is proven on signet — lock, claim with the secret, and the refund after the timeout, all as real transactions:
| what | transaction | block |
|---|---|---|
| lock 20000 sats | 0147e2e5b81123e3072844daa38e8d4772861ce5d4f2e36a2245347b6215caf5 |
322193 |
| claim with the secret | d2503ad9445bc6a1133608e162fc817edad36dfb36c313e761b3499ad999c62f |
322194 |
| lock again | e7e2c9aa865405567081c49d9caf2a41148882522a2b32f7af8ae004181fc88f |
322195 |
| refund after timeout | 83e1836c4a028b853c07cd26dbc5b575dba44184887f9e6d45a592968df6f7da |
322197 |
Refused along the way, by the network and not by us: a refund before the timeout, and a claim with the wrong secret.
The claim published the secret on chain — b7d29d27f54a591308a08378b8736c54bf8213087a70462d36bada920be1c182,
sitting in the witness for anyone to read. That is the mechanism: the same SHA256 hash locks
contracts/ERC20Swap.sol on Base, so revealing the secret to take the Bitcoin also opens the
EVM side. Neither peer can take both.
Still missing: the two halves driven as one swap (scripts/prove-swap.mjs), and mainnet.
Reproduce the Bitcoin half with node scripts/prove-btc.mjs — it costs nothing and broadcasts
nothing until --execute.
Yes — index.html is the frontend, one file, no build, no server. Open it from file:// in any
browser with a wallet. It makes offers, checks other people's offers in plain sentences, and
executes them. It talks to Base and to public relays, never to a server of ours.
CoW has the better price and the better auction. But its settlement contract is onlySolver: a
peer cannot fill a peer's order, and joining the solver set means a pool plus onboarding. NEAR
Intents needs approval and KYC/KYB. Those are venues you send an order to.
This is the other thing: peers meeting directly, any taker, no permission. If you want the best price on an EVM pair, use CoW. If you want two people — or two agents — to trade without asking anyone, use this.
A settlement method is listed here if and only if any taker can fill it. That is what rules
out CoW (onlySolver) and NEAR (KYC). Methods register themselves in one file; the board never
parses their terms.
| method | legs | atomic | status |
|---|---|---|---|
seaport-1.6 |
ERC-20 ↔ ERC-20 on Base | yes, one transaction | works, proven on mainnet |
htlc-v1 |
Bitcoin / Lightning ↔ EVM | two locks, one secret | readable, no_adapter_yet |
| claim | evidence |
|---|---|
| a real swap settles | proven live — Base mainnet, fill 0xe52248be…fee9476, block 51271640, status 1. Maker 2000 USDC → 800 EURC, taker 800 EURC → 2000 USDC. Proof: proofs/fill-30cd40bdde.json |
| the board carries intents without us | proven live — published to three public relays, read back by a fresh client, signature and both legs intact |
| a stranger's client can read them | proven live — 20 lines of WebSocket, no code of ours, full understanding |
| the shipped page finds real intents | proven live — headless Chrome, real relays, two intents listed |
| a worthless offer costs the taker nothing | proven live — refused against Base before any gas |
| our order hash is what Seaport computes | proven live — asserted against getOrderHash on Base |
| Bitcoin works | not yet — htlc-v1 parses and enforces timelocks, no adapter |
Verify the swap yourself:
cast receipt 0xe52248bee6a4e61f1162469102683a42db42abc9428595ad83379dc13fee9476 status --rpc-url https://base.drpc.orgsrc/constants.mjs addresses and values we depend on, all verified on chain
src/offer.mjs build, sign and verify an offer
src/settle.mjs the method registry: what settles, and by what rules
src/settle-seaport.mjs the working method (Base, atomic)
src/settle-htlc.mjs the Bitcoin method (parses, honestly not executable yet)
src/board.mjs publish and read intents over Nostr, or as a file
src/fill.mjs check, simulate and execute a fill
index.html the whole product for a person, one file, works from file://
test/ 43 tests, 8 of them read Base for real
scripts/prove.mjs one real swap, end to end, writes a proof
scripts/prove-board.mjs publishes an intent to public relays, reads it back
scripts/prove-stale.mjs the evil twin: proves a dead offer costs the taker nothing
proofs/ signed evidence with transaction and event ids
SCHEMA.md the intent format, so others can implement it
npm install
npm test # 43 tests, 8 of them read Base for real
npm run test:page # the page in headless Chrome, offline
npm run test:live # the page against the real public relays
npm run prove:board # dry; add --publish to post to public relaysOpen index.html in a browser. It works from file://, with no server.
npm test can never spend money: it holds no key and sends no transaction. Anything that moves
funds needs an explicit --execute and keys you supply.