Run on Monad testnet
This guide deploys the contracts to the public Monad testnet and runs the operator loop, the API, and the web application.
Test prices and test collateral
This deployment uses test USDC and operator-set test prices. The prices are not live ETH or MON prices. Do not deposit real assets. Do not use the public Anvil keys on testnet.
Monad testnet uses chain ID 10143 and test MON for gas. See the network documentation and the faucet.
The contracts are public, the application is local
The contracts are on the public testnet. The web application and the API still run on your computer. A shared public terminal also needs hosted web, API, and operator services.
Prepare accounts
Create three separate test accounts. Fund each one with test MON.
| Variable | Use |
|---|---|
DEPLOYER_PRIVATE_KEY | Deploys the contracts, signs test prices, sends the quote calls, and owns the first maker ladder |
MAKER_PRIVATE_KEY | Owns the second maker ladder and sends the random taker orders. The mark needs two different makers. |
RELAYER_PRIVATE_KEY | Pays gas for signed browser orders |
Use a fourth funded test account in the browser wallet. Separate accounts prevent transaction nonce conflicts.
Put these values in the root .env file:
TESTNET_RPC_URL=https://testnet-rpc.monad.xyz
RPC_URL=https://testnet-rpc.monad.xyz
DEPLOYMENT_FILE=deployments/monad-testnet.json
STATE_FILE=.local/monad-testnet-indexer.json
SERVICE_PORT=3001
WEB_ORIGIN=http://localhost:3000
INDEX_BATCH_BLOCKS=100
DEPLOYER_PRIVATE_KEY=
MAKER_PRIVATE_KEY=
RELAYER_PRIVATE_KEY=The public Monad testnet RPC limits eth_getLogs to 100 blocks, so INDEX_BATCH_BLOCKS must be 100 or less.
Copy web/.env.example to web/.env.local. It selects Monad testnet for the browser. Restart the web server after each change of these values.
Keys
Never put a private key in a NEXT_PUBLIC_ variable or in the web directory. Never copy .env contents into documentation.
Gas on Monad
Monad charges for the gas limit of a transaction, not for the gas used. The scripts estimate each transaction and set a tight limit:
| Transaction | Margin on the estimate |
|---|---|
| Deployment and seed | 20% |
| Quote and observation | 10% |
| Random taker order | 15% |
Fast loop (serve-fast) | 10% |
Standard operator
Run from the repository root:
pnpm install
export PATH="$HOME/.foundry/bin:$PATH"
pnpm contracts:build
pnpm testnet deploy
pnpm testnet seed
pnpm testnet recover
pnpm testnet serveKeep serve running. Run only one operator loop for a deployment.
| Command | Effect |
|---|---|
deploy | Deploys SessionRegistry, TestOracle, TestUSDC, and one Market for each asset, with 300-second observations. It saves deployments/monad-testnet.json. |
seed | Mints and deposits 200,000 test USDC for each maker in each market. Registers one ladder for each maker with setLadder. |
recover | Submits a current test price for each market. Use it before the first serve and after a long pause. |
serve | Runs the quote loop and the random taker until you stop it with Ctrl+C. |
deploy stops if the deployment file already exists. Skip it if the file already selects the intended contracts. The command saves each transaction hash in a journal file before it waits for the receipt. If the deployment stops after it sends a transaction, run deploy again. It continues from the journal.
What serve does
Quotes. serve sends one quote(center, tick, validUntil, updateData) call for each market each REQUOTE_SECONDS (default 300). The call moves the two ladders to the new center and updates the mark. The book shows the ladder levels as virtual orders: 10 levels on each side for each maker.
- The center is the feed mark plus a drift. At each quote, the drift moves by a random value from -3 to +3 ticks of 0.001. It stays within 30 ticks.
- A quote is valid for
3 * REQUOTE_SECONDS + 30seconds. ThusREQUOTE_SECONDSmust be from 30 to 1190. - When an observation is due,
servequotes at once and attaches the signed test price to the same call. There are no separate price transactions. The test price does not change in this loop.
Taker orders. serve also sends a random taker order for each market each TAKER_SECONDS (default 240, minimum 60, 0 stops it). The MAKER_PRIVATE_KEY account sends one IOC order of 1 to 8 contracts. The limit is the best opposite price plus or minus 2 ticks. When the position of the account is above 30 contracts, the order reduces it 4 times in 5. The taker skips a turn when an observation is due, when the book side is empty, or when the account has less than 0.3 MON.
Errors. serve logs an error and tries that market again after 15 seconds. It stops only when the observation gap is larger than the gap limit of the market (three intervals). Then run recover before you start serve again.
Cost
Each quote logs its center, the observation, the gas limit, and the cost in MON. Each taker order logs its side, size, limit, gas limit, cost, and the new mark.
| Item | Projection in the runbook |
|---|---|
Quotes, two markets, REQUOTE_SECONDS=300 | About 0.33 MON each hour, paid by the deployer key |
Quotes, two markets, REQUOTE_SECONDS=180 | About 0.46 MON each hour |
| One taker order on the MON market | 228k to 323k gas, about 0.027 to 0.038 MON at 102 gwei |
Taker orders, two markets, TAKER_SECONDS=240 | About 1 MON each hour, paid by the MAKER_PRIVATE_KEY account |
These values are projections from the root file TESTNET.md. Use the log lines of your run for the actual cost. Keep the deployer account and the maker account funded.
Start the application
In a second terminal:
pnpm dev:servicesIn a third terminal:
pnpm dev:webOpen http://localhost:3000. Connect the test wallet and switch to Monad Testnet. Mint test USDC, approve and deposit, then enable a session. Start with an order of 0.1 contracts. Check its confirmed fill in the positions and the activity. Each market has separate collateral.
Inject a test price move
Open a position before the injection if you want to see its funding result. Then wait for one accepted observation, which activates the funding position. A new book position does not receive the payment of the prior interval. See delayed funding activation.
Stop serve with Ctrl+C. Then run one command at a time:
pnpm testnet shock MON
pnpm testnet rebound MON| Command | Log return | Simple price change |
|---|---|---|
shock | -0.04 | About -3.92% |
rebound | +0.03 | About +3.05% |
Replace MON with ETH to select the other market. Each command waits for a real observation boundary and uses the deployer key. It does not need the maker key or the relayer key.
A command signs a changed test price and submits it with one submitObservation call. It does not change the native test MON token. It does not force the order-book mark to move. It does not execute a swap in an LP pool. Without an existing funding position, there is no trader funding result to show.
After the injections, run recover, then start serve again.
Shock buttons on testnet
The shock bar of the Hedging demo page works on testnet when you start the API with SHOCK_ENABLED=true. The API writes the request to .local/testnet-shock.json. The running loop (pnpm testnet serve or pnpm testnet serve-fast) applies it at the next observation and removes it. With 300-second observations, a shock can take up to 300 seconds to apply. The API and the loop must use the same repository directory.
Each visitor of a public API can then move the test price. Keep SHOCK_ENABLED off for a public address, unless you accept this.
pnpm testnet shock and pnpm testnet rebound are the commands for a stopped loop. pnpm demo shock is for the local chain only.
Interrupted observations
Stop the operator loop before recovery:
pnpm testnet recover
pnpm testnet serveRecovery accepts a current test price. It does not recreate missing returns. A gap above the gap limit skips funding. Use this only for the test deployment.
Frequent fully on-chain fills
The QueuedMarket contract adds quoteAndTake. This call updates the maker ladders, accepts a due observation, and matches one IOC order against the on-chain book. Positions, fees, and collateral change in the same transaction as the fill.
The existing Market contracts cannot get this function. Deploy to a separate deployment file. These commands send paid testnet transactions:
pnpm contracts:build
export TESTNET_DEPLOYMENT_FILE=deployments/monad-fast.json
pnpm testnet deploy-fast
pnpm testnet seed-fast
pnpm testnet recover
FAST_SECONDS=15 pnpm testnet serve-fastFor the services and the web application, select the same file with DEPLOYMENT_FILE. Use serve-fast alone for this deployment. Do not run the standard serve loop on it.
The serve-fast operator targets one quote and one contract of trading for each market each FAST_SECONDS. The operator trades its own collateral. This operator fill model, and the operator-signed observations, are for testnet only. See Operator fills on testnet. seed-fast puts the other maker on the inside level, so the operator does not trade against its own ladder. An IOC order can fill in part or cancel. The log reports the confirmed filled amount.
| Setting | Default | Purpose |
|---|---|---|
FAST_SECONDS | 15 | Target interval for each market, from 5 to 60 seconds |
FAST_BUDGET_FILE | .local/fast-budget.json | Saved transactions and cost records. It does not enforce a spending limit. |
Cost estimate
At 102 gwei, the read-only testnet estimates for the two-maker fixture were:
| Call, for each market | Estimated gas | Gas limit with 10% margin |
|---|---|---|
| Quote and one-contract fill, existing position | 168,548 | 185,403 |
| Quote and fill with an observation | 246,169 | 270,786 |
| First opening fill | 287,177 | 315,895 |
| Reopen a flat position | 236,088 | 259,697 |
With 240 calls for each market each hour, including 12 calls with observations, the projection is about 9.29 MON each hour for the two markets. This is an RPC estimate with synthetic code and storage overrides. It is not an hour-long live run. Run the estimate again for the current fee:
pnpm testnet:estimate-onchainThe script saves its report in .local/onchain-gas-estimate.json. It uses public test keys and never sends a transaction.
10 MON each hour is a target, not a limit
The operator logs the hourly cost and continues at the configured interval when the cost is above this target. It has no spending limit. FAST_HOURLY_MON is not used.
Restart and recovery
The fast loop saves each signed transaction and its cost record in FAST_BUDGET_FILE before broadcast. After a restart, it checks the receipt or sends the same signed bytes again. Keep this file across restarts.
A .lock file prevents two operators at the same time. If the process stops with an error, make sure that it is not running, then remove only that lock file.
A long pause can halt the oracle. serve-fast stops when the oracle is halted. Run recover for this deployment before you start it again.
Check status
The deployment addresses are in the deployment file. The API checks that the chain ID of its RPC matches that file. Do not use the local Anvil deployment file or its indexer state for testnet.
See the deployment record for the addresses in the current deployment files.