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 requestsimport os, time, secrets, requestsfrom decimal import Decimal, ROUND_FLOORfrom eth_account import Accountfrom eth_account.messages import encode_typed_datafrom vordium import Vordex, domains, use_networkfrom vordium.dex import BUY, LIMITfrom vordium.agent_ops import registration, set_policy, sign_owner_opfrom vordium.policy import PolicyV1from vordium.rollb import read_signing_phaseuse_network("testnet")API = "https://testnet.vordex.xyz/chain"owner = Account.from_key(os.environ["PRIVATE_KEY"]) # the account the agent trades foragent = Account.from_key("0x" + secrets.token_hex(32)) # the agent's own key: it never holds fundsstatus = requests.get(f"{API}/status", timeout=10).json()phase = read_signing_phase() # the signing format the chain expects nownow = int(time.time() * 1000)def submit(body): r = requests.post(f"{API}/submit", json=body, timeout=15) return r.status_codedef 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 hourpolicy = 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.05limit = {"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) + 1sig = 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 accepteddex = 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 tickbid = 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 whybig = 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))
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