Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

port0.sh

port0.sh is an open-source exchange built on Miden. A constant-product automated market maker holds a pair of fungible assets in a pool, prices them by the product of the two reserves, and lets any account trade, deposit, or withdraw against the pool. Any token can dock.

The name comes from three places. A program that binds to port 0 asks the operating system for any free port. A free port is a harbor where any ship can dock. A free port charges no customs duties: goods land and leave without a tax. The third meaning is the product. The registry accepts a code commitment and lists the pool. There is no customs house.

Components

ComponentRepositoryRole
Contractsport0_contractsPool account component, registry account component, and note scripts
SDKport0_sdkTypeScript client for quotes, note building, pool reads, and pool operations
Indexerport0_indexerReads the registry and pools over node RPC and serves an HTTP API
Appport0_appStatic web application to browse pools, trade, provide liquidity, and create pools

Status

port0.sh runs on Miden testnet 0.17.1. The code is unaudited and no audit report exists. Pools and notes are public network accounts: pool state and XYK notes are visible on chain. Nothing here is financial advice.

Reading order

A reader new to the system starts with the constant-product AMM and accounts and notes. A builder reading the interfaces continues with contracts and the SDK. An operator deploying the stack reads the runbook. The repositories chapter lists the canonical source and every mirror.

The constant-product AMM

A port0 pool holds two fungible assets, token0 and token1, and enforces the invariant x * y = k between their reserves. The invariant is maintained on every operation, so a trade moves the price along the curve rather than consuming the reserve at a fixed price.

Reserves and price

The reserve of each token is the pool account’s vault balance of that token minus the accrued protocol pot. The spot price of one token0 in token1 is the ratio of the two reserves. The pool does not store the reserves: they are read from the vault at each operation.

Liquidity

A liquidity provider deposits both tokens and receives LP tokens. The LP token is a native fungible asset issued by the pool account itself, with no separate faucet account. An amount a of LP tokens represents the fraction a / lp_supply of the reserves.

  • On the first deposit the pool mints sqrt(x * y) LP tokens and retains MINIMUM_LIQUIDITY = 1000 permanently. The provider receives the remainder.
  • On later deposits the pool mints the smaller of the two token-contribution ratios. The consumed amounts round up in the pool’s favor, and the unused remainder returns to the provider.
  • A withdrawal burns LP tokens and returns the proportional share of both reserves. A burn that would leave less than MINIMUM_LIQUIDITY reverts.

Swaps

A swap sends one token to the pool and receives the other. The output is computed from the full input with the LP fee weight, so the effective input is amount_in * (10000 - lp_fee_bps) / 10000. The reserve grows by the input minus the protocol share. The protocol share is carved out of the LP fee, so the swapper pays the LP fee tier and nothing more.

The min_amount_out field on a swap note bounds slippage. The pool reverts the note when the output would be smaller than the bound.

The price oracle

The pool maintains two cumulative price accumulators and the timestamp of the last update. They update at the start of each mint, burn, and swap:

p0 = (reserve1 << 64) / reserve0
p1 = (reserve0 << 64) / reserve1

Each accumulator adds p * elapsed, where elapsed is the block timestamp delta. The accumulators saturate at u128::MAX and never wrap, so a consumer subtracts two readings to obtain the time-weighted price over the interval. A backward timestamp yields an elapsed of zero, and a pool with a zero reserve returns before updating the accumulators.

Quoting without moving assets

quote_swap runs the swap computation against the current reserves without modifying state. It returns the amount out, the resulting reserves, and the protocol fee, and it is callable by foreign procedure invocation. The SDK ports the same arithmetic to TypeScript, so an application can quote before it builds a transaction.

Accounts and notes

Miden state is held in accounts and moved by notes. Every port0 operation is a note that targets an account.

Accounts

Three account kinds appear in the system:

KindRole
WalletHolds assets and signs transactions
FaucetIssues a fungible asset
ContractRuns account-component code against its own storage and vault

A public account stores its full state on chain. A private account stores only a commitment on chain and keeps the state with its holder. A pool is a public account: its reserves, LP supply, fee rates, and oracle readings are readable by anyone.

An account id is two field elements, prefix and suffix, and renders as 0x-prefixed hex or as a bech32 address.

Network accounts

A network account installs the network-account auth component. The component carries a note-script allowlist, the fee policy, and the nonce handling, and the node’s transaction builder reads the allowlist to consume notes on the account’s behalf. No signing key is involved. Removing the component turns the account into an ordinary account the node does not service.

Every transaction executed by a network account must have an effect: an input note, an output note, or an account state change. An empty scriptless transaction is rejected. A new network account is therefore committed by consuming a funding note as its first transaction, which also pays the first transaction fee from the received funds.

Both the pool and the registry are network accounts.

Notes

A note carries a script, storage, and assets. The script runs in note context and calls an account procedure through the component interface. Note scripts may not create output notes or move assets directly; account procedures may.

VisibilityBehavior
PublicPublished on chain and discoverable by the target through chain sync
PrivateEncrypted and delivered to the target through note transport

A port0 feature note is a public network note: it carries a NetworkAccountTarget attachment bound to the target account, so the node’s transaction builder can route it. Each feature note travels with a separate FEE_SPONSORSHIP note that pays the target’s configured note consumption fee.

Note fees

Notes carry a fee. A fee asset is required to consume a note, and the executing account pays it from its own vault. A pool’s fee faucet must equal the chain’s fee faucet, or the node never consumes the pool’s notes. On the Miden testnet the chain fee asset is USDCX. It comes from the network faucet; it is not minted by this project.

The registry prices two note kinds in its network auth: the register note at the configured register fee, and the registry-collect note at zero, because collection is permissionless and unsponsored. The registry pays the registry-collect transaction fee from its own vault.

Output notes and foreign calls

An account procedure can create output notes, which is how a pool returns LP tokens, refunds, swap output, and collected fees. Output notes use the P2ID script by default, with the recipient and the pool’s stored P2ID root.

A pool reads its protocol fee destination from the registry by foreign procedure invocation. The registry reads a pool’s code commitment and identity the same way at registration. The registry holds the protocol fee destination once for every pool, and it is read only on the collect path.

Glossary

The Miden documentation is the authority for Miden terms: Reference and Builder. The entries below state how the terms are used in this book.

TermMeaningMiden reference
AMMAutomated market maker. A contract that prices an asset pair from its reserves.
XYKThe constant-product invariant x * y = k.
BerthA pool. A single account holding two assets.
ManifestThe registry. The account that holds the accepted pool code commitments and the pool set.
CargoA token. A fungible asset held or traded by a pool.
CrewLiquidity providers. The accounts that deposit both assets and hold LP tokens.
AccountA unit of Miden state holding storage, code, and a vault.Accounts
Account componentCode installed on an account.Accounts
FaucetAn account that issues a fungible asset.Assets
NoteA Miden message carrying a script, storage, and assets to a target account.Notes
Note scriptThe code a note runs.Notes
TransactionThe execution that consumes notes and updates accounts.Transactions
MASMMiden assembly, the language the contracts compile to.MASM
LP tokenThe fungible asset a pool issues to represent a share of its reserves.
LP feeThe fee a swapper pays, in basis points of the input.Fees
Protocol shareThe registry’s share of the LP fee, in basis points of that fee.Fees
Registry shareSee protocol share.
LP fee tierOne of the allowed lp_fee_bps values the registry lists.
Protocol potThe pool’s accrued protocol fees, held in the vault until collected.
Network accountAn account serviced by the node’s transaction builder through a note-script allowlist, with no signing key.Network
Network noteA note carrying a NetworkAccountTarget attachment, so the node can route it to a network account.Notes
FEE_SPONSORSHIPThe note that carries a feature note’s consumption fee and is consumed with it.Fees
P2IDThe pay-to-id note script used for output notes.Notes
FPIForeign procedure invocation. A call from one account’s procedure into another account’s procedure.Cross-component calls
SlippageThe difference between a quoted output and the accepted minimum.
DeadlineA block timestamp after which a note reverts.
MAX_AMOUNTThe u64 cap 2^57 on every reserve, amount, and lp_supply.
MINIMUM_LIQUIDITYThe 1000 LP tokens locked permanently on the first mint.
OracleThe two cumulative price accumulators and the last update timestamp.
CollectThe operation that moves a protocol pot or a registry balance to the protocol account.

Contracts overview

The contracts repository contains eight contracts: two account components and six note scripts.

ContractKindPurpose
port0account componentPool: reserves accounting, LP supply, fee rates, protocol pot, price oracle
pool-registryaccount componentAccepted pool code commitments, pool set, asset pair index, protocol fee policy
mint-notenote scriptAdd liquidity
burn-notenote scriptRemove liquidity
swap-notenote scriptTrade
collect-notenote scriptMove a pool’s accrued protocol fees to the protocol account
register-notenote scriptRegister a pool in the registry
registry-collect-notenote scriptMove the registry’s accumulated register fees to the protocol account

The repository layout:

contracts/
  port0/                    pool account component
  pool-registry/            registry account component
  mint-note/                add-liquidity note script
  burn-note/                remove-liquidity note script
  swap-note/                swap note script
  collect-note/             pool protocol-fee collection note script
  register-note/            pool registration note script
  registry-collect-note/    registry fee collection note script
integration/                MockChain integration tests and deployment binaries

Pool lifecycle

  1. Build the pool account. Its initial storage holds the token0 and token1 asset keys, the two fee parameters, the P2ID root, and the registry pointer. The LP token key is derived from the pool’s account id and is not stored.
  2. Fund the pool with a note and consume that note as the pool’s first transaction. A network account requires every transaction to have an effect, so an empty scriptless deploy is rejected. The funding note commits the account and pays its first transaction fee from the received funds.
  3. Emit the register note. The registry verifies the pool’s code commitment and identity by foreign procedure invocation, then records the pool and its asset pair.
  4. Liquidity providers emit mint notes and burn notes. Traders emit swap notes.
  5. The fee recipient emits collect notes against a pool, or registry-collect notes against the registry. Both are permissionless.

Execution model

