Zerochord
How it works

Venue adapters

The one interface every venue is reduced to, the pool state it produces, and the adapters that ship.

Every venue is reduced to one interface so the quote engine never branches on a venue name. The execution class is part of that interface, because it decides which transaction path is used and what the interface is allowed to promise.

The interface

interface VenueAdapter {
  readonly id: string;
  readonly displayName: string;
  readonly executionClass: ExecutionClass;
  readonly networks: readonly Network[];

  isAvailable(network: Network): boolean;
  discoverPools(provider: ChainProvider, network: Network): Promise<PoolState[]>;
  decodePool(utxo: Utxo, network: Network, readAt: Tip): PoolState;
  quote(input: SwapQuoteInput): SwapQuoteResult;
}

discoverPools finds every pool of the venue on the network, decoded, all read at one tip. Discovery is by payment credential, because a venue's pools sit at many addresses that share one payment credential and differ in their staking part.

quote returns the exact output for an input, in the venue's own integer arithmetic and with the venue's own rounding. Never floating point: an off-by-one in rounding produces an order the validator rejects, not a slightly worse price.

The adapters that ship

AdapterVenue idClassCurve
Dano Finance CLMMdanogo-clmmKernelConcentrated liquidity
Minswap V2minswap-v2CommitmentConstant product
Minswap Stableswapminswap-stableCommitmentStableswap
SundaeSwap V3sundaeswap-v3CommitmentConstant product
SundaeSwap Stableswapssundaeswap-stableCommitmentStableswap

Which of these run on a given network comes from config/<network>.json. A venue that is disabled there, or whose poolScriptHash is null, is skipped with its reason recorded.

PoolState

interface PoolState {
  identity: PoolIdentity;
  executionClass: ExecutionClass;
  assetA: AssetRef;
  assetB: AssetRef;
  reserveA: bigint;
  reserveB: bigint;
  rawValueA: bigint;
  rawValueB: bigint;
  curve: CurveParameters;
  cost: VenueExecutionCost;
  risk: PoolRiskFlags;
  supportsAtomicMultiHop: boolean;
}

Tradeable reserves are not the UTxO value

reserveA and reserveB are the tradeable reserves after every venue specific exclusion has been applied. rawValueA and rawValueB are what the UTxO literally holds.

The two differ, and the difference is the point. The validator reads the datum, so quoting from the UTxO value produces orders the validator rejects.

There is deliberately no decimal count

Decimals are a property of an asset, not of a pool, and their single source of truth is the network's asset registry. A consumer that needs them resolves them there by asset reference.

Carrying a number the adapter never read would invite a consumer to trust it, and a wrong decimal count is a pricing error by orders of magnitude rather than a cosmetic one.

Identity carries the chain state

interface PoolIdentity {
  venueId: string;
  /** `txHash#index` of the pool UTxO this state was decoded from. */
  outRef: string;
  poolId: string;
  address: string;
  readAt: Tip;
}

Curve parameters

CurveParameters is a discriminated union so the maths cannot be mixed up.

{
  type: "constant-product";
  /** Fee numerator per direction; the two can differ on some venues. */
  feeNumeratorAToB: bigint;
  feeNumeratorBToA: bigint;
  feeDenominator: bigint;
}

Risk flags change what the product may promise

PoolRiskFlags are not diagnostics. Each one changes what can be said before a user signs, and each one travels to the quote's warnings array.

FlagWhat it means
dynamicFeeThe pool's fee can change after the quote. Such a route cannot honour a fill-at-the-quoted-rate promise.
noOrderExpiryThe venue's order carries no expiry, so a stale order needs a paid cancel.
permissionedBatchingBatching is permissioned, so a fill depends on a whitelisted third party.
notesThe plain language reasons behind the flags above, for display and for logs.

routing.dynamicFeePolicy decides whether a dynamicFee pool is excluded from routing outright or kept and labelled.

Execution cost

interface VenueExecutionCost {
  /** Batcher or execution fee in lovelace. Null when the venue has none. */
  executionFeeLovelace: bigint | null;
  /** Refundable minimum ada that must ride along with the order. */
  depositLovelace: bigint | null;
  /** True when the fee above is a worst case rather than a fixed amount. */
  feeIsWorstCase: boolean;
}

feeIsWorstCase covers venues whose fee is amortised across a scooped batch. The quote shows the worst case and says so, and a total containing one upper bound is itself only an upper bound.

The venue registry

VenueRegistry.forNetwork(provider, network) builds every adapter the configuration enables.

It refuses a provider for a different network outright, because that provider's reads would come from the wrong chain.

For each adapter it also asserts four things against the configuration, and each mismatch throws naming both sides:

  • The adapter's id matches the venue it was built for.
  • The adapter's executionClass matches the configured one. The two decide different transaction paths, so a silent disagreement would be a wrong promise.
  • The adapter declares support for this network.
  • The adapter reports itself available on this network.

Venues that produce no adapter are collected in registry.skipped with the reason, because "the router saw one venue" and "the router saw one venue and silently lost three" look identical without it.

On this page