Skip to content

Run the local exchange ​

This guide starts the full exchange on a local Anvil chain: contracts, maker quotes, a random taker, the API with the relayer, and the web application.

For the public Monad testnet, use Run on Monad testnet.

Test deployment

This deployment uses test USDC and a test oracle. All accounts are public Anvil test accounts. Do not send real funds to them.

Requirements ​

  • Node.js 22.12 or later
  • pnpm 11
  • Foundry. The contracts use Solidity 0.8.28.

Run the commands from the repository root. If Foundry is not on the shell path, add ~/.foundry/bin to PATH.

PortProcess
8545Anvil
3001API and relayer
3000Web application

Install and check ​

sh
pnpm install
forge build --root contracts
forge test --root contracts
pnpm typecheck
pnpm test
pnpm test:abi
pnpm build

See Checks and troubleshooting for the scope of each check.

Start the exchange ​

Run each long-running command in its own terminal, in this sequence.

1. Start Anvil ​

sh
pnpm local:chain

The script mines one block each second (--block-time 1). Thus the block time and the oracle clock move without other transactions. The chain ID is 31337.

2. Build, deploy, and seed ​

sh
forge build --root contracts
pnpm local:deploy
pnpm demo seed

local:deploy deploys SessionRegistry, TestOracle, TestUSDC, an ETH market, and a MON market. It writes deployments/local.json.

demo seed deposits 200,000 test USDC for each maker in each market. It registers the two maker ladders and sends the first quote of each market.

Two environment variables change a new deployment:

VariableDefaultEffect
LOCAL_OBSERVATION_SECONDS60The observation interval of the new markets. The Hedging demo uses 10.
LOCAL_MARKET_CONTRACTMarketSet QueuedMarket to deploy the market with the on-chain order queue.
sh
LOCAL_OBSERVATION_SECONDS=10 pnpm local:deploy
LOCAL_MARKET_CONTRACT=QueuedMarket pnpm local:deploy

Before a new deployment

Stop the demo loop and the API. Delete .local/local-indexer.json. Then deploy and seed again.

3. Start the demo loop ​

sh
pnpm demo serve

Keep it running while you trade. Stop it with Ctrl+C. See The demo loop for what it does.

pnpm demo run does the deployment, the seed, and the loop in one command.

4. Start the API ​

The root .env file can hold the testnet settings. Node.js does not replace variables that are already set. Thus the values on the command line select the local deployment.

sh
DEPLOYMENT_FILE=deployments/local.json RPC_URL=http://127.0.0.1:8545 \
STATE_FILE=.local/local-indexer.json INDEX_BATCH_BLOCKS=1000 INDEX_CONFIRMATIONS=0 \
RELAYER_PRIVATE_KEY=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 \
pnpm dev:services

The relayer key is the key of public Anvil account 9. The API runs at http://127.0.0.1:3001.

5. Start the web application ​

The file web/.env.local must hold the local values:

dotenv
NEXT_PUBLIC_API_URL=http://127.0.0.1:3001
NEXT_PUBLIC_CHAIN_ID=31337
NEXT_PUBLIC_CHAIN_NAME=Monad Local Demo
NEXT_PUBLIC_RPC_URL=http://127.0.0.1:8545
sh
pnpm dev:web

Open http://localhost:3000.

The file web/.env.testnet keeps the Monad testnet values. Copy it over web/.env.local to go back to testnet. After a change of these values, stop the dev server, remove web/.next, and start the server again.

The default browser origin is http://localhost:3000. If you use a different origin, set WEB_ORIGIN for the API.

Accounts ​

Anvil indexUse
0Deployer, oracle signer, quoter, maker 0
1Maker 1 and random taker
6Browser wallet
7Keeper liquidate
8pnpm test:smoke
9Relayer

Separate accounts prevent nonce conflicts with the demo loop.

Import Anvil account 6 into the wallet. Add this network: RPC http://127.0.0.1:8545, chain ID 31337, currency symbol MON. After a restart of Anvil, reset the account activity in the wallet, because the nonces start again at zero.