Notes invoke pool procedures through the component interface. Pool procedures run in account context and may create output notes and move assets. Note scripts run in note context and may not perform those actions directly.

A pool’s fee parameters, P2ID root, registry pointer, and token keys are initial storage and hold for the pool’s lifetime. No pool parameter is settable after creation.

Asset model

The LP token is a fungible asset issued by the pool account. No separate faucet account exists. The asset key is derived from the pool’s account id as [0, 0, suffix | 0x11, prefix], because the key embeds the account id and the account id is derived from the initial storage commitment, so seeding it would be circular.

Build and test

Each contract is a standalone crate built with cargo-miden:

cargo miden build --manifest-path contracts/port0/Cargo.toml --release

The integration tests run against MockChain, an in-process chain that compiles the contracts and executes the note flows without a network:

cargo test -p integration --release

The SDK’s artifact exporter builds the packages itself. A manual build is needed only to seed a fresh checkout.

Pool

The port0 component is the pool account component. It holds the reserves accounting, the LP supply, the two fee parameters, the protocol pot, and the price oracle.

Storage

The pool component declares ten value slots.

SlotTypeEncoding
token0_asset_keyWordtoken0 asset key
token1_asset_keyWordtoken1 asset key
lp_supplyFelt[supply, 0, 0, 0]
feesWord[lp_fee_bps, protocol_fee_share_bps, 0, 0]
p2id_rootWordP2ID script root used for output notes
protocol_potWord[amount0, amount1, 0, 0]
price0_cumulative_lastWordu128, four 32-bit limbs, least significant first
price1_cumulative_lastWordu128, four 32-bit limbs, least significant first
block_timestamp_lastFelt[timestamp, 0, 0, 0]
registry_idWord[0, 0, suffix, prefix] of the registry account id

Every slot except protocol_pot and the two accumulators is written at account creation. protocol_pot accrues as swaps happen. The LP token key is not a slot; the contract derives it from the pool’s account id.

Procedures

ProcedurePurpose
token0_asset_keyReads the token0 asset key
token1_asset_keyReads the token1 asset key
lp_supplyReads the LP supply
get_reservesReads both reserves
get_feesReads both fee parameters
mintAdds liquidity and mints LP tokens
burnRemoves liquidity and burns LP tokens
swapTrades one asset for the other
quote_swapComputes a swap without modifying state
collectMoves the protocol pot to the protocol account
get_registry_identityReturns the registry pointer, token keys, and fee parameters

get_registry_identity returns twelve felts in the order [prefix, suffix, token0[0..4], token1[0..4], protocol_fee_share_bps, lp_fee_bps]. The registry calls it by foreign procedure invocation at registration.

Reserves

The pool does not store reserves. Reserves equal the vault balances of token0 and token1 minus the protocol pot. The vault is the source of truth, and the protocol pot is the only balance the pool withholds from the curve.

Mint

On the first deposit the pool computes root = isqrt(reserve0_after * reserve1_after) and requires root > MINIMUM_LIQUIDITY. It mints root LP tokens and retains MINIMUM_LIQUIDITY = 1000; the provider receives root - 1000.

On later deposits the pool mints the smaller of amount0 * lp_supply / reserve0 and amount1 * lp_supply / reserve1. The amount of each token used rounds up in the pool’s favor with div_ceil; the remainder returns to the provider.

Burn

For a burn of amount LP tokens, the pool returns amount * reserve0 / lp_supply of token0 and amount * reserve1 / lp_supply of token1, then reduces lp_supply. The burn reverts when amount > lp_supply - MINIMUM_LIQUIDITY and when either payout rounds to zero.

Swap

The pool computes the LP fee on the full input, carves the protocol share out of that fee, and computes the output from the full input with the LP fee weight. The output does not depend on the protocol share.

lp_fee             = amount_in * lp_fee_bps / 10000
protocol_fee       = lp_fee * protocol_fee_share_bps / 10000
amount_in_with_fee = amount_in * (10000 - lp_fee_bps)
amount_out         = amount_in_with_fee * reserve_out /
                     (reserve_in * 10000 + amount_in_with_fee)

The full input enters the vault. The output leaves the vault. The reserve grows by amount_in - protocol_fee, and the protocol fee is recorded in protocol_pot. The pool keeps the LP fee minus the protocol share.

Oracle

The oracle updates at the start of each mint, burn, and swap. It computes

p0 = (reserve1 << 64) / reserve0
p1 = (reserve0 << 64) / reserve1

and adds p0 * elapsed and p1 * elapsed to the two cumulative values, where elapsed is the block timestamp delta. The accumulators saturate at u128::MAX. A backward timestamp yields an elapsed of zero, and a pool with a zero reserve returns before updating the accumulators.

Collect

collect reads the protocol fee destination from the registry by foreign procedure invocation, moves the protocol pot contents to that account as a P2ID note, and resets the pot. An empty pot reverts. The destination is not stored in the pool, so it is rotated in one place and read only on the collect path.

Configuration

A pool’s configuration is its initial storage: lp_fee_bps, protocol_fee_share_bps, p2id_root, registry_id, and the two token asset keys. The LP token key is derived from the pool’s account id. No pool parameter is settable after creation. The registry rejects a pool whose token pair, fee share, LP fee tier, or registry pointer disagrees with the registry.

Registry

The pool-registry component is the registry account component. It holds the canonical pool set, the ordered asset pair index, and the protocol fee policy. Registration is the only way a pool becomes discoverable by the indexer and the app.

Storage

The registry account declares seven storage slots.

SlotTypeKeyValue
accepted_code_commitmentsmappool code commitment1 when accepted
poolsmappool id key [prefix, suffix, 0, 0]pool code commitment
assets_to_poolmapordered asset pair [lo.prefix, lo.suffix, hi.prefix, hi.suffix]pool id key
protocol_fee_tovalueaccount id word [0, 0, suffix, prefix]protocol fee destination
protocol_fee_share_bpsvalue[bps, 0, 0, 0]protocol share every pool must carry
allowed_lp_fee_bpsmapLP fee tier in bps1 when a pool may charge it
p2id_rootvalueP2ID script rootscript root of the payout note

Keys are four-felt words. A pool id key is [prefix, suffix, 0, 0]. An ordered asset pair is [lo.prefix, lo.suffix, hi.prefix, hi.suffix], ordered by (prefix, suffix) as canonical integers.

Registration

register(pool_id, asset_0, asset_1) performs these checks in order:

  1. The two assets are not equal.
  2. The pool id is not already registered.
  3. The asset pair is not already registered.
  4. The pool’s code commitment is in accepted_code_commitments.
  5. The pool’s registry pointer equals the executing registry.
  6. The pool’s token keys match the claimed pair in either order.
  7. The pool’s protocol share equals the registry’s protocol_fee_share_bps.
  8. The pool’s LP fee is an entry in allowed_lp_fee_bps.

It then writes pools and assets_to_pool. The commitment is read from the pool’s CodeInspection::get_code_commitment, and the remaining fields from the pool’s get_registry_identity, both by foreign procedure invocation.

Registration is not a judgment. The registry accepts a code commitment and records the pair. It does not endorse a token.

Code commitment

The accepted commitment covers the pool’s full component set: the port0 component, CodeInspection, the network-account auth, and the fee-policy components. It changes when any of those change, and the registry must be seeded with the value the deployed pool produces. The SDK prints the value with npm run pool:commitment.

The current accepted commitment is 0x51c52cca051456d35329f2add92319f6447f1437a8b716279e536c1c0f6fab09.

Pool lookup

pool_for_assets(asset_0, asset_1) returns [found, prefix, suffix, 0]. The found flag is 1 with the pool id, or all four felts are zero when the pair has no pool. A zero account id is never returned as a pool.

is_pair_registered(asset_0, asset_1) returns the predicate behind that check. pool_code_commitment(pool_id) reads the recorded commitment.

Fee policy

protocol_fee_to returns the protocol fee destination as four felts: [prefix, suffix, 0, 0]. A pool reads it by foreign procedure invocation when it collects, so the address is set in one place and read only on the collect path. protocol_fee_share_bps returns the share every registered pool must carry. allowed_lp_fee_bps holds the permitted tiers.

Registry collect

collect(asset_key, serial_num) moves the registry’s whole balance of asset_key to protocol_fee_to as a P2ID note. It is permissionless: the destination is fixed in storage, so a caller can only flush accumulated register fees toward the protocol. The procedure reverts when p2id_root is unset, when the asset key is empty, and when the registry holds none of the asset.

Foreign procedure roots

A pool addresses the registry’s protocol_fee_to, and the registry addresses the pool’s get_registry_identity and the standard get_code_commitment, all by hard-coded MAST root. Editing either contract shifts those roots, and the call fails with a message that names a missing procedure root. The roots are printed by:

cargo run -p integration --bin registry_roots --release -- <contract-dir>
cargo run -p integration --bin code_inspection_roots

Notes

Every pool operation is a note sent to the pool account. Registration and registry collection are notes sent to the registry account. This chapter lists each note, the assets it carries, and the felts it reads from note storage.

NoteAssetsNote storage (felts, in order)
mint-notetoken0, token1pool id (2), recipient (2), serial (4), tag (1), deadline (1), min_lp_out (1)
burn-noteLP tokenpool id (2), recipient (2), serial (4), tag (1), deadline (1), min_amount0 (1), min_amount1 (1)
swap-notetoken0 or token1pool id (2), recipient (2), serial (4), tag (1), deadline (1), min_amount_out (1)
collect-notenoneserial (4)
register-notenoneregistry id (2), pool id (2), asset0 (2), asset1 (2)
registry-collect-notenoneregistry id (2), asset key (4), serial (4)

Account ids are two felts, prefix then suffix. A serial is four felts. deadline is a block timestamp; the note reverts if the pool executes it after that time. The min_* fields bound slippage.

mint-note

Deposits token0 and token1. The pool mints LP tokens to the recipient. On the first mint the pool mints sqrt(x * y) LP tokens and retains MINIMUM_LIQUIDITY = 1000 permanently; the provider receives the remainder. If the deposited ratio differs from the pool ratio, the pool refunds the excess of one token to the recipient.

