Vordium LabsDocs

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

Bash
npm init -y && npm pkg set type=module
npm install ethers tsx

Make a key for the testnet

Bash
node -e "console.log(require('ethers').Wallet.createRandom().privateKey)" > testnet.key && chmod 600 testnet.key
export PRIVATE_KEY=$(cat testnet.key)

Run it

quickstart.ts
// 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)
}
Bash
npx tsx quickstart.ts

The pieces

  • The domain. Two EIP-712 domains, VordCore and VordexSession, each with the chain id and a salt computed from the genesis. Read the genesis from /status and check it against the spec before you sign anything (Signing).
  • The order body. side and orderType go 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 clientNonce in the open orders.

Network facts last checked against the network 2026-10-22 13:00:37 UTC · mainnet spec v28 · testnet spec v8

On this page