Skip to main content
Version: 0.5.0

Market Maker

Overview

The Market Maker (MM) is an off-chain service that prices and fills EBSwap swaps — encrypted-balance swaps between two token instances under the same Hub, where the swapped amounts stay private on-chain. The MM controls its own encrypted liquidity EPKs (registered to the MM's own key in the Hub, not to EBSwap): it validates a client's swap quote against the zkStables Rate Provider, hands back the maker EPKs and a stateless signed quote (a random salt plus an HMAC the MM re-checks at submit), and fills the client's signed orders atomically. The client prices the swap at the rate provider, computes the amounts, and assembles and signs the swap intent; the MM validates it and submits the fill.

EBSwap is a stateless settler: it holds no funds, controls no EPKs, and stores no state. A swap is a single EBSwap.atomicSwap transaction with two symmetric legs, each a normal encrypted-balance transfer that its owner authorized to EBSwap:

  • taker legamountIn of tokenIn moves from the taker's source EPK to the maker's receive EPK;
  • maker legamountOut of tokenOut moves from the maker's liquidity EPK to the taker's receive EPK.

Both amounts are committed as ElGamal ciphertexts at quote time and bound into a single intentHash that both parties sign. Either both legs execute or the transaction reverts. The client signs its order off-chain and the MM submits the transaction, so the swap is gasless for the client and there is no on-chain setup step.

Each leg also carries its source's own PendingBalanceFlags ({clear, enabled}) — pending-balance management applied to that leg's sender after the transfer (see Encrypted Balances → Pending Balance). The flags are bound into that leg's transferAuth (the ZKEMT paramsHash), so the counterparty and the submitting relayer cannot alter them — a mismatch reverts the whole swap. Each side chooses its own: a taker typically passes the no-op {clear: false, enabled: false}, while a maker consolidating received liquidity may clear (fold pending into spendable) and keep pending armed with enabled: true.

Recommendation: both source EPKs should arm pending protection before swapping. Each leg's proof commits to its source's current encryptedBalances; an attacker who front-runs the swap with any incoming transfer to a source shifts that balance and invalidates the leg's proof, reverting the swap. With pending enabled, incoming funds route to pendingBalances and the committed balance stays stable. Arming must happen in a prior transaction — the per-leg flags above apply only after the transfer, so they cannot protect their own swap.

Use a dedicated swap EPK. Fund a fresh, swap-purpose EPK with just the amount being swapped rather than sourcing from a main-balance EPK. A committed source EPK is effectively tied up for the swap window — its encryptedBalances must not change until settlement, and arming its pending reroutes all its incoming funds — so isolating that to a throwaway EPK leaves the main account fully spendable in the meantime, caps exposure at the swap amount, and composes with the fresh-controller-key deniability model.

Trust model. EBSwap custodies nothing and has no owner and no maker allowlist. Each leg is authorized by the key controlling its source EPK, so neither side can be made to pay without receiving the committed counter-amount, and an EBSwap bug cannot move funds beyond a swap both parties signed. Pricing is anchored to the zkStables Rate Provider: the client sources its price there and the MM cross-checks every order against the same rate provider, so the MM cannot fill at terms other than the rate provider's. A taker is exposed only to the MM declining to fill; since nothing is escrowed, the taker "cancels" simply by letting the order expire.

zkStables Rate Provider. The price + fee source is the zkStables Rate Provider, a separate service in this repo (Rate Provider) returning { price, feeBps, timestamp }. The client queries it (getQuote) to price the swap; the MM verifies the exact quote after the fact via getQuoteAt to cross-check the order. It is queried by asset symbol (zkEUR, EURC), not token address, so the client and MM each map their swap tokens to symbols before calling. Its interface is documented under External dependencies.

Swap flow

Encryption & randomness

Both legs must reproduce, bit-for-bit, the ciphertexts committed in the intent — EBSwap rejects a mismatch. The client builds both ciphertexts (it knows both plaintext amounts: it computed amountIn and amountOut from the rate quote, and the MM validated them), choosing its own randomness:

  • amountInCiphertext = Enc(amountIn, makerReceiveEpk, rIn) — used as the recipient amount in the client's taker-leg proof, so rIn stays with the client. The MM verifies amountIn by decrypting amountInCiphertext (it is encrypted to makerReceiveEpk, which the MM controls), so it never needs rIn.
  • amountOutCiphertext = Enc(amountOut, takerReceiveEpk, rOut) — the MM must reproduce this in its maker-leg proof, so the client shares rOut (and amountOut) with the order. That leaks nothing — the MM is the counterparty: amountOut is sent in plain, and rOut lets the MM both check amountOutCiphertext encodes it and reproduce it for the proof. (The client, conversely, owns takerReceiveEpk and can decrypt to confirm its own output.)

intentHash = keccak256(abi.encode(SwapIntent)) binds the token pair, all four EPKs, both ciphertexts, expiry, and intentSalt, and both parties sign it (takerIntentAuth, makerIntentAuth). Chain and contract binding come from the EIP-712 domain separator of those authorizations, so an intent cannot be replayed across chains or EBSwap deployments.

Pricing & fee

The zkStables Rate Provider returns price (decimal, tokenOut per tokenIn) and feeBps (integer basis points). There is no separate fee legatomicSwap has exactly two legs — so the fee is folded into one of the two amounts. feeToken (one of tokenIn / tokenOut, chosen by the client) selects which side carries it. Given the rate provider price:

feeToken = tokenIn (fee on the input side): amountIn = ceil( (amountOut / price) * (1 + feeBps / 10_000) )
feeToken = tokenOut (fee on the output side): amountOut = floor( (amountIn * price) * (1 - feeBps / 10_000) )

The client fixes whichever amount it cares about and solves for the other from the relation for its feeToken; rounding goes toward the maker (amountIn up, amountOut down), so the maker never loses to rounding. Both amountIn and amountOut are plaintext known to both parties. feeToken is an off-chain quote attribute only — it is not part of SwapIntent; on-chain the two legs just move the final amountIn and amountOut.

The MM does not set the price; it cross-checks, exactly. On mm_quote it calls getQuoteAt(symbolIn, symbolOut, rateQuote.timestamp) itself (over HTTPS from the constant rate provider URL) and requires the returned quote to equal the client's submitted rateQuote — no slippage band, because the value is canonical, which is also why no quote signature is needed. It then enforces freshness (timestamp within a max age) and — using the client's feeToken to pick the relation above — checks (amountIn, amountOut) are consistent with that quote's price/feeBps. It needs feeToken because the amountIn ↔ amountOut relation differs by side. A slippage band, if the MM wants one, is now only an optional risk control against price drift since timestamp, not an authenticity check. So the MM cannot fill at terms other than the rate provider's, and both sides hold the same (amountIn, amountOut) pair before any ciphertext is built.

Quote authentication (stateless)

mm_quote persists nothing. It mints a random intentSalt and authenticates the quote with an HMAC over the agreed terms and that salt:

intentSalt = random 32 bytes
quoteHmac = HMAC(QUOTE_HMAC_SECRET, { tokenIn, tokenOut, amountIn, amountOut, makerReceiveEpk, makerEpk, expiry, intentSalt })

The MM returns both; the client puts intentSalt in the intent and signs as usual, and at submit forwards quoteHmac plus the plaintext amountIn/amountOut (off-chain - the MM is the counterparty and learns them anyway). At mm_submitTakerOrder the MM re-derives the HMAC (no lookup) from the intent's tokens / EPKs / expiry / intentSalt and the provided amounts, and requires it to equal quoteHmac. A match proves the MM issued this exact quote — amounts included, so a lie about them fails the HMAC — and that makerEpk is one of its own. It then verifies (not recovers) that the ciphertexts encode those amounts — cheap scalar-mult checks, no discrete-log.

Single-use. A quote redeems into at most one order: orders.intentSalt carries a UNIQUE constraint, so a second order reusing the salt is rejected. This keeps a quote a one-swap offer — N fills need N quotes, each re-priced against the live rate provider, so a drifted price can't be reused. On a failed/expired order the client simply re-quotes (fresh salt, fresh price).

QUOTE_HMAC_SECRET is the MM's symmetric key — a random startup value if unset (restart → re-quote, fine), or set in env to share across instances. Keep it secret: forging it mints arbitrary terms, bounded by the fill-time economic sanity check.

Endpoints

Transport is JSON-RPC 2.0 over a single POST / endpoint (method names below), except health which is GET /health. All integer fields (amountIn, amountOut, expiry, intentSalt, the rate quote timestamp, rOut, nonces, deadlines) are decimal strings; amount fields (amountIn, amountOut) must be in 1..=2^64-1 (the transfer-circuit domain); addresses, proofs, signatures, and quoteHmac are 0x-hex; a Point is { x, y } with 32-byte 0x-hex coordinates; an ElGamalCiphertext is { R: Point, C: Point } (compatible with the SDK's 256-hex serialized form). Field names mirror the on-chain SwapIntent.

mm_quote

The client submits its own rate-provider-priced amounts plus the rate quote it used; the MM cross-checks, assigns the maker EPKs, and returns a stateless signed quote — a random intentSalt plus a quoteHmac (see Quote authentication). It stores nothing and does not lock liquidity.

Params { tokenIn: symbol, tokenOut: symbol, amountIn: string, amountOut: string, feeToken: symbol, rateQuote: RateQuote }feeToken is tokenIn or tokenOut (the fee side; see Pricing & fee). Result { tokenIn: symbol, tokenOut: symbol, amountIn: string, amountOut: string, feeToken: symbol, makerReceiveEpk: Point, makerEpk: Point, expiry: string, intentSalt: string, quoteHmac: string }

The request token fields are rate-provider symbols; the MM resolves them to on-chain addresses and binds the addresses in the quoteHmac (they are what the SwapIntent carries).

The MM calls getQuoteAt(symbolIn, symbolOut, rateQuote.timestamp) and requires it to equal the submitted rateQuote (exact match), enforces timestamp freshness, checks (amountIn, amountOut) are consistent with the quote's price/feeBps and feeToken (which selects the amountIn ↔ amountOut relation), and confirms it currently holds ≥ amountOut of tokenOut (advisory — see below). If all pass it assigns makerReceiveEpk (taker-leg destination) and makerEpk (maker-leg source, with balance ≥ amountOut), sets an expiry, mints a random intentSalt, and computes quoteHmac = HMAC(QUOTE_HMAC_SECRET, terms + intentSalt) — persisting nothing. expiry is the deadline the client puts in the intent and until which the MM intends to fill.

mm_quote does not lock or reserve liquidity — the liquidity check here is advisory (there is nothing to lock; the quote is a stateless signed salt). The binding reservation happens at mm_submitTakerOrder. See Maker operational requirements for the rationale and defenses.

mm_submitTakerOrder

Delivers the client-built, signed swap order. No on-chain step precedes this — the client signs off-chain and the MM submits the swap.

Params { intent: SwapIntent, takerParams: EncryptedTransferParams, takerFlags: PendingBalanceFlags, takerIntentAuth: Auth, takerTransferAuth: Auth, amountIn: string, amountOut: string, rOut: string, quoteHmac: string } Result { status: string }

takerIntentAuth is the EBSwap-domain signature binding hashIntent(intent) (the client's agreement to the full terms); takerTransferAuth is the ZKEMT caller-bound signature (caller = EBSwap) authorizing the taker transfer, and binds takerFlags via its paramsHash (so the MM must submit the same flags the client signed). The client supplies the plaintext amountIn / amountOut, so the MM validates with no quote lookup and no discrete-log, cheapest checks first (see Quote authentication):

  • intent.intentSalt is unused (fast-path; the UNIQUE insert below is authoritative) and intent.expiry is in the future;
  • HMAC(QUOTE_HMAC_SECRET, terms + intentSalt) over the provided amountIn/amountOut equals quoteHmac — authenticating the order and pinning the amounts (a lie fails the HMAC), so forgeries fall before any EC work;
  • the ciphertexts encode those amounts: decrypt amountInCiphertext (under makerReceiveEpk) and compare to amountIn·G; recompute Enc(amountOut, takerReceiveEpk, rOut) (rOut must be a canonical nonzero scalar, below the BN254 scalar-field modulus (the Noir Field order), since it is stored raw for the maker-leg proof) and compare to amountOutCiphertext — a few scalar mults each, not a recovery;
  • takerParams matches the intent (sender = takerEpk, recipient = makerReceiveEpk, recipientAmountCt == amountInCiphertext) and both taker auths recover to the controller of takerEpk.

If those pass, the MM re-prices against the live rate provider — the economic sanity band (see Maker operational requirements) — and then atomically reserves liquidity and inserts the order (Ordered, rOut included for the maker-leg proof). Under a per-tokenOut lock it checks the maker can still cover amountOut given active + pending minus what open (Ordered/Filling) orders already reserve, and rejects with an insufficient-liquidity error if not — so an over-committed quote is refused here, not silently at fill (see Liquidity model). The UNIQUE(intentSalt) insert rejects a quote reused for a second order (and the race the fast-path misses).

mm_supportedTokens

Result Token[] — tokens the MM currently makes markets for (with the Hub they belong to).

mm_swapIntentStatus

Params { swapIntentHash: "0x…" } Result { status: string } — one of the lifecycle states (see Status lifecycle below).

health (GET /health)

Result { status: string }.

External dependencies

zkStables Rate Provider

The price + fee authority — a separate service in this repo; see Rate Provider for its full design. The client prices a swap with getQuote; the MM (and anyone) verify a quote after the fact with getQuoteAt. Both live at a constant HTTPS URL, so transport integrity is enough — quotes are not signed, because getQuoteAt lets a verifier read the canonical value directly. The contract the MM depends on:

getQuote — Params { symbolIn: string, symbolOut: string } Result RateQuote = { price: string, feeBps: number, timestamp: string } — the current quote, for pricing; timestamp is the quote time; price is symbolOut per symbolIn.

getQuoteAt — Params { symbolIn: string, symbolOut: string, timestamp: string } Result RateQuote — the canonical quote as of timestamp (must equal what getQuote returned at that instant). The MM exact-matches a client's rateQuote against it; auditors / third parties use it to verify a settled swap priced fairly for its timestamp. The rate provider must retain history at least as long as the MM's freshness window (longer for the audit use case).

Status lifecycle

The MM tracks an internal status per order (there is no on-chain state to mirror — EBSwap is stateless).

Ordered ──▶ Filling ──▶ Filled
▲ ├─────▶ Failed
│ └─────▶ Expired
└── requeue (transient failure, bounded by attempts)
  • Ordered — client-built, signed order received and validated via mm_submitTakerOrder; waiting for, or between, fill attempts.
  • Filling — a fill worker has claimed the order and is attempting to settle it.
  • Filled — the atomicSwap settled (Swapped(intentHash, …) observed).
  • Failed — terminal: attempts were exhausted, or the maker cannot cover the order.
  • Expired — terminal: expiry passed with the order unfilled.

A transient fill failure returns the order to Ordered for another attempt (bounded by a max-attempt count); a Filling order left stale by a crashed worker is reclaimed the same way — see Fill loop. There is no Cancelled/Refunded state: nothing is escrowed, so an unfilled order simply expires.

Persistence

Quotes are stateless (signed salts, never stored), so the durable surface is small.

In-memory: rate-quote cache. A per-(tokenIn, tokenOut) ring of recent rate quotes, looked up by timestamp and retained for the freshness window, so getQuoteAt verification hits cache instead of the rate provider on the hot path; the head is the current price. Ephemeral, per instance — not in the database.

Durable (PostgreSQL): orders — created by mm_submitTakerOrder, keyed by intentHash:

FieldNotes
intent_hashPK = hashIntent(intent)
intent fieldstokenIn, tokenOut, four EPKs, both ciphertexts, expiry, intentSalt (random; UNIQUE — one order per quote)
quote_hmacthe MM's HMAC over the quote terms + intentSalt
amount_in, amount_outrecovered plaintext amounts
rate_timestampthe live rate the sanity band priced against at submit
r_outclient-supplied opening of amountOutCiphertext, for the maker-leg proof
taker_ordertaker EncryptedTransferParams + takerIntentAuth + takerTransferAuth
fill_tx_hashsubmitted atomicSwap tx
attemptsfill attempt counter
statussee lifecycle
created_at, updated_attimestamps

Durable (PostgreSQL): maker_balances — the plaintext balance mirror, keyed by (token, epk). The chain holds only ciphertexts, so the MM tracks each maker EPK's spendable active and credited-but-unfolded pending balance per token as NUMERIC(78,0) (a full uint256). A fill debits/credits these, folding moves pending → active, and the reconciler re-baselines them to chain truth (see Liquidity model). It is also the reservation denominator: active + pending minus open-order amountOut is what mm_submitTakerOrder gates on.

Liquidity model

The MM runs a single maker EPK for all tokens, in pending mode on each (armed once per token contract via ZKEMT.managePendingBalance(epk, {clear: false, enabled: true})). An encrypted balance lives per token contract, so one EPK holds the MM's balance in every token; with pending on, incoming transfers land in that token's pending balance, never its spendable active one. A token's active balance therefore moves only by the MM's own actions — a fill spends from it, and folding credited pending into spendable active (active += pending, homomorphic, no proof) is fill-driven, not periodic: the fill loop folds a token's pending as part of a fill — the maker swap leg folds in-swap (makerFlags = {clear: true, enabled: true}) when active already covers, or a standalone managePendingBalance fold runs first when it doesn't. The maker stays armed (enabled: true) across the fold, so the protection is continuous. The one EPK is both makerReceiveEpk (when a token is tokenIn) and makerEpk (when it is tokenOut) — the two legs are on different token contracts, so EBSwap allows them to be the same EPK.

This is what lets the MM run swaps on a token concurrently: a swap that receives the token (as tokenIn) accrues to pending and so cannot stale-invalidate an in-flight maker proof that spends that token (as tokenOut). Concurrent spends of one token still serialize on its single active balance, so for throughput the MM shards a token across several EPKs.

Tracked balances & reconciliation. The maker-leg proof needs the spendable amount as a witness, but the chain stores only ciphertexts, so the MM keeps a plaintext mirror of each maker EPK's active and pending per token (the maker_balances table). The mirror is bootstrapped from chain at startup and updated by each fill; a background reconciler then periodically re-baselines it to chain truth — reading the active + pending ciphertexts, decrypting them with the maker key, and recovering the plaintext by discrete-log search. Both the bootstrap and the periodic pass are the same read-decrypt-recover step, so no balance is ever configured by hand; it also corrects drift (an external deposit to the EPK, or a fill that settled on-chain but crashed before recording). The on-chain proof is the hard guard, so a transient mirror/chain mismatch self-corrects on the next cycle and never causes an over-spend.

Fill loop

Filling is a background worker, decoupled from the mm_submitTakerOrder response: submit only validates, reserves, and persists the order (Ordered); a separate loop settles it. The worker claims one order at a time, assembles and submits its fill, and records the outcome. Every step is driven off the database, so the loop is safe to run across multiple instances and survives a crash mid-fill.

Claiming. Each iteration atomically claims the oldest fillable order (Ordered → Filling, bumping attempts) while skipping rows another worker holds (SELECT … FOR UPDATE SKIP LOCKED). An order left in Filling past a reclaim window — a worker that crashed mid-fill — is treated as stale and re-claimed, so no order is stranded. When the queue is empty the worker idles briefly, then polls again.

Balance strategy. The maker leg spends tokenOut from the maker EPK's active balance (the tracked mirror from the Liquidity model). Against the order's amountOut, the worker takes one of three paths:

  • active covers → build the maker-leg proof against active and submit the swap. The maker leg always carries makerFlags = {clear: true, enabled: true}, so it folds tokenOut's pending into active in the swap itself — no separate op. The fold happens after the leg's transfer, so the proof still commits to the pre-fold active, which a stranger crediting pending cannot invalidate; enabled: true keeps the EPK armed.
  • active short, active + pending covers → fold first: submit a standalone managePendingBalance(tokenOut, {clear: true, enabled: true}) op (active += pending, homomorphic, no proof), fold the mirror, then build the maker-leg proof against the settled (folded) balance and fill.
  • neither covers → the order cannot be filled now → Failed.

Settlement. A fill is a single sponsored atomicSwap — submitted as an ERC-4337 UserOp through a bundler with a paymaster covering gas, so the fill is gasless for the MM as well as the client. The maker-leg proof and both maker authorizations are generated immediately before submission, against the current tracked balance, to minimise staleness. On a successful, non-reverted receipt the worker records the fill atomically — in one DB transaction it debits amountOut from tokenOut active, credits the received amountIn of tokenIn to pending, folds tokenOut's pending if the maker leg folded (there was pending), and marks the order Filled (with the tx hash). Committing the balance moves and the status together is what makes recording exactly-once: a crash mid-record rolls the whole thing back, leaving the order Filling with the mirror untouched, so the retry re-records it once instead of double-applying.

Retries. A transient failure — a submit error, or an included-but-reverted op (e.g. a same-token spend that lost the race and staled the maker proof) — requeues the order (Filling → Ordered) for another attempt, bounded by a max-attempt count, after which it goes Failed. Because the proof is rebuilt each attempt against the then-current balance, a stale-balance revert simply retries clean.

Already-settled guard. A fill can land on-chain but crash before it is recorded, so a re-attempt first checks whether this order's atomicSwap already committed: the maker's intent-auth nonce is consumed in EBSwap's authorization bitmap iff the swap executed. If it is, the worker just records Filled instead of re-submitting into a guaranteed revert — it can't reconstruct the prior attempt's exact balance deltas here, so it leaves the mirror to the reconciler, which re-baselines it from chain (the balance authority).

Nonces. Both maker authorizations for a fill use the order's intentSalt as their nonce. EBSwap's intent-auth and ZKEMT's transfer-auth nonces are independent unordered bitmaps (any unused value works — not a sequence), so the per-order-unique salt is a safe nonce for both legs and doubles as the settled-check key above.

Maker operational requirements

  • Liquidity selection & management. Each maker EPK is registered to the MM's own key (its Hub controller) and funded with that token directly through ZKEMTEBSwap has no custody or withdraw path. Selection and the pending-mode operating model are covered under Liquidity model.
  • Advisory at quote, binding reservation at submit. mm_quote is free and unauthenticated, so it must not lock inventory — trivially weaponized into a book-exhaustion DoS, and there is nothing to lock (the quote is a stateless signed salt). The quote does only an advisory liquidity check; the binding reservation is at mm_submitTakerOrder, which under a per-tokenOut lock counts open (Ordered/Filling) orders' amountOut against active + pending and refuses an order it can no longer cover. So concurrent submits can't jointly over-accept, and an over-committed quote fails at submit rather than silently at fill. Still defend the quote surface with per-client rate limits and a short expiry, and shard liquidity across EPKs for spend throughput (concurrent spends of one token serialize on its single active balance). Refusals under contention are a liveness cost, never a safety one.
  • Economic sanity band (at submit). When an order arrives — before persisting it — re-price it against the live rate provider and refuse anything wildly off (a configurable band in basis points, 0..=10000; default 100 bps, i.e. ±1%). It is a sanity band, not the exact-match check at mm_quote, so it does not break the price-lock — it caps the blast radius of a stale or forged quote (or a leaked QUOTE_HMAC_SECRET). Checking at submit fails fast (nothing is stored); a maker may additionally re-check immediately before signing the maker leg for the tightest bound against rate drift between submit and fill.
  • Fill mechanics. How the worker claims orders, picks a balance path, folds pending, submits the sponsored atomicSwap, and retries — plus the proof being generated immediately before submission to minimise staleness — is the Fill loop. Fills that spend one token still serialize on its single active balance, so throughput comes from sharding that token across EPKs. Three knobs are operator-configurable, defaulting to values suited to fast chains: the per-fill auth deadline (how long a submitted fill stays valid; default 300 s), the stale-Filling reclaim window (must exceed the worst-case fill time; default 300 s), and the max attempt count before an order is failed (default 3).
  • Authorization. The maker signs two authorizations per fill, symmetric to the taker's, both verified against the liquidity EPK's controller (the MM's key): makerIntentAuth (EBSwap EIP-712 domain, binds the accepted intentHash) and makerTransferAuth (ZKEMT EIP-712 domain, caller-bound to EBSwap, authorizes the maker transfer via encryptedTransferWithCallerAuth and binds the maker's own makerFlags). The two nonce spaces are independent, both per the MM key, and reuse the order's intentSalt (see Fill loop). A relayer may submit atomicSwap; the MM key need not be the tx sender.
  • Key custody. One MM key both controls the liquidity EPKs (Hub controller) and signs the two fill authorizations; it starts from an env/KMS-held private key, with a path to a hardware/MPC controller (Fireblocks) later without changing the protocol surface.
  • Expiry budget. intent.expiry is set by mm_quote; it must cover the maker's fill window after the order arrives. Keep it short — until the order is filled, the taker leg spends from the client's live source EPK, so a long window raises the chance the client's balance shifts and the proof goes stale (worst case a retry, never a loss). It also bounds the price the MM is on the hook for. The rate quote's timestamp (price-quote time, age-bounded by the MM) is a separate, earlier clock.
  • Inventory & rebalancing — partially deferred. The pending-mode operating model and its fill-driven managePendingBalance fold (see Liquidity model) are the baseline and in scope — the fold mutates the active ciphertext, and rides with the fill that triggers it, so it never races that EPK's own in-flight fill. What the MVP still defers is the automated inventory manager: sharding a token across a pool of EPKs (to get past same-EPK fill serialization), and cross-token rebalancing — converting accrued tokenIn back into spendable tokenOut liquidity (a reverse swap, redeposit, or external desk). The MVP assumes the maker EPK is pre-funded in each token.