burn-note

Burns LP tokens. The pool returns a proportional share of both reserves to the recipient. The burn reverts when amount_to_burn > lp_supply - MINIMUM_LIQUIDITY and when either payout rounds to zero.

swap-note

Sends one token. The pool pays out the other token, less fees. min_amount_out reverts the trade when the output would be smaller.

collect-note

Moves the pool’s accrued protocol fee to the protocol account the registry designates. The note is permissionless: the destination is fixed in the registry, so a caller can only flush fees toward it. The note reverts when the fee pot is empty.

register-note

Registers the pool in the registry. The note targets the registry account and carries the registry id, the pool id, and the two token faucet ids. The registry’s register procedure performs the checks listed in the registry chapter.

registry-collect-note

Moves the registry’s whole balance of one asset to the protocol account. The note carries the registry id, the four-felt asset key, and a serial. Like a pool collect, it is permissionless and the destination is fixed in storage.

Fee sponsorship

A feature note does not carry its own consumption fee. A separate FEE_SPONSORSHIP note carries exactly one fungible asset, the fee amount in the fee faucet asset, and the feature note id in its storage. The two notes are consumed together by the node’s transaction builder. The registry-collect note is unsponsored and priced at zero, so the registry pays that transaction fee from its own vault.

Fees

Two basis-point parameters describe the fee on a swap. They are separate, and the protocol share does not add to the swapper’s cost.

ParameterWhere it livesMeaning
lp_fee_bpsPool initial storage, one value from the registry’s allowlistThe fee a swapper pays
protocol_fee_share_bpsRegistry value slot, copied into pool initial storageThe protocol’s share of the LP fee, in basis points of it

Registry share

The registry holds one protocol share. The shipped value is 1200, which is 12 percent of the LP fee. Every registered pool must carry that share; the register procedure rejects a pool whose share disagrees with the registry.

The registry also holds an allowlist of LP fee tiers. The shipped tiers are 5, 30, and 100 basis points, which are 0.05, 0.30, and 1.00 percent.

The swap computation

lp_fee             = amount_in * lp_fee_bps / 10000
protocol_fee       = lp_fee * protocol_fee_share_bps / 10000
amount_in_with_fee = amount_in * (10000 - lp_fee_bps)
amount_out         = amount_in_with_fee * reserve_out /
                     (reserve_in * 10000 + amount_in_with_fee)

The full input enters the vault. The output is computed from the full input with the LP fee weight, so amount_out does not depend on the protocol share. The pool’s reserve grows by amount_in - protocol_fee; the pool keeps the LP fee minus the share, and the share accrues to the protocol pot.

At the shipped share of 1200, the protocol fee at each tier is:

lp_fee_bpsLP feeProtocol share of the feeProtocol fee on a 10,000 input
50.05%12%0.6 bps, or 0 base units after integer division
300.30%12%3.6 bps, or 3 base units after integer division
1001.00%12%12 bps, or 12 base units

The protocol fee is lp_fee_bps * protocol_fee_share_bps / 10000, computed with integer division. The table shows the mathematical value and the value the contract records after flooring.

Worked example

Reserves: reserve0 = 1,000,000, reserve1 = 2,000,000. A swap sends 10,000 of token0 at the 30 bps tier, with the shipped share of 1200.

StepValue
lp_fee10,000 * 30 / 10000 = 30
protocol_fee30 * 1200 / 10000 = 3 (floored from 3.6)
amount_in_with_fee10,000 * 9970 = 99,700,000
amount_out99,700,000 * 2,000,000 / (1,000,000 * 10000 + 99,700,000) = 19,743
reserve0_after1,000,000 + 10,000 - 3 = 1,009,997
reserve1_after2,000,000 - 19,743 = 1,980,257

The swapper pays the 30 bps tier. The protocol receives 3 base units of token0, and the LP side keeps the remaining 27 base units of the fee.

Collect

Two permissionless notes move fees to the protocol account:

NoteSourceDestination
collect-noteThe pool’s protocol_potThe registry’s protocol_fee_to
registry-collect-noteThe registry’s balance of one assetThe registry’s protocol_fee_to

Both destinations are fixed in registry storage. A caller can trigger a payout but cannot redirect it.

The pool collect note reverts when the pot is empty. The registry collect note reverts when the registry holds none of the requested asset.

Limits and failure conditions

Numeric bounds

Amounts, reserves, and lp_supply are u64 values bounded by MAX_AMOUNT = 2^57.

The binding constraint is the swap numerator, amount_in_with_fee * reserve_out. amount_in_with_fee is at most amount_in * 10000, so the worst case is MAX_AMOUNT^2 * 10000. The value must stay below 2^128, which requires MAX_AMOUNT < 2^64 / 100. The value 2^57 satisfies the bound with margin. Mint and burn products stay below 2^126.

The oracle accumulators saturate at u128::MAX rather than wrap, so a consumer subtracts two readings without modular arithmetic.

Failure conditions

Each procedure reverts under the conditions below.

Mint

  • The deposit is missing one of the two tokens.
  • A deposited amount is above MAX_AMOUNT.
  • The first deposit mints at most MINIMUM_LIQUIDITY.
  • A later deposit mints zero liquidity.
  • The liquidity out is below min_lp_out.

Burn

  • The note carries multiple assets or an asset that is not the LP token.
  • The LP amount is zero.
  • The pool is empty.
  • The amount is above MAX_AMOUNT.
  • The burn leaves less than MINIMUM_LIQUIDITY.
  • Either payout rounds to zero.
  • A payout is below its minimum.

Swap

  • The note carries multiple assets or an asset that is neither token.
  • The input is zero.
  • The pool is empty.
  • The input or a reserve is above MAX_AMOUNT.
  • The output is zero or at least reserve_out.
  • The resulting reserve is above MAX_AMOUNT.
  • The output is below min_amount_out.

Collect

  • The protocol pot is empty.

Registry collect

  • p2id_root is unset.
  • The asset key is empty.
  • The registry holds none of the asset.

Deadline

Every feature note except collect carries a deadline, a block timestamp. The pool reverts the note when it executes after that time. A deadline is built from the last seen block timestamp plus a margin, so a note does not sit unbounded in the pool’s input queue.

Security model

The system is unaudited. No audit report exists. The statements below describe how the contracts behave, not a guarantee of safety.

Custody

A pool account holds the assets. No operator holds them. Every operation is a note that the node consumes against the pool, and the pool’s vault is the only store of reserves.

No admin and no upgrade path

A pool’s fee parameters, P2ID root, registry pointer, and token keys are initial storage. No procedure sets them after creation. A pool cannot be paused, upgraded, or reconfigured.

The registry’s accepted code commitments and fee policy are initial storage as well. The registry has no setter for either, so changing an accepted commitment or the protocol share means building and deploying a new registry account. The register procedure only adds pool ids and asset pairs to its two maps.

Public state

Pools and notes are public network accounts. Pool reserves, LP supply, fees, pot, and oracle readings are on chain and readable by anyone. XYK notes are public; the protocol does not provide private trading.

Permissionless flows

Pool creation is permissionless. The registry accepts a code commitment; it does not screen a token or endorse a pool.

Both collect paths are permissionless. The pool collect note and the registry-collect note move fees to the destination fixed in registry storage, so a caller can trigger a payout but cannot redirect it.

Network auth

A pool and the registry install the network-account auth component. The component carries a note-script allowlist, a fee policy, and the nonce handling. The node’s transaction builder reads the allowlist to consume notes on the account’s behalf. There is no signing key.

The registry prices both of its admitted note kinds: the register note at the configured register fee, and the registry-collect note at zero. Every admitted script root must be priced, because a root the fee policy does not price aborts fee estimation and the account’s auth procedure rejects that note as an input. The allowlist is initial storage, and the procedures that mutate it are gated on an authority the account does not install.

Foreign procedure roots

A pool addresses the registry’s protocol_fee_to, and the registry addresses the pool’s get_registry_identity and the standard get_code_commitment, all by hard-coded MAST root. A contract edit that shifts a root breaks the call until the constant is updated. The roots are printed by the integration binaries and must be re-derived after a contract change.

Note fees

Notes carry a fee, and the executing account pays it from its own vault. A pool’s fee faucet must equal the chain’s fee faucet, or the node never consumes the pool’s notes. The chain fee asset on the testnet is USDCX. It comes from the network faucet and is not minted by this project.

Reporting

Vulnerability reports go to hello@port0.sh. The contact is listed in the contact chapter.

Install

The SDK is published as @port0.sh/sdk.

npm install @port0.sh/sdk @miden-sdk/miden-sdk@0.17.1

The SDK peers on @miden-sdk/miden-sdk and is developed and tested against 0.17.1. Pin the exact version the testnet runs. A client talks only to a node with the same major and minor version, so the two move together.

Artifacts

The package embeds the eight compiled Miden packages from the contracts repository: the pool and registry account components, and the mint, burn, swap, collect, register, and registry-collect note scripts. CONTRACT_SHA256 holds the digest of each package for build-time drift detection.

Source

The canonical source is the Radicle repository. GitHub and npm are mirrors. The repositories chapter lists every location.

Quick start

import { MidenClient } from "@miden-sdk/miden-sdk";
import { XykClient, deadlineFrom } from "@port0.sh/sdk";

const client = await MidenClient.create({
  rpcUrl: "https://rpc.testnet.miden.io/",
  feeFaucetId: "0x4cbdcaffe75f0a317482224dae6436", // chain fee faucet, required
  proverUrl: "testnet",
  autoSync: false,
});
await client.syncChain();

const xyk = XykClient.from(client);
const pool = xyk.pool({
  poolId,
  feeFaucetId,
  noteFees: { swap: 133n }, // fee schedule fixed at pool creation
});

const state = await pool.state();

const result = await pool.swap({
  sender,
  recipient,
  tokenIn: token0,
  amountIn: 1000n,
  slippageBps: 50n,
  deadline: deadlineFrom(latestBlockTimestamp, 3600n),
  // submit: true, waitForConfirmation: true,
});

