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.
| Port | Process |
|---|---|
| 8545 | Anvil |
| 3001 | API and relayer |
| 3000 | Web application |
Install and check
pnpm install
forge build --root contracts
forge test --root contracts
pnpm typecheck
pnpm test
pnpm test:abi
pnpm buildSee 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
pnpm local:chainThe 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
forge build --root contracts
pnpm local:deploy
pnpm demo seedlocal: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:
| Variable | Default | Effect |
|---|---|---|
LOCAL_OBSERVATION_SECONDS | 60 | The observation interval of the new markets. The Hedging demo uses 10. |
LOCAL_MARKET_CONTRACT | Market | Set QueuedMarket to deploy the market with the on-chain order queue. |
LOCAL_OBSERVATION_SECONDS=10 pnpm local:deploy
LOCAL_MARKET_CONTRACT=QueuedMarket pnpm local:deployBefore 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
pnpm demo serveKeep 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.
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:servicesThe 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:
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:8545pnpm dev:webOpen 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 index | Use |
|---|---|
| 0 | Deployer, oracle signer, quoter, maker 0 |
| 1 | Maker 1 and random taker |
| 6 | Browser wallet |
| 7 | Keeper liquidate |
| 8 | pnpm test:smoke |
| 9 | Relayer |
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
- Select
Connect wallet. - Select
Faucet. It mints 10,000 test USDC. - Open the
Accountdialog. Enter an amount and selectApprove and deposit. - Select
Enableon the trading session row. - 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:
center = feed mark + drift + jump| Part | Rule |
|---|---|
| Feed mark | 1.573 for MON, 0.36 for ETH |
| Drift | A 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. |
| Jump | The 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 shockor 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:
pnpm demo shock MON -4
pnpm demo shock ETH 3The 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.
liquidatecan use Anvil account 7.oracle,shock,rebound, andrecovermust 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:
pnpm test:smokeThis 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.