Vordium LabsDocs

Your first agent

Register an agent key, give it a policy and a market limit, let it trade, watch the chain refuse an order the policy forbids, then revoke it.

Testnet only · the full agent rules are enforced on the testnet

An agent is a key that trades for your account inside limits the chain enforces (Agents). This sample creates one, gives it a small budget for one market, lets it place an order inside the budget and one outside it, and revokes it. Run the Python quickstart first so your account holds test USDC.

Python
# Your first agent on the Vordium testnet: register it, give it a policy, let it trade, watch the chain refuse
# what the policy forbids, then revoke it.   pip install vordium==1.4.6 requests
import os, time, secrets, requests
from decimal import Decimal, ROUND_FLOOR
from eth_account import Account
from eth_account.messages import encode_typed_data
from vordium import Vordex, domains, use_network
from vordium.dex import BUY, LIMIT
from vordium.agent_ops import registration, set_policy, sign_owner_op
from vordium.policy import PolicyV1
from vordium.rollb import read_signing_phase

use_network("testnet")
API = "https://testnet.vordex.xyz/chain"
owner = Account.from_key(os.environ["PRIVATE_KEY"])   # the account the agent trades for
agent = Account.from_key("0x" + secrets.token_hex(32))  # the agent's own key: it never holds funds
status = requests.get(f"{API}/status", timeout=10).json()
phase = read_signing_phase()                           # the signing format the chain expects now
now = int(time.time() * 1000)

def submit(body):
    r = requests.post(f"{API}/submit", json=body, timeout=15)
    return r.status_code

def agent_record():
    r = requests.get(f"{API}/agents/{owner.address}/{agent.address}", timeout=10)
    return r.json() if r.status_code == 200 else {}

def wait(check, tries=40):
    for _ in range(tries):
        v = check()
        if v:
            return v
        time.sleep(1.5)

# 1. Register the agent: a name, a mandate and an expiry (on the chain's clock). The owner signs.
op, value = registration(owner.address, agent.address, "Rests small ETH bids", now + 3600_000, now, phase, name="Grid bot")
print("register:", submit(sign_owner_op(owner.key.hex(), op, value)))
print("registered as:", wait(lambda: agent_record().get("name")))

# 2. Its policy: place and cancel only, at most 50 USDC of exposure, 1x, a daily loss limit, expiring in an hour
policy = PolicyV1(caps_bitmask=1 | 2, side_mode=0, max_total_exposure_6dec=50_000_000, max_leverage_bps=10_000,
                  daily_loss_limit_6dec=5_000_000, daily_fee_cap_6dec=0, ops_per_window=0, ops_window_ms=0,
                  active_from_ms_of_day=0, active_to_ms_of_day=0, oracle_band_bps=1_000, lock_auto_clear=False,
                  expires_ms=now + 3600_000, enabled=True)
op, value = set_policy(owner.address, agent.address, policy, now + 1, phase, now)
print("policy:", submit(sign_owner_op(owner.key.hex(), op, value)))
print("policy stored:", bool(wait(lambda: agent_record().get("policy_v1"))))

# 2b. A policy denies every market it does not name. Allow ETH (pair 1): orders up to 0.02 ETH, a position up to 0.05
limit = {"owner": owner.address, "agent": agent.address, "pair": 1, "allowed": True, "maxOrder18dec": 2 * 10**16,
         "maxPosition18dec": 5 * 10**16, "maxLeverageBps": 10_000, "nonce": now + 2}
print("market limit:", submit(sign_owner_op(owner.key.hex(), "SetAgentMarketLimit", limit)))
print("market limit stored:", bool(wait(lambda: agent_record().get("market_limits"))))

# 3. The agent trades as a session key of the owner. An account holds one session at a time, so a session held
#    for another key (a browser, an earlier run) is ended first, then the owner opens one naming the agent key.
dom = domains.domain("VordexSession", status["chain_id"], status["genesis_sha256"])
def owner_sign(types, msg):
    return "0x" + owner.sign_message(encode_typed_data(domain_data=dom, message_types=types, message_data=msg)).signature.hex().removeprefix("0x")