// result.note, result.sponsorNote, result.request, result.quote,
// result.minAmountOut, result.tokenOut, result.fee, result.feeFaucetId

Notes on the example

  • feeFaucetId is required by MidenClient.create and must be the chain fee faucet. On the testnet it is USDCX.
  • proverUrl selects the remote prover. Omitting it falls back to the local prover, which proves slowly.
  • syncChain refreshes the local store, which is a cache of chain state.
  • Pool fetches pool state internally, so each operation method is a single call.
  • All amounts are bigint in the smallest unit of the asset. All account ids are the web-sdk AccountId type.

Amounts and words

type Word4 = readonly [bigint, bigint, bigint, bigint];
type SerialNumber = Word4;

Word4 is a four-limb value: an asset key, a registry id word, or a serial. SerialNumber is the same shape.

Operations

Each operation runs the local quote, applies the slippage bound, builds the feature note and its FEE_SPONSORSHIP note, and assembles one transaction request. The shape is (client, config, state, params, options?).

swap(client, config, state, params: SwapParams, options?): Promise<SwapOutput>
addLiquidity(client, config, state, params: MintParams, options?): Promise<MintOutput>
removeLiquidity(client, config, state, params: BurnParams, options?): Promise<BurnOutput>
collectFees(client, config, state, params: CollectParams, options?): Promise<OperationOutput>
registerPool(client, config: RegistryConfig, params: RegisterPoolParams, options?): Promise<RegisterPoolOutput>

Outputs

OperationOutput is { note, sponsorNote, request, fee, feeFaucetId }.

OutputExtra fields
SwapOutputquote, tokenOut, minAmountOut
MintOutputquote, minLpOut
BurnOutputquote, minAmount0, minAmount1
RegisterPoolOutputnote, sponsorNote, request, fee, feeFaucetId

Submission

SubmitOptions is { submit?: boolean, waitForConfirmation?: boolean }. submit defaults to false: the caller receives the request and hands it to a wallet adapter. Setting submit: true sends the request through the MidenClient.

Parameters

OperationFields
SwapParamssender, recipient, tokenIn, amountIn, slippageBps, deadline, optional serial, tag
MintParamssender, recipient, amount0, amount1, slippageBps, deadline, optional serial, tag
BurnParamssender, recipient, amountLp, slippageBps, deadline, optional serial, tag
CollectParamssender, optional serial
RegisterPoolParamssender, poolId, asset0, asset1

Helpers

applySlippage(amount: bigint, slippageBps: bigint): bigint
resolveSwapSide(tokenIn: string, token0: string, token1: string): 0 | 1
deadlineFrom(blockTimestampSeconds: bigint, marginSeconds: bigint): bigint

applySlippage returns floor(amount * (10000 - slippageBps) / 10000) and throws INVALID_ARGUMENT for slippageBps outside [0, 10000).

resolveSwapSide returns 0 for token0 and 1 for token1, and throws INVALID_ARGUMENT when tokenIn matches neither side.

deadlineFrom returns blockTimestampSeconds + marginSeconds.

Pool configuration

type PoolConfig = {
  poolId: AccountId;
  feeFaucetId: AccountId;
  noteFees: PoolNoteFees;
  deadlineMarginSeconds?: bigint;
};

noteFee(noteFees: PoolNoteFees, kind: PoolNoteKind): bigint
DEFAULT_DEADLINE_MARGIN_SECONDS   // 3600n

PoolNoteFees is Partial<Record<PoolNoteKind, bigint>>, and PoolNoteKind is "mint" | "burn" | "swap" | "collect". noteFee returns the configured fee for a kind, or 0n when unset. The fee schedule is fixed at pool creation and must be kept by the application for building correct sponsorship notes.

Pure math

The pure math functions have no WASM dependency and mirror the contract arithmetic exactly, including check order and rounding.

isqrt(n: bigint): bigint
divCeil(numerator: bigint, denominator: bigint): bigint

amountOut(amountIn, reserveIn, reserveOut, lpFeeBps, protocolFeeShareBps): SwapQuote
quoteMint(amount0, amount1, reserve0, reserve1, lpSupply): MintQuote
quoteBurn(amountToBurn, reserve0, reserve1, lpSupply): BurnQuote

amountOut computes:

lp_fee             = floor(lp_fee_bps * amount_in / 10000)
protocol_fee       = floor(protocol_fee_share_bps * lp_fee / 10000)
amount_in_with_fee = amount_in * (10000 - lp_fee_bps)
amount_out         = floor(amount_in_with_fee * reserve_out /
                           (reserve_in * 10000 + amount_in_with_fee))

The failure order is ZeroAmountIn, EmptyPool, Overflow for any input above MAX_AMOUNT, ZeroAmountOut, InsufficientLiquidity when out >= reserve_out, and Overflow when reserve_in_after >= MAX_AMOUNT.

quoteMint on the first deposit mints isqrt(x * y) LP tokens and retains MINIMUM_LIQUIDITY. On later deposits it mints the smaller of the two token-contribution ratios, rounds the consumed amounts up with divCeil, and refunds the unused remainder.

quoteBurn returns the proportional share of both reserves. A burn that would leave less than MINIMUM_LIQUIDITY throws InsufficientLiquidity, as does a burn whose payout rounds to zero for either token.

Pool reads

decodePoolState(account: Account): PoolState
fetchPoolState(getAccount, poolId): Promise<PoolState>

decodePoolState reads the ten value slots and the vault. Reserves equal the vault balances of token0 and token1 minus the protocol pot. It throws NOT_A_POOL when the token asset keys are missing or zero, and INVARIANT when a vault balance is below the recorded pot.

PoolState fields: accountId, token0Key, token1Key, token0FaucetId, token1FaucetId, lpSupply, fees (lpFeeBps, protocolFeeShareBps), registryId, p2idRoot, protocolPot (amount0, amount1), price0CumulativeLast, price1CumulativeLast, blockTimestampLast, reserve0, reserve1. The protocol fee destination lives on the registry and is read only when the pool collects.

Request building

The SDK turns parameters into notes and assembles the transaction request a wallet executes. This chapter describes the layers below the operations.

Note builders

Each builder returns a web-sdk Note. The XYK feature builders produce a public network note with a NetworkAccountTarget attachment to the target account.

buildMintNote(input: MintNoteInput): Note
buildBurnNote(input: BurnNoteInput): Note
buildSwapNote(input: SwapNoteInput): Note
buildCollectNote(input: CollectNoteInput): Note
buildRegisterNote(input: RegisterNoteInput): Note
buildRegistryCollectNote(input: RegistryCollectNoteInput): Note
buildFeeSponsorshipNote(input: FeeSponsorshipNoteInput): Note

p2idRoot(): Word4
noteScript(kind: NoteKind): NoteScript
POOL_NOTE_KINDS: readonly PoolNoteKind[]  // ["mint","burn","swap","collect"]
NOTE_KINDS: readonly NoteKind[]           // POOL_NOTE_KINDS plus "register" and "registry-collect"

Common input fields are sender, poolId, recipient where applicable, deadline, and the min_* slippage bounds. The serial defaults to randomSerial(), and tag defaults to accountTargetTag(recipient).

buildRegisterNote takes sender, registryId, poolId, asset0, and asset1. The note targets the registry and carries the 8-felt register layout with no assets.

buildRegistryCollectNote takes sender, registryId, assetKey, and an optional serial. The note targets the registry and carries the 10-felt registry-collect layout with no assets.

buildFeeSponsorshipNote takes sender, target, featureNote, feeFaucetId, feeAmount, and optional reclaimer (defaults to sender) and reclaimHeight (defaults to 0, disabled). The note carries exactly one FungibleAsset(feeFaucetId, feeAmount) and no attachment.

noteScript(kind) deserialises the compiled package from src/generated/contracts.ts once and caches it.

Note storage layouts

Each function returns bigint[] in the exact order the note script asserts.

FunctionFeltsLayout
mintNoteStorage11pool, recipient, serial, tag, deadline, minLpOut
burnNoteStorage12pool, recipient, serial, tag, deadline, minAmount0, minAmount1
swapNoteStorage11pool, recipient, serial, tag, deadline, minAmountOut
collectNoteStorage4serial
registerNoteStorage8registry, pool, asset0, asset1
registryCollectNoteStorage10registry, asset key (4), serial (4)
feeSponsorshipStorage7feature note id (4), reclaimer [suffix, prefix], reclaim height

Account ids inside layouts are [prefix, suffix]. The reclaimer in feeSponsorshipStorage is the exception: it is [suffix, prefix], matching FeeSponsorshipNoteStorage.

Encoders

accountIdFelts(id): [bigint, bigint]          // [prefix, suffix]
fungibleAssetIdWord(faucetId): Word4          // [0, 0, suffix | 0x11, prefix]
accountTargetTag(id): bigint                  // top 14 prefix bits, low bits zeroed
randomSerial(): SerialNumber
randomSeed(): Uint8Array                      // 32 bytes
isWord4(value): value is Word4
ACCOUNT_TARGET_TAG_LEN                        // 14

accountTargetTag is computed in bigint. JavaScript number bitwise operators truncate to int32 and produce a negative tag whenever bit 31 of the prefix is set.

Combined request

ownOutputNotesRequest(
  client: MidenClient,
  notes: Note[],
  targetPoolId: AccountId,
): Promise<TransactionRequest>

The function builds one TransactionRequest with notes as own output notes, plus the 0.17 machinery a wallet-executed multisig request needs:

  • A three-word MultisigAuthArgs preimage, [boundBlock, 0, 0, 0] || SALT || CONVERSION_INFO (12 felts), committed with Poseidon2.hashElements and declared with withAuthArg, with the preimage in the advice map. SALT is fresh per request. CONVERSION_INFO is [feeFaucetId.suffix, feeFaucetId.prefix, 1, 1], read from client.feeFaucetId(). The bound block is client.getSyncHeight().
  • The target pool as a foreign account with empty storage requirements, so the kernel prices the emitted network note through estimate_note_fee.

Notes are pushed rather than constructor-consumed so the caller keeps valid Note references.

