Quickstart
This page opens a real position on Monad mainnet from a standalone script, then reads it back and closes it. Budget about 10 minutes.
If you would rather trade without holding gas or signing every transaction, skip to Gasless Trading — but read this page first, because the trade parameters are identical.
Real funds
Everything here targets mainnet. Start with a few dollars of collateral.
1. Prerequisites
- Node 18+ and
viem(npm i viem). - An account holding
MONfor gas andUSDCfor collateral. - A Monad mainnet RPC URL.
2. Shared setup
Create config.ts. Every other snippet on this site assumes these exports.
import { defineChain, createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
export const monad = defineChain({
id: 143,
name: 'Monad',
nativeCurrency: { name: 'MON', symbol: 'MON', decimals: 18 },
rpcUrls: { default: { http: [process.env.RPC_URL!] } },
blockExplorers: { default: { name: 'MonadVision', url: 'https://monadvision.com' } },
contracts: { multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' } },
})
export const DIAMOND = '0xea1b8E4aB7f14F7dCA68c5B214303B13078FC5ec' as const
export const USDC = '0x754704Bc059F8C67012fEd69BC8A327a5aafb603' as const
export const LVUSD = '0xFD44B35139Ae53FFF7d8F2A9869c503D987f00d1' as const
export const API = 'https://service.leverup.xyz'
export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
export const publicClient = createPublicClient({ chain: monad, transport: http() })
export const walletClient = createWalletClient({ account, chain: monad, transport: http() })3. Find the pair
Pair addresses are not stable across listings. Look them up:
import { API } from './config'
const res = await fetch(`${API}/v1/pairs?symbol=BTC&size=1`)
const { content } = await res.json()
const pairBase = content[0].base as `0x${string}`
console.log(content[0].pairName, pairBase, content[0].status)4. Fetch oracle data
Opening a position requires a fresh oracle payload and the fee to attach. Fetch it immediately before sending the transaction.
async function fetchOracleUpdate(pairBase: `0x${string}`, collateral: `0x${string}`) {
const res = await fetch(`${API}/v1/oracle/price/updates/by-position`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pairBase,
collateral,
blockChain: 'MONAD',
options: {
includeEncodingData: true,
includeFee: true,
includePrice: true,
includePublishTime: false,
},
}),
})
if (!res.ok) throw new Error(`oracle: HTTP ${res.status}`)
const data = await res.json()
return {
updateData: {
pythPriceUpdateData: data.pythPriceUpdateData ?? [],
pythProPriceUpdateData: data.pythProPriceUpdateData ?? [],
},
// Attach the sum as msg.value; `verifition_fee` is spelled that way in the API.
value: BigInt(data.updateFee ?? '0') + BigInt(data.verifition_fee ?? '0'),
extraFee: 0n,
}
}5. Approve collateral
One-time, per collateral token. Approve the Diamond, not a facet address.
import { parseUnits, erc20Abi } from 'viem'
import { publicClient, walletClient, DIAMOND, USDC } from './config'
const hash = await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: 'approve',
args: [DIAMOND, parseUnits('1000', 6)],
})
await publicClient.waitForTransactionReceipt({ hash })6. Open a position
$100 notional BTC long, backed by 10 USDC of margin — roughly 10x.
import { parseUnits } from 'viem'
import { publicClient, walletClient, DIAMOND, USDC, LVUSD } from './config'
const OPEN_ABI = [{
type: 'function',
name: 'openMarketTradeV2',
stateMutability: 'payable',
inputs: [
{
name: 'data', type: 'tuple', components: [
{ name: 'pairBase', type: 'address' },
{ name: 'isLong', type: 'bool' },
{ name: 'tokenIn', type: 'address' },
{ name: 'lvToken', type: 'address' },
{ name: 'amountIn', type: 'uint96' },
{ name: 'qty', type: 'uint128' },
{ name: 'price', type: 'uint128' },
{ name: 'stopLoss', type: 'uint128' },
{ name: 'takeProfit', type: 'uint128' },
{ name: 'broker', type: 'uint24' },
],
},
{
name: 'updateData', type: 'tuple', components: [
{ name: 'pythPriceUpdateData', type: 'bytes[]' },
{ name: 'pythProPriceUpdateData', type: 'bytes[]' },
],
},
{ name: 'extraFee', type: 'uint96' },
],
outputs: [{ name: 'tradeHash', type: 'bytes32' }],
}] as const
const oracle = await fetchOracleUpdate(pairBase, USDC)
// Read the current price from the same oracle response and allow 1% of slippage.
const markPrice = 100_000 // replace with the live price
const qty = parseUnits((100 / markPrice).toFixed(10), 10) // $100 notional
const worstPrice = parseUnits(String(markPrice * 1.01), 18) // long -> max price
const hash = await walletClient.writeContract({
address: DIAMOND,
abi: OPEN_ABI,
functionName: 'openMarketTradeV2',
args: [
{
pairBase,
isLong: true,
tokenIn: USDC,
lvToken: LVUSD,
amountIn: parseUnits('10', 6),
qty,
price: worstPrice,
stopLoss: 0n,
takeProfit: 0n,
broker: 0,
},
oracle.updateData,
oracle.extraFee,
],
value: oracle.value,
})
await publicClient.waitForTransactionReceipt({ hash })This does not mean you have a position yet
openMarketTradeV2 creates a pending trade. A keeper fills it in a follow-up transaction. Poll until the position appears.
7. Read the position back
import { formatUnits } from 'viem'
const READ_ABI = [{
type: 'function',
name: 'getPositionsV4',
stateMutability: 'view',
inputs: [{ name: 'user', type: 'address' }, { name: 'pairBase', type: 'address' }],
outputs: [{
type: 'tuple[]', name: '', components: [
{ name: 'positionHash', type: 'bytes32' },
{ name: 'pair', type: 'string' },
{ name: 'pairBase', type: 'address' },
{ name: 'tokenIn', type: 'address' },
{ name: 'marginToken', type: 'address' },
{ name: 'isLong', type: 'bool' },
{ name: 'margin', type: 'uint96' },
{ name: 'qty', type: 'uint128' },
{ name: 'entryPrice', type: 'uint128' },
{ name: 'stopLoss', type: 'uint128' },
{ name: 'takeProfit', type: 'uint128' },
{ name: 'openFee', type: 'uint96' },
{ name: 'executionFee', type: 'uint96' },
{ name: 'fundingFee', type: 'int256' },
{ name: 'timestamp', type: 'uint32' },
{ name: 'holdingFee', type: 'uint96' },
{ name: 'earliestCloseTime', type: 'uint256' },
{ name: 'accruedFundingFee', type: 'int256' },
{ name: 'accruedHoldingFee', type: 'uint256' },
],
}],
}] as const
for (let i = 0; i < 30; i++) {
const positions = await publicClient.readContract({
address: DIAMOND,
abi: READ_ABI,
functionName: 'getPositionsV4',
args: [account.address, pairBase],
})
if (positions.length > 0) {
const p = positions[0]
console.log('hash ', p.positionHash)
console.log('side ', p.isLong ? 'LONG' : 'SHORT')
console.log('qty ', formatUnits(p.qty, 10))
console.log('entry ', formatUnits(p.entryPrice, 18))
console.log('margin ', formatUnits(p.margin, 18), 'LVUSD')
break
}
await new Promise((r) => setTimeout(r, 1000))
}8. Close it
const CLOSE_ABI = [{
type: 'function',
name: 'closeTrade',
stateMutability: 'nonpayable',
inputs: [{ name: 'positionHash', type: 'bytes32' }],
outputs: [],
}] as const
const hash = await walletClient.writeContract({
address: DIAMOND,
abi: CLOSE_ABI,
functionName: 'closeTrade',
args: [positionHash],
})
await publicClient.waitForTransactionReceipt({ hash })Like opening, this requests a close. The position disappears once the keeper settles it.
Where to go next
| Goal | Page |
|---|---|
| Understand the units you just used | Precision & Units |
| Limit orders, TP/SL, margin management | Onchain Integration |
| Trade without gas or wallet popups | Gasless Trading |
| Query data over HTTP instead | REST API |
| Something reverted | Error Reference |