First trade in TypeScript
The same first trade on the testnet with ethers.js and fetch — every signature built in a few lines you can read.
This is the Python quickstart in TypeScript, with no Vordium package: ethers.js signs, fetch sends. Seeing each signature built by hand is the quickest way to understand the formats.
Install
npm init -y && npm pkg set type=module
npm install ethers tsxMake a key for the testnet
node -e "console.log(require('ethers').Wallet.createRandom().privateKey)" > testnet.key && chmod 600 testnet.key
export PRIVATE_KEY=$(cat testnet.key)Run it
// Your first trade on the Vordium testnet, in TypeScript: test USDC, a trading key, a limit order, a cancel.
// npm install ethers · run with: PRIVATE_KEY=0x… npx tsx quickstart.ts
import { Wallet, keccak256, concat, getBytes } from 'ethers'
const API = 'https://testnet.vordex.xyz/chain' // the testnet's REST read service
const get = async (path: string) => (await fetch(API + path)).json()
const post = async (url: string, body: unknown) => {
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
return { status: r.status, body: await r.json().catch(() => ({})) }
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const owner = new Wallet(process.env.PRIVATE_KEY!) // a key you made for the testnet — never a mainnet key
const status = await get('/status')
const spec = await (await fetch('https://testnet.vordex.xyz/network-spec.json')).json()
if (spec.testnet !== true || spec.genesisSha256 !== status.genesis_sha256) throw new Error('not the testnet')
// Every signature is bound to this chain's genesis: salt = keccak256(tag ‖ genesis sha256 as 32 raw bytes).
// Each tag is the ASCII text of the domain's tag and the byte 0x01, as hex.
const TAG = { VordCore: '0x564f524449554d5f564f5244434f524501', VordexSession: '0x564f524449554d5f564f5244455853455353494f4e01' }
const domain = (name: 'VordCore' | 'VordexSession') => ({
name, version: '1', chainId: status.chain_id,
verifyingContract: '0x0000000000000000000000000000000000001002',
salt: keccak256(concat([TAG[name], getBytes('0x' + status.genesis_sha256)])),
})
// 1. Test USDC: once per address per day; a second claim the same day is refused, which is fine
const f = await post('https://testnet.vordex.xyz/faucet', { address: owner.address, to: 'chain' })
console.log('faucet:', f.status, f.body.status ?? f.body.error)
// 2. A trading key (a session): the owner signs SessionCreate once; the session key then signs every order
const session = Wallet.createRandom()
const expiresAt = Math.floor(Date.now() / 1000) + 24 * 3600
const sessionNonce = Date.now() // must be higher than this account's last session nonce
const sessionSig = await owner.signTypedData(domain('VordexSession'),
{ SessionCreate: [{ name: 'sessionKey', type: 'address' }, { name: 'expiresAt', type: 'uint256' }, { name: 'nonce', type: 'uint256' }] },
{ sessionKey: session.address, expiresAt, nonce: sessionNonce })
const s = await post(`${API}/session/create`, { owner: owner.address, session_key: session.address, expires_at: expiresAt, nonce: sessionNonce, signature: sessionSig })
console.log('session:', s.status)
for (let i = 0; i < 30; i++) { // received first; it counts once the chain confirms it
const v = await get(`/session/valid?owner=${owner.address}&session_key=${session.address}`)
if (v.valid && v.confirmed) { console.log('session confirmed: true'); break }
await sleep(2000)
}
// 3. A limit buy of ETH (pair 1) 5% under the mark, so it rests on the book instead of filling.
// A price must sit on the market's tick and a size on its lot; the chain drops an order that does not.
const mark = (await get('/oracle/1')).mark_price / 1e6 // the oracle read carries 6 decimals
const pair = (await get('/markets/specs')).specs.pairs.find((p) => p.pair.pair_id === 1)
const tick8 = BigInt(pair.tick_8dec) || 100n // 0 means no tick is set (the chain keeps 6 decimals)
const lot18 = BigInt(pair.lot_18dec) || 10n ** 14n
const price8 = (BigInt(Math.round(mark * 0.95 * 1e8)) / tick8) * tick8 // down onto the tick, signed with 8 decimals
const price6 = Number(price8 / 100n) // the body carries the price to 6 decimals
const want18 = BigInt(Math.round((20 / (Number(price8) / 1e8)) * 1e18)) // about 20 USDC of ETH
const size18 = ((want18 + lot18 - 1n) / lot18) * lot18 // up onto the lot, as an 18-decimal integer
const clientNonce = Date.now() // your id for the order; unique per account
const order = { owner: owner.address, pairId: 1, side: 0, size: size18, price: price8, leverageBps: 10000, orderType: 1,
reduceOnly: false, postOnly: false, triggerPrice8dec: 0, triggerDir: 0, clientNonce }
const orderSig = await session.signTypedData(domain('VordCore'), { PlaceOrder: [
{ name: 'owner', type: 'address' }, { name: 'pairId', type: 'uint32' }, { name: 'side', type: 'uint8' },
{ name: 'size', type: 'uint128' }, { name: 'price', type: 'uint64' }, { name: 'leverageBps', type: 'uint32' },
{ name: 'orderType', type: 'uint8' }, { name: 'reduceOnly', type: 'bool' }, { name: 'postOnly', type: 'bool' },
{ name: 'triggerPrice8dec', type: 'uint64' }, { name: 'triggerDir', type: 'uint8' }, { name: 'clientNonce', type: 'uint64' }] }, order)
const placed = await post(`${API}/order/place`, {
owner: owner.address, sessionKey: session.address, pairId: 1, side: 'buy', orderType: 'Limit',
size_18dec: size18.toString(), price: price6 / 1e6, leverageBps: 10000, reduceOnly: false, post_only: false,
trigger_price_8dec: 0, trigger_dir: 0, clientNonce, signature: orderSig })
console.log('order:', placed.status, placed.body.status)
// 4. Find it among the open orders once the chain has applied it, then cancel it
let resting: any
for (let i = 0; i < 30 && !resting; i++) {
resting = (await get(`/orders/${owner.address}`)).find((o: any) => o.client_nonce === clientNonce)
if (!resting) await sleep(1000)
}
console.log('resting:', resting.id, resting.side, resting.order_type, resting.price_8dec / 1e8)
const cancelSig = await session.signTypedData(domain('VordCore'),
{ CancelOrder: [{ name: 'owner', type: 'address' }, { name: 'orderId', type: 'uint64' }] }, { owner: owner.address, orderId: resting.id })
const c = await post(`${API}/order/cancel`, { order_id: resting.id, owner: owner.address, session_key: session.address, signature: cancelSig })
console.log('cancel:', c.status)
for (let i = 0; i < 30; i++) { // gone from the open orders once the cancel is applied
if (!(await get(`/orders/${owner.address}`)).some((o: any) => o.id === resting.id)) { console.log('cancelled: yes'); break }
await sleep(1000)
}npx tsx quickstart.tsThe pieces
- The domain. Two EIP-712 domains,
VordCoreandVordexSession, each with the chain id and a salt computed from the genesis. Read the genesis from/statusand check it against the spec before you sign anything (Signing). - The order body.
sideandorderTypego by name in the body ("buy","Limit"), and by number in the signed data (0 and 1). The price goes in the body as a decimal number; you sign it as an integer with 8 decimals. - Reading back. The order is identified by your
clientNoncein the open orders.
Network facts last checked against the network 2026-10-22 13:00:37 UTC · mainnet spec v28 · testnet spec v8