Pool and registry creation

createPoolAccount(client, params): Promise<Account>
buildPoolAccount(client, params): Promise<Account>
poolCodeCommitment(client, params): Promise<string>
deployPoolAccount(client, poolId, fundingNoteId, options?): Promise<TransactionSubmitResult>
createFeeFaucet(client, params): Promise<Account>
createRegistryAccount(client, params): Promise<Account>
deployRegistryAccount(client, registryId, fundingNoteId, options?): Promise<TransactionSubmitResult>

CreatePoolAccountParams is { token0Key, token1Key, registryId, lpFeeBps, protocolFeeShareBps, p2idRoot?, feeFaucetId, noteFees?, seed? }. createPoolAccount builds the pool component from the compiled package and the ten init slots, installs the standard CodeInspection component, installs the network auth with the fee schedule, and inserts the account into the client’s local store. It does not deploy.

poolCodeCommitment returns the account code commitment as lowercase hex, which is the value the registry’s accepted_code_commitments map must hold. It is computed through the same path as createPoolAccount, so it cannot drift from a deployed pool.

deployPoolAccount consumes the funding note as the pool’s first transaction. The 0.17 network auth rejects a transaction with no effect, so a scriptless deploy fails. The pool id is passed to transactions.consume as a hex string, not as an AccountId, because the client re-parses account references worker-side.

CreateRegistryAccountParams is { acceptedPoolCodeCommitment, feeFaucetId, registerFee, protocolFeeTo, protocolFeeShareBps, allowedLpFeeBps, p2idRoot, seed? }. createRegistryAccount seeds accepted_code_commitments and allowed_lp_fee_bps, prices both registry note kinds in the network auth, and inserts the account. deployRegistryAccount consumes a funding note as the first transaction.

createFeeFaucet creates a public fungible faucet. It is not the pool’s network note fee asset. A pool’s feeFaucetId must be the chain fee faucet, or the node never consumes the pool’s notes. Use the helper for application-level faucets, such as a launchpad token.

Discovery

isXykPool(account: Account): boolean
xykNoteScriptRoots(): Record<PoolNoteKind, string>
class PoolRegistry { register(poolId); get(canonicalId); list() }

isXykPool returns true when the account is a network account whose note-script allowlist contains all four XYK pool note roots. The node RPC has no account-listing-by-code-root endpoint, so chain-wide discovery belongs to the indexer.

Errors

The SDK raises two error types. The pure math layer raises PoolMathError, which mirrors the contract’s PoolMathError. Everything above the math layer raises XykError, whose code is a stable string.

PoolMathError

KindCondition
EmptyPoolThe pool is empty on one or both sides
OverflowA value would exceed the numeric bound
ZeroAmountInThe input amount is zero
ZeroAmountOutThe output amount is zero
InsufficientLiquidityThe pool cannot satisfy the operation
InvariantStored reserves and LP supply disagree

The message string matches the contract’s PoolMathError.

XykError

CodeCondition
NOT_A_POOLThe account has no token asset keys, or the keys are zero
POOL_NOT_FOUNDThe account or registry is not in the local store
POOL_NOT_CONFIGUREDA required pool slot is missing
MISSING_SLOTA named storage slot is absent
INVARIANTA vault balance is below the recorded protocol pot
INVALID_ARGUMENTA parameter is out of range or inconsistent
UNSUPPORTED_OPERATIONThe requested operation has no implementation

Failure order

amountOut raises in this order: ZeroAmountIn, EmptyPool, Overflow for any input above MAX_AMOUNT, ZeroAmountOut, InsufficientLiquidity when the output is at least reserve_out, and Overflow when the resulting reserve is at least MAX_AMOUNT.

quoteMint checks addAmount(reserve, amount) against MAX_AMOUNT before the ratio branch, so an over-sized deposit reverts even when part of it would be refunded.

quoteBurn raises InsufficientLiquidity when the burn would leave less than MINIMUM_LIQUIDITY, and when either payout rounds to zero.

The reference error table maps protocol rejections seen in operation to their cause and fix.

Indexer overview

The indexer reads the chain through the node RPC only. It uses no Miden client, no store, and no keystore. It polls the on-chain registry for pool ids and each pool for state, keeps a bounded history in SQLite, and serves the result over HTTP.

The indexer is the application’s pool source. The home page lists every pool it serves, and the pool page loads state and configuration from it. There is no local pool list and no manual import.

What it reads

DataSource
Pool setRegistry pools map
Reserves, LP supply, pot, fees, oracleThe pool’s ten value slots and its vault
Token symbols and decimalsEach faucet’s token_config slot
Note fee schedule and fee assetThe pool’s fee_asset_id value slot and the basic_constant_fee schedule map
Activity and 24 h aggregatesClassified reserve and pot deltas between polls

Reserves equal the vault balances of token0 and token1 minus the protocol pot, matching the contract’s get_reserves.

Process shape

The process starts the HTTP API, a registry poll loop, a block subscription, and a pool scheduler. The registry poll discovers pools. The block subscription marks a pool due when a block names it as changed. The scheduler fetches pools whose own cadence says they are due. A SQLite write cycle persists the newest bucket of each tier and the pending activities.

The API opens its own read-only connection to the same SQLite file. WAL lets the API read while the poller writes, so neither waits on the other.

Source

The canonical source is the Radicle repository. GitHub is a mirror. The repositories chapter lists every location.

Indexer configuration

Read from the environment at startup. A .env file in the current directory is loaded first. Real environment variables take precedence, and a missing file is not an error.

VariableDefaultMeaning
MIDEN_RPC_URLhttps://rpc.testnet.miden.ioNode RPC endpoint
PORT0_REGISTRY_IDrequiredRegistry account id, hex
INDEXER_LISTEN127.0.0.1:8080HTTP listen address. Loopback by default, for an API behind a reverse proxy
INDEXER_ALLOWED_ORIGINS(empty)Comma-separated origins allowed to read the API cross-origin. Empty means same-origin only; no wildcard is accepted
POOL_POLL_SECS15Scheduler tick in seconds, clamped to 1..=5; per-pool cadence is internal
REGISTRY_POLL_SECS60Registry poll interval
INDEXER_DBport0_indexer.sqliteSQLite path; empty or off keeps the index in RAM only
INDEXER_SNAPSHOT_RETENTION(empty)Empty prunes each resolution to its window; all keeps every bucket
RUST_LOGport0_indexer=infoTracing filter

Listen address

The API listens on loopback by default. A deployment that serves the API directly sets INDEXER_LISTEN=0.0.0.0:8080. A deployment behind a reverse proxy leaves the default and proxies to it.

Cross-origin reads

Cross-origin reads are closed by default. INDEXER_ALLOWED_ORIGINS lists the origins the API answers, and only GET and POST are allowed, without credentials. The refresh route is state-changing, so no wildcard is accepted. An empty list means same-origin only, which is the default for a service behind a proxy. A malformed entry is logged and dropped.

The shipped .env.example lists the Vite development ports, http://localhost:5173 and http://localhost:5180, so a local app can fetch the API cross-origin.

Database

INDEXER_DB holds the pool set, token metadata, one row per history bucket, and one row per classified activity. An empty value or off keeps the index in RAM only and forgets every series, activity, and yield window on restart.

INDEXER_SNAPSHOT_RETENTION=all keeps every bucket. The default prunes each resolution to its window. See storage and retention.

Storage and retention

SQLite holds the pool set, token metadata, one row per history bucket, and one row per classified activity. History reads do not go through the in-memory model: the API opens its own read-only connection, and WAL lets it read while the poller writes, so neither waits on the other.

The RAM mirror keeps only the open bucket of each tier, which is all a poll needs to extend it. Memory is therefore O(pools) rather than O(pools x rows).

Column types

Every amount is a TEXT column. SQLite’s INTEGER is a signed 64-bit value, so a u64 above 2^63 - 1 cannot be stored in it and a u128 never can; both appear in pool state. rusqlite has no u64 binding. Amounts travel as the decimal strings the API already uses, and block numbers, timestamps, and resolution periods take an i64 that cannot overflow.

Row cost

The file grows by the measured cost of a row: 360.3 bytes per snapshot and 97.4 bytes per activity, including their indexes. The figures come from 200,000 rows of each type inserted into the shipped schema.

Retention ladder

Snapshots follow five tiers. Each tier is pruned to its own window on every write cycle except the last.

TierResolutionRetainedRows per pool
T01m12 h720
T15m7 d2,016
T21h30 d720
T36h180 d720
T41dforever365/year

A tier is not computed from the one above it. Every poll feeds all five, and each tier merges the poll into its open bucket: open from the first sample, high and low from the extremes, close from the newest. The merge is exact for a candle.

At 10,000 pools the four bounded tiers hold 15.05 GB, and the daily tier adds 1.32 GB a year. INDEXER_SNAPSHOT_RETENTION=all keeps every bucket instead.

Activity retention

Activity rows are one per state change, so they scale with trading rather than with time alone. At 100, 1,000, and 10,000 activities per pool per day, one year costs 35.6 GB, 356 GB, and 3.56 TB. The default activity retention is 7 days, which holds the cost to 6.8 GB at 1,000 activities per pool per day.

A snapshot

A snapshot is the state at the end of its bucket, plus the bucket’s opening and extreme states. The extremes are stored as reserve pairs, not as a price, because a price is a ratio of two reserves and only the pair reconstructs it exactly.

Polling

The scheduler runs on a short tick, but a pool is fetched only when its own cadence says so. A block subscription moves that schedule forward whenever a block touches the pool.

Idle forPolled every
< 5 min15 s
< 1 h60 s
< 1 d10 min
>= 1 d1 h

A block names the accounts it changed. The subscription turns activity into an immediate poll, so a timer is unnecessary for a quiet network. The arrangement is a fallback in both directions: a dropped stream reconnects from the last processed block and the cadence keeps polls flowing while it is down, and the cadence keeps working if the stream never connects. At 10,000 dormant pools the network cost settles at about 3 requests a second instead of 667.

Registry discovery