Trade in the terminal ​

  1. Select Connect wallet.
  2. Select Faucet. It mints 10,000 test USDC.
  3. Open the Account dialog. Enter an amount and select Approve and deposit.
  4. Select Enable on the trading session row.
  5. Place an order.

The main wallet signs deposits, withdrawals, session authorization, and revocation. The browser session signs orders and cancellations. The relayer submits these signed actions.

The two markets have separate collateral. A closed book position can keep a funding obligation. Wait for the next accepted observation before you expect its margin to become available. A frozen mark can also block a withdrawal.

The demo loop ​

pnpm demo serve uses Anvil account 0 as the quoter and Anvil account 1 as the taker. The loop keeps a local nonce for each account and does not wait for each receipt. It logs reverted transactions.

Quotes ​

The loop sends one quote call for each market each second. Each quote expires after 30 seconds, so a stopped loop does not leave a live ladder for long.

The quote center has three parts:

text
center = feed mark + drift + jump
PartRule
Feed mark1.573 for MON, 0.36 for ETH
DriftA random walk in ticks of 0.001 that returns to the feed mark. Each second: drift += -0.02 * drift + noise. The noise is uniform from -2 to 2 ticks. The drift stays within 40 ticks.
JumpThe makers raise their quotes after a large oracle move. Each observation adds the squared log return times 365 / 7, as annual variance. The jump loses half of its size each 60 seconds.

A 4% oracle move adds about 0.087 to the center, which is 87 ticks. Calm noise adds less than one tick. Thus the mark rises after a shock, and a long position also gains from the mark for a short time.

Observations ​

When an observation is due, the quote also carries a signed observation of the test price. There are no separate price transactions.

The price of the observation is the last oracle price times one factor:

  • a pending shock, if pnpm demo shock or the API recorded one for that market, or else
  • calm noise: a random log return with an annual volatility of 110% for MON and 60% for ETH, limited to 1% for each observation.

Taker orders ​

Each 2 seconds, Anvil account 1 sends one IOC order of 1 to 8 contracts for each market. The limit price crosses the best price by 2 ticks. When the position of the account is above 30 contracts, the order reduces the position with a chance of 4 in 5.

QueuedMarket ​

On a QueuedMarket, the loop sends quoteAndClear in place of quote when the queue has entries. Queued orders thus clear within about one second. The services report queue: true in GET /markets. The web application does not show the queue.

Move the oracle price ​

Use a second terminal while the loop runs:

sh
pnpm demo shock MON -4
pnpm demo shock ETH 3

The command records a percent for one market in .local/demo-shock.json. The loop applies it at the next observation of that market. With the default interval, that observation comes within 60 seconds. The squared return shows in the funding and in the realized variance of that observation.

The percent is a simple price change. -4 multiplies the oracle price by 0.96. The value must be above -100 and at most 100. If you omit it, the command uses -4.

The web application has the same function. The Hedging demo page shows a bar with the buttons Shock −4% and Rebound +3%. The buttons call POST /demo/shock on the API. The bar shows only when GET /health reports shockEnabled: true, which is always the case on the local chain 31337 with the test oracle.

A shock does not need a stopped loop

pnpm demo shock and the shock bar work while the loop runs. The keeper commands shock and rebound are different: they send their own observation from Anvil account 0, so you must stop the loop first.

Keeper commands on the local chain ​

For a keeper command, use the same overrides as the API and set KEEPER_PRIVATE_KEY.

  • liquidate can use Anvil account 7.
  • oracle, shock, rebound, and recover must use Anvil account 0, the test oracle owner. Stop the demo loop before you use account 0, because the loop sends from that account.

See Scripts and keeper commands.

Integration check ​

With a new deployment and a running API, run:

sh
pnpm test:smoke

This check uses public Anvil account 8. It deposits test collateral, authorizes a new session, places an order through the relayer, checks its confirmed event, rejects a replay, cancels the order, and verifies session revocation. It changes the local chain state. Do not point it at the public testnet.

Keep the oracle current while you use the terminal. After a long pause, use the recover keeper command. Recovery does not restore omitted variance.

Deployed on Monad testnet.