1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602 | # EXCH/1 — the bursa market protocol
The contract between a bursa client and the markets it runs on. The
implementation is `client/src/bursa/lib/` — `protocol` (the op union),
`artifacts` (the two signed instruments), `match` (the fold), `trade` (the
settlement walk); this document is the normative text.
**Depends on BANK/2 — `banca/docs/PROTOCOL.md` §13.** Read that first.
Everything here assumes banks can hold money conditionally; without BANK/2 this
protocol has nothing to settle with.
**The base contract is sueta protocol v2, unchanged** — transport, sealing, the
OrbitDB log, presence, the offline mailbox, all of it from `ardegazu.rooms.*`
and specified in `chat/docs/PROTOCOL.md`. This document covers only what bursa
adds: the op union, the matching fold, and the honest limits of both.
**App salt:** `bursa.ardegazu.ro/v1`.
**Class 1, per `dev/docs/HOST-ELECTION.md` §9.** A market is a mergeable log
with a deterministic tie-break, so it carries **no host election, no `ro`, no
`cl` and no host id — ever.** Matching is a fold, not a referee. An election
here would put a single point of failure, and a front-running opportunity,
into a design that has neither.
---
## 1. What a market is
**One encrypted OrbitDB log, addressed by a link, replicated by everyone who
holds it.** The link is a bare secret:
```
#<secret>
```
43 base64url characters, chat's and board's form — **not** banca's
`#<secret>.<pub>`. A market has no operator, and the absence is deliberate:
- Every race the market can have is resolved by the fold. There is nothing for
an authority to decide.
- Listing a pair is implicit in quoting it. There is nothing to admit.
- Spam is filtered per viewer, never dropped, for the reason banca refuses to
cap `req` (§9): a silent drop makes two replicas disagree, and disagreement
is the one thing a market must not have.
It also has a practical payoff. banca had to be **excluded from social-kit's
`SUITE_APPS`** (`social-kit/src/ardegazu/social/invites.cljs`) because the
hub's invite picker mints a bare 43-char secret, and a bare secret is not a
bank — the picker would have minted rooms nobody could open. A bare-secret
bursa drops into the picker and works, and bursa is in `SUITE_APPS`.
**A suite identity is required to trade.** Entry authorship comes from the log's
verified author id — the suite Ed25519 idPub for a member with an identity, the
per-device OrbitDB fallback key for anyone without one. The fold gates every
authorship-authorized op (`bank`, `take`, `leg` — §3) on the canonical
43-character base64url shape, so ops authored by the fallback identity never
fold: **an identity-less visitor is read-only.** The one exception is the one
banca makes (banca §4): an identity-less client may still *relay* a signed
quote or cancel, because those are authorized by the maker's signature inside
them. Positions bind to a key, so bursa must never auto-adopt on an identity
conflict.
---
## 2. The bank directory — and the privacy cost that comes with it
To verify a counterparty's lock you must read their bank's log, and to read a
bank's log you must hold its link. So **a currency is only tradable in a market
where its bank's link has been published**, and the market room needs a
directory. This is the "runtime directory of banks is bursa-era work" that
`banca/client/src/banca/lib/publicbanks.cljs:5` flagged.
```
{ t:"bank", ts, s } // s = a WHOLE bank link token, "<secret>.<bankerPub>"
// or "<secret>.<bankerPub>.<v>" (banca §13.5)
```
Authorship-authorized: any member may publish a bank they hold. The fold keeps
the directory as rows in first-seen `(clock, hash)` order, one per distinct
token — a repeated token is inert — and a currency resolves to the **first
published** row whose banker issues it, deterministically on every replica
(`view/bank-for`): a banker running two banks answers with the one published
here first. The tradable universe is the currencies of the BANK/2 rows.
**Only a BANK/2 link — the version-suffixed form** (banca §13.5) — **is
tradable.** A BANK/1 bank has no escrow, so a leg cannot be locked in it and a
trade against it cannot be atomic. A `bank` op naming a v1 link is **valid and folds**, and the
directory keeps it, marked untradable with the reason in words: "this bank runs
BANK/1 and cannot hold money conditionally". Dropping it instead would render as
an unexplained absence, and an unexplained absence reads as a bug — the failure
mode this suite spends most of its interface rules avoiding.
**This hands the entire ledger of that bank to every member of the market,
forever.** BANK/1 §10 is explicit — a stranger *with* the link sees every op,
every amount and every account key, permanently, and closing an account does not
take that back. Publishing a bank into a market is therefore an irreversible
disclosure of everyone who banks there, made by one member, on their behalf.
Three consequences, all of which the interface must carry:
1. **The confirmation says what it does.** Not "add bank" — "everyone in this
market will be able to read every payment in this bank, forever". The link is
the capability and the sheet that shows it says so.
2. **The six bot banks are already public by design** — they are a bundled
constant in banca, they approve every knock, and handing the token to
everyone is the product. Publishing those costs nothing.
3. **A private bank should not be published, and bursa cannot stop it.** The
member who holds the link can disclose it here or anywhere else. What bursa
owes its users is to never do it silently and never do it by default.
**Nothing dedupes a bank against a name.** Two entries claiming `LEI` are two
different currencies with different banker keys, and the interface shows the
issuer, because the whole point of `<bankerPub>.<CODE>` is that there is no
registry and nobody to ask.
---
## 3. The op union
Five ops. Every op is `JSON.stringify`'d before the rooms core seals it, so
**key order is part of the bytes a peer verifies**: builders use
`ardegazu.rooms.js/ordered`, never `#js {}` or `js-obj`, which silently switch
to hash order at nine pairs at every optimization level. `t` is always first and
`ts` always second.
```ts
{ t:"bank", ts, s } // 3 keys; a bank link for the directory
{ t:"quote", ts, o } // 3 keys; o = a bxo, carried VERBATIM
{ t:"cancel", ts, c } // 3 keys; c = a bxc, carried VERBATIM
{ t:"take", ts, h, qty } // 4 keys; h = the QUOTE ENTRY's hash
{ t:"leg", ts, m, lh } // 4 keys; m = the match id; lh = a lock entry hash
```
Implementation truths a second implementation must copy exactly:
- **One signed quote books once**, however many entries carry it (a mailbox
relay and a live append are the same quote). The dedup key is the inner
SIGNATURE — canonical base64url makes it unique per signed artifact.
- **A take is INERT** — no record, nothing consumed — when its quote is closed,
exhausted or unknown, when the taker IS the maker, and when the fill's quote
leg cannot settle (see the rounding rule below). A take is an invitation to
lock, and an invitation that cannot be answered must not book.
- **A `leg` attaches only from a party to its match**, once per lock hash. The
pointer is advisory, but a spammed pointer wastes every reader's bank scan.
- **A cancel folds from its own (clock, hash) position**, in the main fold —
NOT as a pre-pass. A take that sorts before it filled a live quote and
stands; one after it finds the quote closed. Nothing in this fold needs
whole-set knowledge the way banca's ack and escrow rules do, so nothing here
is a pre-pass.
**Exact key sets.** Every validator checks `Object.keys(op).length`. v1 has **no
extension slot**: a field that does not exist above cannot be added by a peer
and quietly ride along into someone's fold. A new field means a new `t`, or a
protocol bump. An op whose `t` this version does not know is dropped silently —
an op the fold cannot judge is an op the fold must not fold.
**A validator is the contract.** An op that does not validate is **dropped,
never repaired** — it never enters the entry set, so it cannot influence any
pass.
**There is no retention bound anywhere.** No quote cap, no match cap, no
drop-oldest. A book is a function of all history and a fold that forgot would be
a fold that lied. The room replays with `loadTail(-1)` — every entry, at boot,
offline included. The scaffold's newest-500 window is lifted, deliberately.
### 3.1 `bxo` — the quote
Signed by the **maker**, so any member or a mailbox relay can carry it into the
book and it means the same thing wherever it lands. This is banca's `pay`
asymmetry (`protocol.cljs:40-56`) applied to an order.
```jsonc
{ "v":1, "t":"bxo", "id":"<16B b64url>", "pair":"<curBase>|<curQuote>",
"side":"bid"|"ask", "px":{"num":119,"den":100}, "qty":10000,
"from":"<makerIdPub>", "ctx":"", "ts":0, "exp":0, "sig":"…" }
```
**Twelve keys.** Domain `bursa-ord`, its own — a trading instrument is not a
payment instrument and a signature over one must never read as the other.
`canon` is social-kit's, as everywhere in the suite.
- `pair` names both currencies in full `<bankerPub>.<CODE>` form. Both banks
must be in the directory for the quote to be actionable; a quote naming an
unknown bank still folds and still displays, greyed, because the bank may be
published a minute later and a book that forgot it would be wrong.
- `side` is `"bid"` (maker buys base, pays quote) or `"ask"` (maker sells base,
receives quote). **These are wire values and are never translated.**
- `px` is a **rational** `{num, den}`, integers `1..2^50` each — quote minor
units per base minor unit. The bound is the same exact-double ceiling every
amount carries, so `num · qty` stays exactly representable in the BigInt fold
(§5.6). The shape banca's `rate` already uses
(`mk-rate`, banca's `protocol.cljs`), and for the same reason: **there is no float
anywhere on this wire and no division ever crosses it.** A price is a pair of
integers and stays one through the fold, the display and the RL features.
- `qty` is base minor units, `1 ≤ qty ≤ 2^50` — wallet-kit's `AMT_MIN`/`AMT_MAX`,
the same ceiling, for the same arithmetic reason.
- `exp` is **advisory** and never folded. See §5.4.
- **There is no hashlock in a quote.** See §4.2 — this is the design's one
genuinely non-obvious omission.
**The quote-leg amount, and the rounding rule.** A fill of `f` base minor units
at `px` implies `f·num/den` quote minor units, which need not be an integer.
The rounding ALWAYS FAVOURS THE MAKER, and the direction is load-bearing:
| maker side | who pays the quote leg | rounding | why |
|---|---|---|---|
| ask | the taker | **up** (ceil) | a taker who grinds a fill into 1-unit pieces overpays per piece — grinding hurts the grinder |
| bid | the maker | **down** (floor) | rounding up here would let a taker grind the MAKER's money out one overpaid piece at a time, on an order the maker cannot re-price per fill |
A quote leg that floors to ZERO, or exceeds 2^50, cannot be locked — the atomic
construction degenerates — so a take that computes either is inert.
### 3.2 `bxc` — the cancel
Signed by the maker, for the same relayability.
```jsonc
{ "v":1, "t":"bxc", "h":"<quote entry hash>", "from":"<makerIdPub>", "ts":0, "sig":"…" }
```
**Six keys.** Domain `bursa-cxl`. It names the quote's **entry hash**, not its
`id`, so a cancel is unreplayable against a re-appended quote: every append has
one entry hash, forever.
`from` must equal the quote's `bxo.from` or the cancel is inert. A cancel by
anyone else is not a cancel.
---
## 4. Settlement
> **The market log is advisory. The binding artifacts are the two locks, in the
> two bank logs.**
Nothing in a market room moves money. Not a quote, not a take, not a match. The
fold's job is to let every replica agree on *who agreed to what*; the money is
BANK/2's problem, and it is atomic there.
### 4.1 The sequence
```
1. maker appends `quote` (market log)
2. taker appends `take` naming the quote's entry hash and a qty
3. the fold names a MATCH — m = the take entry's hash
4. maker picks a fresh 32-byte secret s, H = sha256 of its base64url TEXT
(wallet-kit's one hashing rule — banca §13.3)
maker appends `lock` to ITS bank: the maker's leg, hashlock H, ctx = m
5. maker appends `leg` to the market so the taker can find it fast
6. taker reads the maker's lock, COPIES H out of it
taker appends `lock` to ITS bank: the taker's leg, hashlock H, ctx = m
7. taker appends `leg`
8. maker appends `claim` to the TAKER's bank, revealing s — maker is paid
9. taker reads s out of its own bank's log
taker appends `claim` to the MAKER's bank — taker is paid
```
The maker holds the secret and locks first, because the secret-holder must
(banca §13). That puts the first-mover exposure — funds held until refund if
the counterparty never locks — on the maker, which is the always-on bot, and
never on the human, who commits nothing until the maker's lock is visible.
### 4.2 Why the hashlock is not in the quote
A quote can be **partially filled by several takers**. If one hashlock covered
the whole quote, the first claim would publish a secret that unlocks *every*
other taker's leg on that quote — each of them claimable by anyone watching,
against a maker leg that was never paid for.
So the hashlock is **per match, not per quote**, and it does not need to be in
the market log at all: the maker's `lock` carries `hash` in the bank log, and
the taker reads it there. The `leg` op is a pointer that saves a scan, not a
channel for the condition.
**The preimage is likewise never in the market log.** The maker reveals it by
claiming in the taker's bank, and the taker reads it out of its own bank's log —
which the taker is replicating anyway. A `secret` op was in the first draft of
this design and is deliberately absent: a secret with two homes has two chances
to be wrong, and the second home was redundant.
### 4.3 Both parties need an account at BOTH banks
Easy to miss, and it shapes onboarding. Alice locks in her own bank (to pay) and
is *paid into* Bob's bank (to receive). So does Bob. A single DUH/MOR trade
therefore needs **four** account relationships: Alice at D and at M, Bob at M
and at D.
For the six resident banks this is invisible — they run `join:"approve"` with an
approve-all banker and a faucet, so a knock is answered in seconds and the
directory gives you the link to knock with. For a bank running `join:"approve"`
with a human banker it is a real wait, and the interface must show it as a
precondition of the trade rather than letting a taker discover it at lock time:
**a take whose payee account does not exist yet is a take that cannot settle.**
The check is cheap and local — both banks are already replicated by anyone who
can verify a leg — so bursa can and must say "you have no account at Banca
Moroiului" before the button, not after.
### 4.4 A match is an invitation, not an obligation
**The maker's lock is voluntary.** Nothing in this protocol, and nothing in
BANK/2, forces a matched maker to lock. This is a real property and it must not
be discovered later:
- A stale or bogus match costs nobody anything. A member can withhold a `take`
and release it weeks later with a back-dated Lamport clock — `(clock, hash)`
is blind to arrival time — and hit a quote the maker cancelled long ago. The
maker simply does not lock. That is the whole defence, and it is sufficient,
because the taker has committed nothing either.
- Equally, **a maker cannot be sure a cancel beats a take**, and does not need
to be.
- What a market cannot offer in v1 is a *firm* quote. The teeth of a real
exchange are that a match binds; here the teeth are that a defection costs
nothing to anyone.
The designed upgrade is stated so it is not re-invented: a **firm quote** is one
backed by a live lock the maker has already placed. BANK/2 cannot express it —
a lock names a beneficiary, and a resting quote has none — so it needs a
beneficiary-less escrow op, and that is BANK/3 work, not v1.
### 4.5 No auto-crossing
The fold **never matches two resting quotes against each other**, even when a
bid crosses an ask. A `take` is an explicit act by an identified party, and
that is what makes the maker's decision to lock a decision about a known
counterparty. Auto-crossing would manufacture matches with nobody present to
act on them, and the book would silt up with dead ones.
This makes bursa a **quote-driven book**: takers hit named quotes. To a taker it
looks like any orderbook — bids and asks stacked by price, depth, a spread — and
the difference is only that resting orders do not consume each other. Say so;
do not let the word "orderbook" imply a continuous double auction it is not.
Auto-crossing becomes coherent once quotes are firm, and not before.
---
## 5. The matching fold
### 5.1 The algorithm
```
ingest validate the WHOLE entry — `op`, `hash` AND `clock` — and drop it
if any of the three does not hold. Never repair.
sort entries by (lamport clock, entry hash) — board's AccessFold order,
tie-break included, written TOTAL by construction.
verify the SIGNATURE verdicts for every `bxo` and `bxc`. Asynchronous and
pessimistic — an unverified quote is not a quote, an unverified
cancel not a cancel; a verdict landing marks the fold stale and it
refolds.
main fold walk the sorted entries oldest-first, judging each against the
state folded SO FAR: banks join the directory (first publication
of a token wins), quotes open, cancels close FROM THEIR OWN
POSITION, takes consume remaining quantity, legs attach to
matches.
```
**The whole fold is a pure function of the entry set.** Not of arrival order,
not of the wall clock, not of who is online. That is what makes replicas
unpartitionable. Unlike banca, this fold has **no pre-passes**: nothing here
needs whole-set knowledge the way banca's ack and escrow rules do, so every
rule — cancellation included — is a state transition judged at its own
`(clock, hash)` position. A take that sorts before a cancel filled a live
quote and stands; one after it finds the quote closed.
### 5.2 The envelope is as untrusted as the op — and here it decides priority
banca's ingest guard is copied **verbatim**, and an orderbook needs it harder
than a ledger does.
`rooms-kit` reads an entry's clock as `entry.clock?.time ?? 0`, a *nullish*
coalesce that passes any non-null value through, and `@orbitdb/core` signs
`clock` verbatim while only checking it is not `undefined`. An ordinary member
can therefore **sign an entry whose `clock.time` is the string `"abc"`**, and it
verifies. `(a.clock - b.clock)` is then `NaN`, and `Array.prototype.sort` with a
NaN comparator orders by **input permutation** — that is, by arrival order.
banca measured **seven distinct folded states over 24 arrival orders of one
entry set** (`banca/client/src/banca/lib/fold.cljs`, the ordering header).
In a bank that is a permanent partition. In a book it is also a **front-running
primitive**: priority among takes is `(clock, hash)`, so a peer who can make the
comparator non-total can make its own take sort first on some replicas.
```
hash a non-empty STRING
clock a NON-NEGATIVE SAFE INTEGER (Number.isSafeInteger)
op valid per §3
```
and drop otherwise. The comparator being total by construction is a second
layer, not the defence.
**`op.ts` is never a sort key.** It is a user-controlled number in a JSON
payload. Price-time priority is `(clock, hash)` and nothing else.
### 5.3 Priority, and what it honestly gives you
Two takers racing one quote resolve by `(clock, hash)` — deterministically, on
every replica, with no matcher, no host and no round trip. That is the property
that matters: **everyone agrees who got the fill.**
What it is *not* is fair in the wall-clock sense. Lamport clocks measure
causality, not time; a peer that has seen more of the log carries a higher
clock, and among genuinely concurrent takes the tie-break is the entry hash,
which is effectively random. So priority is *arbitrary but universal*. An
interface must not imply first-come-first-served, because it is not.
`(clock, hash)`-first being blind to arrival time is also what lets a withheld
take be released later against a live quote (§4.4). Bounded by the maker's
voluntary lock.
### 5.4 Expiry is not foldable
`bxo.exp` is sanity-checked against `bxo.ts` — `exp > ts`, `exp ≤ ts + 90 days`,
both clock-free comparisons between two values that travel together — and **the
fold never reads it.** A verdict that depended on the reader's clock would let
two replicas three minutes apart disagree about whether a quote was live, and a
market whose replicas disagree is not a market.
An expired quote therefore stays takeable in the fold, and it is the *interface*
that greys it and the *maker* that cancels it or declines to lock. This costs
nothing, exactly as it costs banca nothing: the maker's voluntary lock is a
strictly stronger defence than expiry, at any age.
**Consequence for implementers:** bursa's own validators are clock-free **by
construction** — `quote-shape` and `cancel-shape` take no clock argument at
all, because every bound they check is between values that travel together.
The bank folds bursa replicates to settle are banca's own code, and banca
passes wallet-kit's explicit admission clock as `null` — **no clock**
(`banca/client/src/banca/lib/wallet.cljs`, `NO-CLOCK`). That form drops exactly
one bound (`ts` no further ahead than `SKEW_MS`) and keeps every other; it is
*deterministic, not weaker*. Anything that is neither a finite number nor
`null` reads as *omitted* and selects the wall clock, so a typo cannot silently
pick the permissive mode.
### 5.5 Fills
A `take` names a quote entry hash and a `qty`.
```
fill = min(qty, remaining)
```
- `fill = 0` (the quote is exhausted, cancelled, or unknown) ⇒ the take is
**inert**. It is not a failed record and consumes nothing: unlike a bank
payment there is no sequence slot here to protect, because nothing moved.
- `0 < fill < qty` ⇒ a **partial fill**. The taker gets what was left. This is
immediate-or-cancel semantics against available depth, and it is deterministic
because `remaining` is a function of the fold so far.
- Each fill creates one **match**, keyed by the take's entry hash.
**No sequence slots in the market.** banca needs them because a `pay` moves
money and a replayed order must never move it twice. A `take` moves nothing —
the slot that matters is the payer's **payment** slot in the bank, consumed by
the `lock` (banca §13.3), which is where the double-spend defence belongs and
where it already is. Adding a second slot space here would be a defence against
nothing, with its own permanent-consumption failure mode.
### 5.6 Arithmetic
Quantities fold in **BigInt** and are reported both exactly (a decimal string)
and as a `Number` for display. A double is exact on integers to 2^53 and one
quantity may be 2^50, so a `Number` fold holds exactly **eight** maximal
quantities before it starts losing units while reporting a clean-looking total.
Prices are rationals throughout. Comparing two prices is
`a.num * b.den` vs `b.num * a.den` in BigInt — never a division, never a float,
not even for sorting the book. The only place a division is allowed to happen is
the final decimal string a screen renders: amounts ride banca's own string
arithmetic over the currency's `charter.dec` (`fmt-units`), and a price becomes
a decimal by BigInt long division, truncated, never rounded up
(`bursa.lib.view/px-to-decimal` — a displayed price must not promise more than
the book holds).
### 5.7 Match status
The market fold's match record carries only the facts the market log holds —
the parties, the fill, the quote-leg amount, the attached `leg` pointers.
**Where the trade IS lives in the two banks**, so its phase is computed by the
trade engine (`bursa.lib.trade/status`) as a pure function of the match record
and the two bank fold snapshots — no clock, no network, the same answer on
every replica and for both parties:
| phase | meaning |
|---|---|
| `maker-lock` | matched; nothing locked — the maker moves first, or nothing happens |
| `taker-lock` | the maker's leg is held; the taker's answer is due |
| `maker-claim` | both legs held; the maker's claim publishes the secret and makes the trade unstoppable |
| `taker-claim` | the taker's leg is claimed; the taker collects the maker's |
| `done` | the maker's leg is claimed — both sides are paid |
| `void` | a leg was RETURNED and the trade cannot complete; the other leg's own status says whether money is still held |
What reaches a screen beside the phase is BANK/2's vocabulary applied per leg
(**locked / provisional / final / returned** — banca §5 and §13.7), because the
money's state is the truth and the phase is a summary of it. A trade reading
`done` whose legs are both merely `provisional` must not say *final* anywhere —
the BANK/1 §12 rule that a word that promises must be kept by some screen, and
the one this app is most likely to break.
---
## 6. Injection surfaces, named
**Crafted clocks.** §5.2. The ingest gate is the defence. In a book this is a
front-running vector, not only a partition one.
**Withheld, back-dated takes.** A take held and released later with a low clock
hits a quote its maker considers long gone. Bounded by: the maker does not lock
(§4.4). Nothing is lost by either party.
**Quote spam.** Any member may append unlimited quotes, and the retention rule
forbids capping the list. Mitigations are interface-level — a per-author view, a
mute, a minimum size filter — never a silent drop, because a drop would make two
replicas disagree.
**Bank-link disclosure.** §2. The irreversible one, and the only op in this
protocol whose damage is not confined to the market room.
**Quote-stuffing the directory.** A member can publish many `bank` ops naming
banks it invented. Each costs a validator call and a row. The interface shows
the issuer key beside every currency and never dedupes by code.
**Log flooding.** Anyone with the link can append. Sealing is the access control
(the base protocol), so a URL holder can add entries a member will fold or drop.
There is no rate limit in the protocol.
**Memo and name rendering.** `ctx` arrives from members and is bounded and
control-character-free by the validators — but it is **not sanitized for
markup**. Every interface built on this fold renders it as a text node, never as
HTML.
---
## 7. What each party can see
| party | the market log | the banks |
|---|---|---|
| stranger without the link | nothing — cannot compute the address | nothing |
| stranger **with** the market link | every quote, take, match and bank link, forever | **every ledger published into the market**, forever |
| member | the same | the same |
| relay / mailbox node | ciphertext, sizes, timing, IP addresses | nothing |
**A market is as public as its most public member makes it**, and it is
transitively as public as the banks published into it. There is no per-pair
privacy inside a market and this design does not offer one.
---
## 8. Limitations — read these before you trade here
- **A match does not bind anyone.** §4.4. Settlement is voluntary; what the
protocol guarantees is that it is *atomic when it happens*, never that it
happens. There is no reputation layer in v1 and no penalty for a maker who
never locks.
- **Priority is arbitrary, though universal.** §5.3. Not first-come-first-served.
- **Publishing a bank is irreversible disclosure.** §2.
- **Escrow constrains your counterparty, not the banks.** Both bankers can still
mint without limit and refuse to ack. BANK/1 §11 is unchanged and is still the
thing to read before keeping money anywhere in this suite.
- **A locked leg can be stuck.** If no preimage is revealed, the banker never
attests and the beneficiary never releases, the money stays locked. banca
§13.6.
- **No firm quotes, no auto-crossing, no multi-hop routing, no partial claims.**
Each is named as future work where it is designed, not left as a gap for a
reader to discover.
- **Nothing here is insured, redeemable or legal tender.** A position in bursa is
a record of what two logs say, agreed to by the people reading them.
---
## 9. What an interface built on this must say
The fold is honest by construction; an interface is not. These extend BANK/1
§12's rules rather than replacing them, and they are the ones bursa is most
likely to get wrong.
**The leg's word is the truth; the match's word is a summary.** Never show a
match as complete while a leg is `provisional`. A trade is as final as its least
final leg.
**"Matched" must not read as "traded".** A match is an invitation (§4.4). The
word beside it says what has to happen next and who has to do it — and the set
of matches whose wording promises a settlement must be **exactly** the set some
screen is offering to settle, asserted as an iff over every scenario, because
banca learned that lesson the expensive way with a wallet that read *"the banker
has not acknowledged it yet"* beside a banker's tab that read *"nothing is
waiting"*, on the same mint, forever.
**Say what publishing a bank does, before the button.** §2. Not after, not in a
tooltip: "a chip with its meaning parked behind a hover is the same lie with an
extra step."
**Do not say first-come-first-served.** §5.3.
**Show the issuer, always.** Two banks may both mint `LEI`. A code without its
issuer is not a currency.
**Every untrusted string is a text node.** `ctx`, bank names, currency symbols.
`test/source-hygiene.test.mjs` forbids `innerHTML`, `outerHTML`,
`insertAdjacentHTML` and any `esc` function outright, with an empty allowlist.
**The link is the capability, and the sheet that shows it says so.** Anyone
holding a market link replicates the whole book — and every bank published into
it — forever.
|