The registry’s pools map is the pool set. The node returns at most 1,000 entries inline. Above that cap the indexer detects the limit and discovers pools through the paginated SyncAccountStorageMaps endpoint instead, which has no entry limit. It reads a block range per poll, walks up to 64 pages, and resumes from the block it reached. Discovery is idempotent, so an overlapping range costs one read and nothing else.

Manual refresh

POST /pools/{id}/refresh marks one pool due at the next tick. The response is 202 because the fetch happens on the tick, not inline. The endpoint is limited to one refresh per pool every 5 seconds and a global token bucket of 20 per second. A caller that trips either limit gets 429 with Retry-After. An unknown pool id gets 404 before either limit is consulted.

Vault reads

The vault is requested with the empty word as its asset_vault_commitment. The field is a hint: omitting it means no vault data is returned, and the response then carries an empty asset list that is indistinguishable from a genuinely empty vault. A vault the node refuses to enumerate is an error, not a zero balance.

Capacity

Measured row costs, 10,000 pools, one year. Retention runs five tiers: 1m for 12 h, 5m for 7 d, 1h for 30 d, 6h for 180 d, and 1d forever. The per-pool row counts are arithmetic on those windows (720 + 2,016 + 720 + 720 bounded, plus 365 a year for the daily tier). The byte figures are the measured 360.3 and 97.4 bytes per row.

INDEXER_SNAPSHOT_RETENTION=all keeps every bucket, whose per-pool yearly count is 525,600 + 105,120 + 8,760 + 1,460 + 365 = 641,305.

PolicySnapshot rowsSnapshot storage
Keep every bucket6.41e9/year2.31 TB/year
Default (bounded tiers)4.18e7 total15.05 GB, plus 1.32 GB/year

Activity rows are one per state change. At 100, 1,000, and 10,000 activities per pool per day, one year costs 35.6 GB, 356 GB, and 3.56 TB. The default activity retention is 7 days, which holds that to 6.8 GB at 1,000 per day.

Limits in order of arrival

WallDetail
Registry readThe node does not return a storage map above 1,000 entries inline, so the inline read of the pools map stops at 1,001 pools. The indexer falls back to SyncAccountStorageMaps, which has no entry limit.
RPC ratePolling 10,000 pools per 15 s is 667 get_account calls per second, each carrying the pool’s storage maps and vault. This is not serviceable by a public node. Event-driven fetching is the alternative.
Indexer RAMThe in-memory model keeps one open bucket per tier (5 snapshots) and up to 4,096 activities per pool with no global cap. At 10,000 pools the activity ring dominates at about 3.3 GB.
API payloadGET /pools returns one page, 50 by default and 500 at most, on the order of 1 KB per entry. A client that walks every cursor re-reads about 10 MB per sweep.
Disk2.31 TB a year under INDEXER_SNAPSHOT_RETENTION=all. VACUUM needs a second copy, so retention has to drop tables or partitions rather than rows. DELETE never returns space to the filesystem.
Browser10,000 rows re-rendered every 15 s once the user has loaded every page. The first page is 50 rows; full expansion needs virtualization.

HTTP API

The indexer serves a read-only JSON API. Amounts are decimal strings because u64 and u128 exceed the safe integer range of JSON numbers. spot_price and fee_apr are display-only f64 values.

Cross-origin

The API answers cross-origin only for the origins in INDEXER_ALLOWED_ORIGINS. Only GET and POST are allowed, without credentials. The refresh route is state-changing, so no wildcard is accepted. An empty list means same-origin only.

GET /status

Returns the indexer state.

FieldTypeMeaning
registry_idstring or nullRegistry account id, hex
last_registry_blocknumber or nullLast block a registry read covered
known_poolsnumberPool ids discovered from the registry
fetched_poolsnumberPools whose state has been fetched

GET /pools

Returns one page of pool summaries, keyset paginated by pool id.

QueryDefaultMeaning
tokennoneFaucet id, hex or bech32, to keep only pools holding it
limit50Page size, clamped to 1..=500
cursornoneThe next_cursor of the previous page

Response:

{
  "pools": [ { "id": "0x...", "...": "..." } ],
  "next_cursor": "0x..."
}

next_cursor is absent on the last page. The list omits each pool’s fee_schedule, which is the largest part of a summary and is only needed by a pool page.

GET /pools/{id}

Returns one pool summary with its fee_schedule and the newest history points. The response is the summary fields below, plus:

FieldTypeMeaning
fee_schedulearrayOne { root, amount } per priced note script root
historyarrayNewest snapshots, default 1m resolution

GET /pools/{id}/history

Returns the pool’s sampled history.

QueryDefaultMeaning
resolution1mOne of 1m, 5m, 1h, 6h, 1d

The response is an array of snapshot objects. An unknown resolution returns 400. Without a database the response is an empty array rather than an error: the pool itself is still served.

POST /pools/{id}/refresh

Marks one pool due at the next scheduler tick. Returns 202 on acceptance, 404 for an unknown pool id, and 429 with Retry-After when the per-pool cooldown or the global token bucket denies the request. The fetch happens on the tick, not inline.

Pool summary fields

FieldTypeMeaning
idstringPool account id, hex
token0, token1stringFaucet ids, hex
token0_symbol, token1_symbolstring or nullFaucet symbol
token0_decimals, token1_decimalsnumber or nullFaucet decimals
fee_asset_idstring or nullFaucet of the network note fee asset
fee_schedulearray or absent{ root, amount } per note script root; detail only
reserve0, reserve1stringReserves, vault balances minus the protocol pot
lp_supplystringLP supply
kstringreserve0 * reserve1 as u128
spot_pricenumberreserve1 / reserve0, display only
pot0, pot1stringAccrued protocol fees
lp_fee_bpsnumberThe pool’s LP fee tier
protocol_fee_share_bpsnumberThe pool’s protocol share
blocknumberBlock of the last state change
first_seen_unixnumberWall-clock second the indexer first saw the pool
last_change_unixnumberWall-clock second of the last state change
volume_24h_0, volume_24h_1stringTrailing 24 h volume per token
lp_fees_24h_0, lp_fees_24h_1stringTrailing 24 h LP fees per token
protocol_fees_24h_0, protocol_fees_24h_1stringTrailing 24 h protocol fees per token
swaps_24h, adds_24h, removes_24h, collects_24hnumberTrailing 24 h activity counts
fee_aprnumberAnnualised fee yield for liquidity providers, as a fraction
fee_apr_window_secsnumberSeconds the yield is annualised over

Fee APR

Fees accrue in whichever token the swapper paid, so both sides are valued in token0 at the pool’s own price before the ratio is taken. That price values the reserves at 2 * reserve0 in token0 terms, which is the capital the yield is taken against. The rate is annualised over fee_apr_window_secs, which is the pool’s age while it is below 24 hours. A pool watched for ten minutes reports ten minutes of yield stretched to a year.

Snapshot fields

FieldTypeMeaning
blocknumberBlock of the last state change in the bucket
timestampnumberThe pool’s block_timestamp_last
polled_atnumberWall-clock second the bucket opened
reserve0, reserve1stringState at the end of the bucket
open_reserve0, open_reserve1stringState at the bucket’s first sample
high_reserve0, high_reserve1stringReserves at the bucket’s highest price
low_reserve0, low_reserve1stringReserves at the bucket’s lowest price
lp_supplystringLP supply at the close
kstringConstant product at the close
pot0, pot1stringProtocol pot at the close
price0_cumulativestringCumulative price0 at the close
price1_cumulativestringCumulative price1 at the close

A snapshot’s timestamp is the pool’s block_timestamp_last, so it is zero for a pool that has never been operated on and unchanged across samples that saw no state change. reserve0 and reserve1 are the close of the bucket. The open and extreme reserves are stored as pairs because a price is a ratio of two reserves and only the pair reconstructs it exactly.

App overview

The app is a static web application. Each page is a real HTML document with its own metadata, its own module entry, and a minimal amount of TypeScript. There is no application framework and no client-side router.

RouteFilePurpose
/index.htmlBrowse every registered pool from the indexer
/pool.html?id=<poolid>pool.htmlPool state, swap, add liquidity, remove liquidity
/create.htmlcreate.htmlDeploy a pool and register it
/about.htmlabout.htmlWhat the pools are, and the risks of a testnet

Every page carries the same chrome: the testnet notice as a strip above the header, dismissed once and remembered under the key that <html data-notice-key> names, the footer with the copyright year, and the chart attribution. The index adds the hero drawing, the brand heading under it, a link to the launch page, and the slogan.

Pool ids are canonical account-id hex strings with or without a 0x prefix.

Pool list

The home page lists the pools the indexer serves, 50 per page, polled every 15 s while the tab is visible. Each row carries the pair and its short pool id, the price of one token0 in token1, both reserves, 24-hour volume in both tokens, the LP fee, and the fee yield with the window it is annualised over. Rows are ordered by pair name so a row keeps its place between refreshes. A filter accepts a faucet id, and the Load more control walks the indexer’s keyset cursor.

The page needs no chain read and no WASM. Prices come from the indexer’s reserves through the same exact bigint arithmetic the pool page uses.

Pool page

The pool page renders the state summary, the swap, add-liquidity, and remove-liquidity forms, and the indexer’s sampled history. A pool is absent from the indexer between its registration transaction and the next registry poll, so the page waits instead of failing: it shows a spinner banner, re-fetches every 5 s, and loads the pool when it appears. After a minute it states that a pool which was never registered will not appear. While the page is open, pool metadata and history are re-read every 60 s.

A confirmed action does not change the visible state immediately. The wallet confirms at its own block, the pool account changes in the next block, and the indexer samples within 15 s. After a swap, deposit, or withdrawal, the page polls the chain state, wallet balances, the indexer record, and the history every 5 s for up to 60 s, and stops as soon as reserves or LP supply change.

History comes from GET /pools/{id}/history at 1m, 5m, 1h, 6h, or 1d resolution and is drawn as candlesticks with a crosshair, a price line, and a last-value label. The chart library is the largest dependency in the app and only this page uses it, so it is imported on demand.

