Dealer pools (instant swaps)
Instant swaps against liquidity instead of a waiting maker. A dealer pool is a constant-product
market between two coins, backed by a liquidity provider's own coins at one address. The
market board quotes from the pool's coins; the taker builds and signs the whole swap; the dealer's
service (arrekta-dealer) checks its side and signs last, usually within a second or two. One
transaction: both coins move or nothing does. Nobody holds anyone else's coins.
Implemented in node-rs/crates/arrekta-market (pool.rs, pools.rs), node-rs/crates/arrekta-dealer,
both SDKs (pool.ts, pool.py, market client calls), @arrekta/wallet-core, the Instant tab of
each wallet's Swap screen, and the explorer's Market page.
1. What others do (research)
- XRPL AMM (XLS-30) and Stellar liquidity pools (CAP-38) are native: pools are ledger objects, priced by the protocol, so nobody has to be online. XRPL adds a continuous auction slot that lets arbitrageurs trade at a discount and pays the proceeds to liquidity providers (XRPL AMM integration, XLS-30 on mainnet). Stellar chose the minimal design: constant-product pools between any two assets with a fixed 0.3 % fee, and path payments that route through pools when they beat the order book (Stellar: AMMs, Liquidity on Stellar).
- Uniswap v2's constant-product curve with a 0.3 % fee on the input is the reference formula:
out = in·997·R_out / (R_in·1000 + in·997)(whitepaper). - SideSwap (Liquid) has no on-chain AMM: its instant swaps are market orders into an order book where competing dealers quote, settled as atomic swaps, with no custody: if the service disappears, the next instant swap simply fails (Citadel Dispatch transcript, Liquid blog, Stacker News).
- 0x RFQ separates an indicative price from a firm quote: market makers reserve assets for a firm quote, which is valid for about a minute (0x RFQ, integrating RFQ).
- Cardano DEXs (eUTXO, like Arrekta's UTXO model) hit contention: a pool held in one UTXO can be spent by one transaction per block, so Minswap and others use off-chain batchers, which control ordering and can extract value from users (Minswap batchers, IOG: architecting dApps on eUTXO, Cardano forum on batcher slippage).
Why dealer pools, not native pools (yet). Arrekta's spending policies (PROTOCOL.md §4.2) are a
fixed language of keys, hashlocks and timelocks: no script can enforce x·y ≥ k, so a trustless
on-chain pool needs a consensus change (a reserved lock kind, §4.3, activated by version bits).
Dealer pools give the same experience now on the existing swap protocol, keep each provider's
coins in their own keys, and leave room for native pools later.
| Choice | Ours | Why |
|---|---|---|
| Pricing | Uniswap v2 constant product, fee on the input, per-pool fee (default 0.3 %, at most 10 %) | The known formula; computed identically in Rust, TypeScript and Python (shared fixture). |
| Signing | The taker signs first, the dealer last | Each signature covers the whole transaction (§6.4); the dealer checks the price on its own reserves before it signs. |
| Live price vs quote | Indicative price while typing, firm quote on Review (30 s) | As 0x: a firm quote sets the pool's coins aside, so typing must not. |
| Contention | Pools keep several coins (split), quotes pick free ones, swaps chain on unconfirmed change (up to 20 deep; the mempool allows 25 ancestors) | Several takers can swap at once and in the same block, without a batcher. |
| Ordering | The board sequences quotes (each priced after the ones before it); the dealer allows a 0.5 % tolerance | Concurrent quotes that land out of order still fill, and a dealer never pays more than its curve price plus the tolerance. |
| Liveness | A pool is quoted only while its dealer polls with a signed heartbeat | No stale quotes from a dealer that went away. |
2. How a swap works
- Price (
POST /market/v1/pools/quote,firm: false): the board picks the online pool that pays the most for the amount and returns the amounts, price and price impact. Nothing is set aside. - Quote (same call, firm): the taker names its own unspent coins that pay for the trade
(
taker_coins). The board checks them on chain and that they hold at least what the pool asks. Each coin backs one open quote: a new quote with the same coins replaces the old one. The board then takes a lock, computes the pool's reserves (below), prices the trade, and picks free pool coins of the asset it pays, largest first, preferring confirmed ones. The quote holds them for 30 seconds and names the highestvalid_until(tip + 10). - Build and sign (
buildPoolSwap): inputs are the quoted pool coins, then the taker's; outputs pay the poolgive, return its change, pay the takerwantand the taker's change. Version 2 withvalid_until. The taker pays the network fee, out of what it receives when it receives ARRK. The swap must spend every coin named for the quote. The taker signs its inputs. - Post (
POST /market/v1/pools/swaps): the board checks the swap against the quote (check_swap): exactly the quoted pool coins, unsigned; the pool receives at leastgive, pays at mostwant, loses nothing else; every other input is the taker's, signed, valid and unspent; no issuer actions; a fee is paid. - Dealer (long poll
POST /market/v1/pools/{id}/poll): checks it again on its own view of its reserves (judge): only its coins, the trade within its size limit, the payout at most the curve price plus the tolerance. It signs, broadcasts, and reportsbroadcast; or reports a signedrejected. Unanswered swaps expire after 60 seconds. - The board's watcher records the trade (and its price) when the transaction confirms.
Reserves as the next quote sees them: the pool's confirmed coins not being spent in the mempool, plus the change outputs of its swaps already broadcast, adjusted by every swap posted but not yet broadcast and every open quote. The dealer keeps the same view of its own coins.
3. API
Anonymous through the gateway (the read rate budget), like the rest of the market board.
| Method | Path | Body / returns |
|---|---|---|
| GET | /market/v1/pools?base"e | {pools: [pool]} |
| GET | /market/v1/pools/{id} | pool |
| POST | /market/v1/pools/quote | {give, want, amount, exact: "in"|"out", pool?, firm?, taker_coins} → price (firm: false), or firm quote {id, pool, give, want, price, price_impact_bps, expires, valid_until, coins}; taker_coins = [{outpoint, address}], required when firm |
| POST | /market/v1/pools/swaps | {quote_id, swap} (base64 PST) → swap |
| GET | /market/v1/pools/swaps/{txid} | swap: {txid, pool_id, state, error, height, pool_receives, pool_pays} |
| POST | /market/v1/pools | dealer: {address, public_key, base, quote, fee_bps, at, signature} |
| POST | /market/v1/pools/{id}/close | dealer: {at, signature} |
| POST | /market/v1/pools/{id}/poll | dealer: {at, signature, wait ≤ 12} → {swaps: [{txid, swap, pool_receives, pool_pays}]} |
| POST | /market/v1/pools/{id}/swaps/{txid} | dealer: {state: "broadcast"} or {state: "rejected", error, signature} |
A pool is \{id, pair, base, quote, address, fee_bps, state, online, reserves: \{base, quote\}, price, created_at\}. Swap states: posted → broadcast → confirmed, or rejected, expired,
dropped. Errors: insufficient-liquidity, pool-busy (its coins are set aside for a moment),
taker-coins, insufficient-funds, coin-spent, coin-in-quote (a coin is in a swap the pool
has not answered), quote-expired, pool-closed, swap-terms, swap-unsigned, bad-signature, stale-signature,
address-in-pool.
Pool id: hex(TH("market-pool", "{address}:{base}:{quote}")). Dealer messages (PROTOCOL.md
§3.5, origin arrekta-market, signed by the pool address's key):
Run pool {id} ({base}/{quote}) at {address} with a fee of {fee_bps} bps on the {network} market at {at}
Close pool {id} on the {network} market at {at}
Pool {id} is online on the {network} market at {at}
Reject swap {txid} in pool {id} on the {network} market
Registrations and closes must be at most 10 minutes old and newer than the last accepted one; heartbeats at most 2 minutes old and never older than the last. One address backs one pool.
| SDK | TypeScript | Python |
|---|---|---|
| curve | poolAmountOut, poolAmountIn, poolPriceImpactBps | pool_amount_out, pool_amount_in, pool_price_impact_bps |
| taker | listPools, getPool, pricePoolSwap, quotePoolSwap, buildPoolSwap, postPoolSwap, getPoolSwap | list_pools, get_pool, price_pool_swap, quote_pool_swap, build_pool_swap, post_pool_swap, get_pool_swap |
| dealer | poolId, pool*Message, signPoolMessage, registerPool, closePool, pollPool, reportPoolSwap | pool_id, pool_*_message, sign_pool_message, register_pool, close_pool, poll_pool, report_pool_swap |
Shared fixture: sdk/fixtures/pools.json (curve vectors up to 2^64, ids, signed messages, one
built swap), generated by sdk/fixtures/gen_pools.py and checked by the Rust board too.
4. Wallets
Call (@arrekta/wallet-core) | Does |
|---|---|
pools() | open pools |
poolPrice({give, want, amount, exact}) | indicative price for display |
poolQuote(…) → preparePoolSwap(quote) | firm quote backed by the wallet's coins (takerCoins), then the built swap spending them, and its preview (coin warnings included) |
confirmPoolSwap(id) | signs and posts; a swap record with pool: {id, quoteId, txid, state} |
poolSwapStatus(recordId) | completed once the dealer broadcast it; cancelled (coins free) if refused or expired; syncBoard also checks |
Apps: the Swap screen opens on Instant: pick what you pay and receive, type an amount, and the estimated amount, price, price impact (caution from 3 %, danger from 10 %) and pool fee update as you type. Review swap takes a firm quote and shows the exact preview with a countdown; Swap now signs and follows it until the pool has signed (usually within seconds).
5. Running a dealer
# .env
ARREKTA_DEALER_POOLS=GOLD/ARRK:30 # A/B:fee_bps,… (tickers or asset ids)
./all_svc/arrekta-compose.sh build dealer
./all_svc/arrekta-compose.sh up platform dealer
curl http://127.0.0.1:29009/healthz # each pool's address, reserves, coins
- The seed is
.secrets/dealer.seed(created on firstup); pool i uses keym/…/0/i, so every pool has its own address. Deposit both coins there, plus a little ARRK for the split transactions when neither side is ARRK. To withdraw, stop the dealer and spend the coins with the seed. - Settings:
ARREKTA_DEALER_TOLERANCE_BPS(50),ARREKTA_DEALER_MAX_TRADE_BPS(2000: the largest payout as a share of that reserve),ARREKTA_DEALER_SPLIT(8 coins per asset, re-split when idle and fewer than half are left). - The dealer talks to the board through the gateway like any outside provider would.
6. Limits and next steps
- Risk to providers. A dealer pool has the impermanent loss of any constant-product pool, and it trades only while the dealer runs. A pool's coins are spendable by its key at any time.
- Quote reservations. A firm quote holds pool coins for 30 seconds. Because each of the taker's coins backs at most one open quote, keeping a pool busy costs real coins, as listing an offer on the board does; the gateway's per-client rate limit applies on top.
- Several providers in one pool. Today each pool is one provider. Shared pools with LP shares need either a custodian (regulated) or native pools.
- Native pools. A soft fork that defines a pool lock kind (PROTOCOL.md §4.3) with reserve and invariant rules would make pools trustless and dealer-free, as on XRPL and Stellar. It is a consensus change: PROTOCOL.md, both node implementations and the test vectors.
- Exchange listings for ARRK, so board and pool prices can be shown in dollars.