Sable Dark Pool
Find the other side before revealing the order.
A participant submits an indication of interest — an asset, a side, a size band, a price band, settlement preferences, an expiry — and Sable privately tells them whether a compatible counterparty exists. The terms are sealed at rest and opened only in memory to run the matcher. They are never shown to a counterparty; a match discloses the agreed overlap, and nothing else.
Read this first: what this is, and what it is not
This is matching infrastructure, and the scope is not a disclaimer bolted onto a trading product — it is the design.
- Sable Dark Pool is not an exchange, ATS, broker-dealer, dealer, or securities venue, and must not be described or operated as one.
- There is no order book. Sable publishes no depth, no resting-intent counts, no last price, and no bucketed aggregate — for any market, to anyone. Those endpoints do not exist.
- There is no execution. A match is Sable's statement that two parties declared overlapping interest. It binds nobody.
- Matching does not guarantee settlement. That sentence is returned on the wire in every response, not only written here.
- There is no custody, ever. Sable never holds an asset, never holds cash, never nets, and is never a counterparty. After both parties confirm, Sable signs a settlement instruction; the parties transact directly on-chain themselves, and Sable then verifies that transfer and signs a receipt.
- Eligible use is restricted pending legal and market-structure review. A
dark-pool-like product creates regulatory, market-structure, custody and
licensing obligations that vary by jurisdiction and asset type. The whole
subsystem is therefore off by default and is enabled by the operator only
for the specific assets a completed legal review covers. Where it is not
enabled, every endpoint answers
501and says so. Operators: complete that review before enabling this, and take your own counsel on whether the assets you intend to list can be matched at all in your jurisdiction. Sable ships the infrastructure; it does not and cannot give you that advice.
Sable is a facilitator and a witness. Nothing in this product makes it a counterparty, a custodian, or a venue.
The lifecycle
Every transition is timestamped and appended to a per-intent, append-only
history you can read at GET /v1/intents/:id.
| State | Meaning |
|---|---|
created | Accepted and sealed. |
active | Resting, and visible to the matcher. |
matched | Locked into a match with a counterparty. |
expired | Its TTL passed. An expired intent is never matchable. |
cancelled | Withdrawn by its owner. |
settlement_pending | Both parties confirmed; a settlement instruction is signed. |
settled | The on-chain transfer was verified. |
failed | The settlement window closed without a verified transfer. |
Privacy: what is sealed, and what is not
Intent terms — quantity range, price range, settlement preferences, exclusions, your private note — are stored, because you cannot match against an order you did not keep. They are AES-GCM-sealed with the gateway master key, opened in memory only to run the matcher or to answer your own read, and never logged. The database holds a sha256 commitment of your terms plus routing labels (asset, side, status, expiry) and has no column that could hold a price or a size.
This is a deliberate, disclosed exception to Sable's normal rule that content is never persisted — the same posture as hosted-agent code, Relay content and Vault metadata. See the privacy contract.
Matching is deterministic and versioned
The rules are published, and the version in force is stamped on every intent and
every match (sable-match/v1). There is no preferential tier: no account is
matched ahead of another, and no rule is hidden.
- Same asset.
- Opposite side.
- Price compatibility — the bands must intersect. The match settles at the midpoint of the overlap, rounded down; neither side is favoured, and the overlap is disclosed to both parties so neither has to trust the midpoint blind.
- Quantity compatibility — the size bands must intersect. The match takes the largest size both parties said they would do.
- Settlement compatibility — at least one shared rail, taken in
lexicographic order. v1 verifies
evm:<chain_id>rails. - Counterparty eligibility — neither account suspended, the asset on the operator's reviewed allowlist, and neither party excluded by the other.
- Beneficial-owner overlap is refused. The same account on both sides, or two accounts sharing a linked wallet, is a wash pair and never matches.
- Time priority breaks ties: earliest intent first, then intent id.
Because the engine imposes that total order before pairing anything, the same set of intents produces the same matches regardless of the order they were submitted, read back, or raced into the gateway.
Controlled disclosure
Disclosure is staged, and each stage releases the minimum required for the next step.
- A proposal. You learn there is a match, the agreed quantity, the agreed price, the overlap band it came from, and the settlement rail. You learn nothing about who the counterparty is, what their band was, or how large their interest is.
- Both parties confirm. Only then does Sable mint and sign a settlement instruction naming the seller's own payee address. The counterparty's account is never disclosed at any stage.
- The parties settle. The buyer pays the seller directly on-chain and submits the transaction hash. Sable verifies it and signs a receipt.
Before the pair is locked in, the gateway re-confirms both intents are still active, re-checks eligibility under a row lock, and only then writes the match — so a withdrawal that races a match cannot produce both, and cannot disclose.
Leakage controls
Inferring hidden order flow is the attack this product exists to prevent, so the controls are part of the contract rather than a hardening pass.
- Uniform negatives.
POST /v1/matchesreturns exactly two shapes, and the negative one is identical — bar its timestamp — whether there was no counterparty, an ineligible counterparty, a refused wash pair, an ineligible asset, or an intent that is no longer resting. It carries no reason field, because a reason field is a side channel. Do not infer one. - Uniform not-found. An intent or a match that is not yours answers
404, indistinguishably from one that does not exist. - Constant timing. Every match attempt is padded to a fixed floor, so the time an answer took is not itself an answer.
- Probing detection. Attempts are recorded per account. Sustained probing
raises an operator alert and answers
429. - No aggregates.
GET /v1/markets/:asset_id/statusreports the deployment's eligibility configuration and your own participation. It reports nothing about anyone else — not even a bucketed count, because in a thin market a bucket identifies one participant.
Market-abuse controls
- Wash trading is refused by the matcher itself: same account, or two accounts sharing a linked wallet.
- Spoofing / excessive cancellation — an account whose 30-day cancelled-to-total ratio exceeds the operator's limit, over a minimum sample, is refused new intents and flagged for review.
- Resting-intent caps bound any one participant's share of the book.
- Investigation hooks — refusals are recorded as content-free signals (a kind, an account, counts) for a human to review. They are never an automatic penalty.
API
All routes are session-authed and answer 501 where the operator has not
enabled matching. Full shapes in the API reference.
| Method | Path | What it does |
|---|---|---|
POST | /v1/intents | Submit an indication of interest (terms sealed). |
GET | /v1/intents/:id | Your own intent, its terms, and its full history. |
POST | /v1/intents/:id/cancel | Withdraw a resting intent. Idempotent. |
POST | /v1/matches | Ask whether a compatible counterparty exists. |
GET | /v1/matches/:id | A match you are a party to, at its current disclosure stage. |
POST | /v1/matches/:id/confirm | Confirm your side. Idempotent. |
POST | /v1/matches/:id/settle | Verify the on-chain transfer you made, and get a signed receipt. |
GET | /v1/markets/:asset_id/status | Eligibility rules and your own participation. |
Submitting an intent
curl https://api.buildsable.com/v1/intents \
-H "authorization: Bearer $SABLE_SESSION" \
-H 'content-type: application/json' \
-d '{
"asset_id": "fund-a",
"side": "buy",
"quantity_min": 1,
"quantity_max": 100,
"price_min_micro_usd": 1000,
"price_max_micro_usd": 2000,
"settlement_rails": ["evm:1"],
"expires_in_secs": 86400
}'
Prices are per unit, in micro-USD (1,000,000 µ$ = $1). Quantities are integers in the asset's own units; Sable does not interpret the scale, and both parties must mean the same thing by it.
Asking for a match
curl https://api.buildsable.com/v1/matches \
-H "authorization: Bearer $SABLE_SESSION" \
-H 'content-type: application/json' \
-d '{"intent_id": "int_…"}'
A hit discloses the overlap:
{
"matched": true,
"match": {
"id": "mt_…",
"your_side": "buy",
"status": "proposed",
"quantity": 60,
"price_micro_usd": 1750,
"price_band_low_micro_usd": 1500,
"price_band_high_micro_usd": 2000,
"settlement_rail": "evm:1",
"counterparty": null,
"both_confirmed": false
}
}
A miss is uniform, and carries no reason:
{
"matched": false,
"rule_version": "sable-match/v1",
"checked_at": "2026-09-06T10:47:27Z",
"note": "No match was made. This response is identical whatever the cause, by design."
}
Confirming and settling
Once both parties confirm, Sable returns a signed settlement instruction
that verifies through the public
POST /v1/receipts/verify — the same endpoint every other
Sable signature uses. Signing an instruction is not settling it:
{
"settlement_instruction": {
"payee_address": "0x…",
"chain_id": 1,
"amount_micro_usd": 105000,
"how": "The buyer pays the payee address DIRECTLY on-chain, then POSTs the transaction hash to /v1/matches/{id}/settle. Sable verifies that transfer and signs a receipt. Sable never holds the funds."
}
}
The buyer makes the transfer from a wallet linked to their account, then:
curl https://api.buildsable.com/v1/matches/mt_…/settle \
-H "authorization: Bearer $SABLE_SESSION" \
-H 'content-type: application/json' \
-d '{"tx_hash": "0x…"}'
Sable reads the transaction from an RPC node, matches it against the instruction it signed, and returns a signed settlement receipt. Read the receipt's two verification fields honestly:
cash_leg: "VERIFIED"— Sable found the on-chain transfer of the agreed amount from the buyer's linked wallet to the seller's payee address.asset_leg: "UNAVAILABLE"— Sable cannot observe delivery of the asset and does not claim to. A missing input is never reported as a verified one.
Webhooks
Content-free by construction — ids, the asset, the side, the status, and for a settlement the transaction hash the parties themselves published. Never a quantity, a price, a band, or a counterparty: a webhook body travels to a third-party URL, which is the last place order terms belong.
intent.created · intent.matched · intent.expired · intent.cancelled ·
match.confirmed · settlement.pending · settlement.completed ·
settlement.failed
See webhooks.
For operators
Matching is off unless both of these are set, and it is off for every asset not named:
SABLE_INTENT_MATCHING_ENABLED=true
SABLE_INTENT_MATCHING_ASSETS=fund-a,fund-b
The remaining knobs — resting-intent caps, TTL bounds, the match-attempt timing
floor, the probing thresholds, and the cancellation-ratio limit — are documented
in .env.example. The timing floor in particular is the timing side channel's
only defence; do not set it to zero outside a test.