The pool page leads with the current price of one token0 in token1, computed by the contract’s own formula against the chain state. Until the first chain read returns, the headline is filled from the indexer’s reserves with the same ratio arithmetic.

Pool operations

All pool actions are notes to the pool account. The app builds the note and its FEE_SPONSORSHIP note through the SDK with submit: false, then hands the resulting TransactionRequest to the connected wallet, which signs, submits, and reports confirmation.

Quoting is local and exact: the SDK’s contract math runs against the latest fetched pool state. Slippage is a basis-point bound in [0, 9999]. Deadlines are the maximum of the wall clock and the pool’s last-seen block timestamp, plus one hour.

The deploy step of pool creation is an exception: the pool is a network account without a key. The wallet sends a small chain fee asset note to the pool address, and the app’s own client consumes that note as the pool’s first transaction, which commits the account and pays its first fee. There is no scriptless deploy, because the 0.17 network auth rejects a transaction with no effect.

Branding

The name and slogan are configuration. The two drawings are files.

ItemEffect
VITE_BRAND_NAMEWordmark, page titles, og:site_name, footer
VITE_BRAND_LOGO_TEXTText mark beside the header logo; falls back to the name
VITE_BRAND_SLOGANOne line under the home page name; empty hides it
src/assets/branding/hero.htmlThe home page hero
src/assets/branding/logo.htmlThe mark in the header of the other pages

Both drawings are injected as they stand. A drawing may carry a <style> element, which is where its own motion belongs.

Build

npm install
npm run dev        # dev server with same-origin gRPC-web RPC proxy
npm test           # vitest, pure-logic unit tests
npm run typecheck  # tsc --noEmit
npm run lint       # oxlint
npm run build      # tsc -b && vite build, output in dist/
npm run preview    # serve dist/ locally

The build output is plain static files: four HTML documents, one CSS file, one small JS bundle per page, and a shared lazy chunk containing the Miden SDK.

App configuration

Environment variables are read at build time (import.meta.env).

VariableDefaultMeaning
VITE_MIDEN_RPC_URLhttps://rpc.testnet.miden.ioMiden node RPC for on-chain reads
VITE_CHAIN_FEE_FAUCET_ID0x4cbdcaffe75f0a317482224dae6436Faucet of the chain’s fee asset
VITE_PROTOCOL_FEE_RECIPIENT(empty)Expected protocol fee recipient; gates pool creation
VITE_REGISTRY_ID(empty)On-chain pool registry account; empty skips registration
VITE_REGISTRY_REGISTER_FEE0Register note consumption fee, in the registry’s fee asset
VITE_INDEXER_URL(empty)Indexer base URL; empty disables browsing and the pool page
VITE_PARA_API_KEY(empty)Para API key; empty keeps the Para option disabled
VITE_PARA_ENVIRONMENTBETAPara environment; PRODUCTION for production builds

Protocol fee recipient

VITE_PROTOCOL_FEE_RECIPIENT accepts a hex account id or a bech32 address. It gates pool creation on the create page. The pool’s actual protocol fee destination is protocol_fee_to in the registry; the create page reads the registry’s fee policy and builds the pool with the registry’s rate.

Registry

VITE_REGISTRY_ID accepts a hex account id or a bech32 address. When set, the create flow emits a register note after the pool is deployed. The register note is a network note consumed by the registry account, so registration is asynchronous: the wallet confirms emission and the node’s transaction builder consumes the note in a later block. VITE_REGISTRY_REGISTER_FEE must match the registry’s note fee schedule.

Indexer

VITE_INDEXER_URL is the application’s pool source. The home page lists every pool the indexer serves, and the pool page loads its state and configuration from it. The indexer reads the on-chain registry for the pool set and each pool account for reserves, token metadata, and the network note fee schedule. An empty value disables browsing and the pool page.

The browser fetches the indexer cross-origin in development, so the indexer’s INDEXER_ALLOWED_ORIGINS must list the dev app origin. The shipped indexer .env.example lists the Vite ports.

RPC

In development the RPC is reached through a same-origin proxy (/rpc.Api, configured in vite.config.ts), so the browser never sends cross-origin gRPC-web requests. Production builds talk to the RPC directly. The app targets the 0.17 testnet.

SDK dependency

The app depends on the published @port0.sh/sdk, pinned to the exact version the deployment is tested against. The runbook covers the deployment.

Wallets

The app wires Bread Wallet through @miden-sdk/miden-wallet-adapter-miden. The wallet picker shows its detection state and opens the install page when the extension is missing. Para Wallet’s 0.17 packages are installed, and the login flow is gated behind VITE_PARA_API_KEY: without a configured key the picker explains the dependency instead of failing later.

Version matrix

PackageVersion
@miden-sdk/miden-sdk0.17.1
@miden-sdk/miden-wallet-adapter-base0.17.0
@miden-sdk/miden-wallet-adapter-miden0.17.0
@miden-sdk/para0.17.0
@getpara/web-sdk3.18.0
@port0.sh/sdk0.1.0, pinned exactly

Wallet menu

The header shows a Login button until a wallet connects. Afterwards it shows the account id. Pressing it opens a menu whose Utils group holds two utilities and whose last item disconnects.

UtilityWhat it does
One-time mintCreates a public faucet whose whole supply is minted to the wallet in one transaction, with maxSupply equal to that supply. Nothing can be minted afterwards.
Send public noteSends the chain fee asset through the wallet’s own requestSend with a public note type, the only path that can target an account that does not exist on chain yet.

Both build their form in a modal, so no page carries a utility it does not need.

Launchpad

The create page can mint a fixed-supply token before pool creation. The faucet is created with maxSupply equal to the initial supply, and the whole supply is minted to the creator’s wallet. The launched faucet id can fill either token field of the pool form.

Funding

A fresh faucet has an empty vault and cannot pay the fee of its own first transaction. The connected wallet sends 0.000266 USDCX to the new faucet, which is exactly the two transaction fees it will pay: consume and mint, at 2 x 133 base units, the measured testnet fee. The faucet’s first transaction consumes that note, which creates the faucet on chain and pays its fee out of the received funds. The supply mint follows as the second transaction.

Validation

FieldRule
Symbol3 to 6 uppercase ASCII letters. The 0.17 protocol rejects digits.
DecimalsInteger from 0 to 8
Initial supplyEntered in tokens, bounded by MAX_AMOUNT of 2^57 base units

The supply bound is the AMM contract’s own limit: amounts are u64 with u128 intermediates and 10,000 bps fee headroom.

Network note fees

Network note fees are denominated in the chain’s fee asset, paid to the chain’s own fee faucet. Since 0.17 the node never runs network transactions for an account whose fee asset differs from the chain’s protocol configuration, so the app reads the faucet from the client instead of asking the user. The creator’s wallet must hold the chain fee asset to pay network note fees when acting on a pool afterwards.

Runbook

How to stand up the stack and deploy a pool. Four repositories take part.

RepositoryRole
port0_contractsPool component, registry component, and note scripts
port0_sdkTypeScript SDK: contract artifacts, the registry account builder, and the pool operations
port0_indexerRust indexer: reads the registry and pools over node RPC, serves HTTP
port0_appStatic web app: browse pools, trade, provide liquidity, create pools