held = requests.get(f"{API}/session/get", params={"owner": owner.address}, timeout=10).json().get("session_key")
if held and held.lower() != agent.address.lower():
    n = int(time.time() * 1000)
    sig = owner_sign({"SessionRevoke": [{"name": "owner", "type": "address"}, {"name": "nonce", "type": "uint256"}]}, {"owner": owner.address, "nonce": n})
    requests.post(f"{API}/session/revoke", timeout=15, json={"owner": owner.address, "session_key": held, "nonce": n, "signature": sig})
    wait(lambda: not requests.get(f"{API}/session/get", params={"owner": owner.address}, timeout=10).json().get("session_key"))
exp, n = int(time.time()) + 1800, int(time.time() * 1000) + 1
sig = owner_sign({"SessionCreate": [{"name": "sessionKey", "type": "address"}, {"name": "expiresAt", "type": "uint256"}, {"name": "nonce", "type": "uint256"}]},
                 {"sessionKey": agent.address, "expiresAt": exp, "nonce": n})
requests.post(f"{API}/session/create", timeout=15, json={"owner": owner.address, "session_key": agent.address, "expires_at": exp, "nonce": n, "signature": sig})
print("agent session:", bool(wait(lambda: (requests.get(f"{API}/session/get", params={"owner": owner.address}, timeout=10).json().get("session_key") or "").lower() == agent.address.lower())))

# 4. Inside the policy: a small resting bid (about 20 USDC of ETH) is accepted
dex = Vordex()
mark = Decimal(str(dex.mark_price(1)))
pair = next(p for p in requests.get(f"{API}/markets/specs", timeout=10).json()["specs"]["pairs"] if p["pair"]["pair_id"] == 1)
tick = Decimal(pair["tick_8dec"]) / 10**8 or Decimal("0.000001")    # a price must sit on the market's tick
bid = float((mark * Decimal("0.95") / tick).to_integral_value(ROUND_FLOOR) * tick)
small = dex.place_order(agent.key.hex(), owner.address, pair_id=1, side=BUY, size=0.008,
                        price=bid, order_type=LIMIT, client_nonce=int(time.time() * 1000))
print("small order:", small.get("status_code"), small.get("status") or small.get("error"))
# 5. Outside it: 0.03 ETH is more than the 0.02 the market limit allows, so the chain refuses it and says why
big = dex.place_order(agent.key.hex(), owner.address, pair_id=1, side=BUY, size=0.03,
                      price=bid, order_type=LIMIT, client_nonce=int(time.time() * 1000) + 1)
print("large order:", big.get("status_code"), big.get("code"), big.get("reason"), "—", big.get("meaning"))

# 6. The owner revokes the agent; its key can sign nothing from then on. Its resting bid stays until the owner
#    cancels it (Cancel all, in the API reference).
print("revoke:", submit(sign_owner_op(owner.key.hex(), "RevokeAgent", {"owner": owner.address, "agent": agent.address, "nonce": now + 3})))
print("status:", wait(lambda: agent_record().get("status") if agent_record().get("status") != "active" else None))

What you should see

  • register: 202 and the agent's name read back from the chain.
  • The policy and the market limit stored.
  • The small order submitted.
  • The large order refused: 403 agent_refused max_order — The order is larger than the policy allows.
  • revoke: 202, then status: revoked.

What it shows

  • The account signs, the agent trades. Registration, policy, market limit and revocation are signed by your account key. The agent's own key only ever signs orders — and only as a session of your account.
  • Default deny. A policy that names no market lets the agent trade none: without step 2b the chain refuses every order with market_not_allowed.
  • Refusals say why. Every refusal carries a reason key and its meaning. The full list is in the refusal codes.
  • One session per account. Opening the agent's session ends any other session the account holds; the sample ends the old one first, as the app does.

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

On this page