Testnet version: 0.17.1. Every @miden-sdk/* and miden-node-proto pin matches it.

Part 1: one-time operator setup

1. Contracts

In the port0_contracts checkout:

cargo miden build --manifest-path contracts/port0/Cargo.toml --release

The SDK’s artifact exporter builds the packages itself, so this step is only needed to seed a fresh checkout.

2. SDK artifacts

In the port0_sdk checkout:

npm install
npm run generate   # exports the compiled packages into src/generated
npm run build

3. Pool code commitment

npm run pool:commitment

Prints the accepted pool code commitment, currently 0x51c52cca051456d35329f2add92319f6447f1437a8b716279e536c1c0f6fab09. The value covers the pool’s full component set: the port0 component, CodeInspection, the network-account auth, and the fee-policy components. It changes whenever any of those change, and the registry must be seeded with the value the deployed pool produces.

4. Registry account

cp .env.example .env       # set PORT0_FEE_FAUCET_ID and PORT0_POOL_COMMITMENT
npm run registry:deploy

The tool is interactive. On first run it creates an operator wallet, prints its bech32 address, and polls until the fee asset arrives. It consumes the incoming note, then funds the registry with whatever the operator holds above the transaction fee, capped at PORT0_FUND. Send at least 134 base units to proceed; 1000 + 133 funds the registry at the default. The tool then builds the registry with createRegistryAccount, funds it, and deploys it, printing the registry id. No re-run is needed.

A stored registry is reused only when it accepts the configured pool code commitment and admits both registry note kinds. A registry that predates the registry-collect entry is reported and a fresh registry is deployed instead.

Wait timeout: PORT0_WAIT_SECS (default 600). On timeout it prints the address and exits. Keys and store: ~/.miden/stores/<PORT0_STORE>/.

5. Indexer

In the port0_indexer checkout:

cp .env.example .env       # set PORT0_REGISTRY_ID to the registry id from step 4
cargo run --release

Serves http://127.0.0.1:8080, loopback by default for an API behind a reverse proxy. GET /status reports the registry id, the last registry block, and the known and fetched pool counts. Pools appear after the registry poll (default 60 s) and the pool poll (default 15 s).

The API answers cross-origin only for the origins in INDEXER_ALLOWED_ORIGINS. The shipped .env.example lists the Vite dev ports, so npm run dev at http://localhost:5173 is admitted. A deployment that serves the app and the API from one origin needs no entry.

6. App

In the port0_app checkout:

cp .env.example .env       # set VITE_REGISTRY_ID and VITE_INDEXER_URL=http://localhost:8080
npm run dev

VITE_REGISTRY_ID enables registration during pool creation. VITE_INDEXER_URL is the pool source for browsing and the pool page. An empty value disables both.

Part 2: deploy a pool through the app

Prerequisite: the connected wallet holds USDCX. The wallet pays the pool funding note and the register note fee. The measured per-transaction fee is 133 base units.

  1. Open /create.html and connect the wallet.
  2. Choose the token pair. Either launch a fixed-supply token, which creates a faucet and funds it with 2 x 133 = 266 base units of USDCX, or enter two existing faucet ids.
  3. Choose the LP fee tier: 5, 30, or 100 basis points, whichever the registry lists. The protocol’s cut is a share of that fee, read from the registry and carved out of it, so the swapper pays the LP fee tier and nothing more.
  4. Submit. The page runs three on-chain steps and reports each.
StepWhat happensWho pays
Create pool accountBuilt locally, not yet on chain. Fees, the P2ID root, and the registry pointer are initial storage; the LP token key is derived by the contractNothing
DeployThe wallet sends 133 base units of USDCX to the pool; then the app consumes that note as the pool’s first transactionWallet funds; the pool pays its first fee from the received funds
RegisterRegister note via the wallet, sponsored with the registry’s register fee; the node’s transaction builder consumes it against the registry, which verifies the pool code commitment, the registry pointer, the token pair, and the fee values by foreign procedure invocationWallet
  1. The page redirects to /pool.html?id=<poolid>. The pool appears on the home page once the indexer has polled the registry and fetched the pool. Until then the pool page shows a spinner and re-checks the indexer every 5 s, so the redirect lands on a page that fills itself in.

If a stage fails, the page keeps the flow state and shows Retry and Restart. Retry resumes at the failed stage; it does not recreate the pool, and the deploy stage reuses an existing funding note instead of sending a second one. Restart clears the state and starts from the pool account.

Part 3: deploy a pool programmatically

The same steps without the UI. The wallet-signed steps can be submitted by the client directly when the account’s key is in the client keystore.

import { MidenClient } from "@miden-sdk/miden-sdk";
import { XykClient } from "@port0.sh/sdk";

const client = await MidenClient.create({
  rpcUrl,
  feeFaucetId,
  proverUrl: "testnet",
  autoSync: true,
});
const xyk = XykClient.from(client);

// 1. Build the pool account locally. Fees, the P2ID root, and the registry
// pointer are initial storage; the LP token key is derived by the contract.
const account = await xyk.createPoolAccount({
  token0Key,
  token1Key,
  registryId,
  lpFeeBps: 30n,
  protocolFeeShareBps,
  p2idRoot,
  feeFaucetId,
  noteFees,
});
const poolId = account.id();

// 2. Fund it, then 3. deploy by consuming the funding note.
const { txId: fundTx } = await client.transactions.send({
  account: sender.toString(),
  to: poolId.toString(),
  token: feeFaucetId,
  amount: 133n,
  type: "public",
  waitForConfirmation: true,
});
await client.syncChain();
const consumable = await client.notes.listAvailable({ account: poolId.toString() });
await xyk.deployPoolAccount(poolId, consumable[0].id().toString(), {
  waitForConfirmation: true,
});

// 4. Register. The pool was configured when it was created: fees, the P2ID
// root, and the registry pointer are initial storage, and the LP token key is
// derived by the contract.
await xyk.registerPool(
  { registryId, feeFaucetId, registerFee },
  { sender, poolId, asset0: token0FaucetId, asset1: token1FaucetId },
  { submit: true, waitForConfirmation: true },
);

Failure conditions

SymptomCause and fix
PORT0_REGISTRY_ID is requiredThe indexer has no registry id. Set it in .env.
registry response is missing slot pool_registry::pool_registry::poolsThe indexer points at an account that is not the registry. Correct the id.
Pool absent from the home pageThe indexer is not running, the registry id is wrong, or the register note has not been consumed by the node’s builder yet. Check GET /status.
Pool code commitment not accepted at registrationThe registry was seeded with a stale commitment. Re-run npm run pool:commitment and redeploy the registry, or register a pool built with the accepted component set.
Trading disabled on the pool pageThe indexer could not read the pool’s fee asset id or fee schedule. Check the pool’s network-account slots.
funding not received within Ns (balance X/Y)The operator address was not funded within the wait. Send it fee asset base units and re-run, or raise PORT0_WAIT_SECS.
SummaryAnchorMismatchError at registerThe wallet captured its chain anchor at a block other than the request’s bound block; a block landed between the two syncs. The page syncs the wallet immediately before the build to narrow the window. Retry rebuilds the request at a fresh tip.
ERR_NETWORK_ACCOUNT_TRANSACTION_HAS_NO_EFFECTA network-account deploy with no effect. Deploy by consuming a funding note.
procedure with root digest ... could not be foundThe client minor is behind the node. Bump every @miden-sdk/* and miden-node-proto pin to the node’s version.

Network

port0.sh runs on Miden testnet 0.17.1.

FactValue
Protocol and testnet version0.17.1
RPChttps://rpc.testnet.miden.io (gRPC, miden.node.v1.NodeService)
Remote proverhttps://tx-prover.testnet.miden.io
Chain fee faucet0x4cbdcaffe75f0a317482224dae6436
Chain fee assetUSDCX, 6 decimals
Block timeabout 3 seconds
Measured transaction feeabout 133 base units of USDCX

Version rule

A client talks only to a node of the same major and minor version. A stale minor fails with errors that look like application bugs, such as a missing procedure root, a null pointer passed to Rust, or a missing account header record. Pin every @miden-sdk/* and miden-node-proto dependency to the exact version the testnet runs, and bump them together.

PackageVersion
@miden-sdk/miden-sdk0.17.1
@miden-sdk/miden-wallet-adapter-base0.17.0
@miden-sdk/miden-wallet-adapter-miden0.17.0
@miden-sdk/para0.17.0
@getpara/web-sdk3.18.0
@port0.sh/sdk0.1.0
Guest miden SDK0.15.0
miden-protocol, miden-tx, miden-standards0.17.1
VM crates0.35.0

Fees and notes

Notes carry a fee, and the executing account pays it from its own vault. A pool’s fee faucet must equal the chain’s fee faucet, or the node never consumes the pool’s notes. USDCX comes from the network faucet. Do not mint it.

Node state window

The node serves account state for the last 50 blocks. Anchors and block numbers older than that fail with a pruned-block error. A network-note-emitting transaction expires 20 blocks after its reference block, which is about one minute, so slow proving can outrun the window.

Numeric bound

Every pool amount, reserve, and lp_supply is bounded by MAX_AMOUNT = 2^57. The bound keeps the u128 intermediates inside u128::MAX.

Errors

Protocol and integration errors seen in operation, with the cause and the fix.

SymptomCauseFix
null pointer passed to rustA main-thread WASM object passed to the worker clientPass hex strings into every client method
expected instance of X or eTwo WASM copies in the bundle, or a cross-instance objectForce one copy with Vite resolve.dedupe; build all objects from one module
procedure with root digest ... could not be foundThe client minor is behind the nodeBump every @miden-sdk/* and miden-node-proto pin to the node’s version
no account header record found for given IDThe account is not in the local store, or the handle is staleImport the account; pass strings
could not interpret input as a bech32-encoded account id ... invalid checksumA bare hex id failed the hex test and was parsed as bech32Test with /^(?:0x)?[0-9a-f]+$/i, or prefix 0x
Empty-vault assertion on a faucet mint or deployA fresh account pays its own feeFund with a note and consume it as the first transaction
ERR_NETWORK_ACCOUNT_TRANSACTION_HAS_NO_EFFECTAn empty scriptless deploy of a network accountDeploy by consuming a funding note
advice stack read failed on a multisigA two-word fee-conversion preimage where three words are readBuild the three-word MultisigAuthArgs
account's ... component reuses the fee conversion salt ...No salt declared for a multisigDeclare a fresh salt, or the full MultisigAuthArgs
storage root not found ... slot ... blockDeclaring specific storage slots on a public foreign accountUse empty storage requirements; a public target resolves lazily
SummaryAnchorMismatchError ...The wallet captures a chain anchor at its own sync height while a 0.17 multisig summary binds the request’s bound blockSync the wallet immediately before the build. Retry rebuilds the request at a fresh tip
private account ... cannot be retrievedImporting a private accountPrivate accounts are not importable
invalid amount: 250,000An amount parser rejects grouping commas while a formatter emits themStrip commas in the parser
Cannot access 'x' before initializationA render helper runs before a const or let it reads is initializedDeclare the variable above the first call
Cannot read properties of undefined (reading 'includes')Module code runs before a const declared laterMove the bootstrap call to the end of the module

Rule

A protocol rejection names a violated invariant. Resubmitting until the error stops hides the cause and turns a deterministic defect into an intermittent one. Change the code so the invariant holds on the first attempt.

Repositories

Radicle is the canonical source. GitHub is the mirror. Each repository README states the canonical source and links back to it.

Contracts

SourceLocation
Radiclehttps://radicle.network/nodes/iris.radicle.network/rad%3Az2byaq213P9X1TgSh9N1b4dCG73VE
GitHubhttps://github.com/port0-sh/port0_contracts

SDK

SourceLocation
Radiclehttps://radicle.network/nodes/iris.radicle.network/rad%3Az3qSaS569gAZiHRtjiAuxvrCWzhgp
GitHubhttps://github.com/port0-sh/port0_sdk
npmhttps://www.npmjs.com/package/@port0.sh/sdk

App

SourceLocation
Radiclehttps://radicle.network/nodes/iris.radicle.network/rad%3Az3fi1mouSisLs3PhaLxJiUZwhZnLd
GitHubhttps://github.com/port0-sh/port0_app

Indexer

SourceLocation
Radiclehttps://radicle.network/nodes/iris.radicle.network/rad%3Azf39RzWpXwMZ43w5818EPy5jbg7M
GitHubhttps://github.com/port0-sh/port0_indexer

Book

The source of this book is the port0_book repository. It is built with mdBook and served at https://docs.port0.sh.

Contact

ChannelLocation
Sitehttps://port0.sh
GitHubhttps://github.com/port0-sh
X@port0_sh
Telegramt.me/port0_sh
Emailhello@port0.sh

General inquiries and vulnerability reports both go to hello@port0.sh.

The book points to the repositories for the source of truth. A claim that exists only on a social channel has no durable evidence.