Docs
API https://api.orblivion.com Home

Sessions

A session lets an agent trade without its owner's main key online. The owner signs once: the limits, the tokens, the risks the agent may accept, and an expiry. After that a separate executor key trades inside those limits, and each trade also needs Orblivion's co-signature for exactly that one call.

How a session works#

Three keys take part, and no single one of them can move funds:

KeyWho holds itWhat it does
The main keythe wallet's ownersigns the session once (a delegation, and on a plain wallet an EIP-7702 authorization), and can revoke it on chain at any time
The executorthe agent: its own hot key, or for a hosted agent one Orblivion's signing service holds for itsends each trade
The co-signerOrblivion's separate signing serviceco-signs each trade after its own checks, for exactly one call

It uses MetaMask's audited Delegation Framework v1.3.0, deployed on Robinhood Chain:

  1. The root. The owner's wallet delegates to the co-signer, with caveats that the chain's DelegationManager enforces on every trade (below). Its salt is the hash of every limit the owner chose, so the signature binds them.
  2. The leaf. For each trade, the co-signer delegates onward to the executor, allowing exactly one call (this router call, this value, this calldata), once, for at most two minutes.
  3. The trade. The executor sends redeemDelegations([leaf, root]) to the DelegationManager, which runs every caveat and then has the wallet call the Universal Router.

The wallet must run MetaMask's EIP7702StatelessDeleGator v1.3.0 for this. A plain wallet (an EOA) gets it through an EIP-7702 authorization the owner signs at the grant. The key stays the owner: the wallet keeps working as before, and one authorization to address 0 resets it.

What the owner signs#

The root's caveats#

CaveatEnforces, on chain
AllowedTargetsthe Universal Router is the only contract the session can call
AllowedMethodsonly execute(bytes,bytes[],uint256)
ValueLteat most this much ETH per trade (the per-trade cap, in wei at the grant's ETH price)
NativeTokenPeriodTransferat most this much ETH per 24-hour window (sessions that trade ETH)
Timestampnothing after the expiry
LimitedCallsat most this many trades
Redeemeronly the executor can redeem

The binding#

The root's salt is the hash of the session's limits, its binding. Every new grant is version 4, which binds:

  • the wallet, the executor and the expiry;
  • the tokens, and each token's per-trade cap in its smallest unit;
  • the dollar caps per trade and per day, and the number of trades;
  • the risk terms (below): the severe risks the session may trade with, and Dossier Shield and its policies;
  • the fees that go with its Shield mode (fees, see Fees);
  • a random nonce.

The SDKs rebuild the binding from what the owner asked for and refuse a grant whose binding, caps or caveats differ (GRANT_BINDING, GRANT_LIMITS, GRANT_CAPS, CAVEAT_*), before anything is signed. They price each token's cap themselves and refuse one more than 2% (plus the token's own fees) away from their own price. They also refuse a grant that names any co-signer other than the one they pin.

The authorization, on a plain wallet#

A plain wallet needs an EIP-7702 authorization to EIP7702StatelessDeleGator v1.3.0. This changes the wallet's code, which is the owner's decision: the SDKs sign it only with allow7702: true (TypeScript) or allow_7702=True (Python), and otherwise refuse with CONSENT_REQUIRED. The executor's first trade carries it as a type-4 transaction, so the main key sends nothing. The authorization is tied to the wallet's nonce: any transaction from the wallet after the grant makes it stale, and the grant has to run again.

The setup, for token inputs#

ERC-20 tokens leave the wallet only through Permit2, so a session that sells a token needs a one-time setup the main key sends: approve(Permit2, budget) and Permit2.approve(token, router, budget, expiry), each exactly the session's budget. That budget caps every ERC-20 the session can spend, in total, until it expires. ETH needs no setup.

Grant a session#

import { createOrblivionClient } from '@orblivion/sdk'
import { privateKeyToAccount } from 'viem/accounts'

const owner = createOrblivionClient({ account: privateKeyToAccount(OWNER_KEY) })      // the main key
const executor = createOrblivionClient({ account: privateKeyToAccount(AGENT_KEY) })   // the agent's hot key

const grant = await owner.sessions.grant({
  executor: executor.address,
  maxTradeUsd: 20, maxDayUsd: 60, maxTrades: 50, hours: 24,
  allow7702: true,                 // the owner's consent to set the wallet's code
})
await owner.sessions.setup(grant)  // once, only if the session sells a token such as USDG

const t = await executor.sessions.trade({ sessionId: grant.sessionId, session: grant, sell: 'ETH', buy: 'USDG', amountUsd: 5 })
console.log(t)

Over the API, a grant takes two calls to POST /api/v1/live/session/grant.

1. Start it, with the main key's API key:

FieldDefaultMeaning
walletrequiredthe owner's wallet (the key's)
executorrequiredthe executor's address; not the wallet, not the co-signer
tokens["ETH", "USDG"]the tokens the session may trade: core symbols or addresses, at least two
max_trade_usd25the most one trade may spend; at most $1,000
max_day_usd100the most per 24 hours; from max_trade_usd to 100 times it
max_trades50from 1 to 10,000
expires_in_s86400from 60 seconds to 30 days; at most 24 hours when a token has no Chainlink feed (ORBIO, and any token by address whose price reference isn't one)
setup_budget_usdmax_day_usdthe ERC-20 budget for the whole session
accept_risks, shield, policiesnone, false, offthe session's risk terms

The answer is a pending session and everything the main key signs: sign.delegation (the EIP-712 typed_data, its digest, the delegation_hash and the binding), sign.authorization for a plain wallet, sign.curve_roots for curve tokens, the setup steps, caveats with what each means, the limits, risk_terms, on_chain_bounds, exposure (approvals the wallet has outside the session, which it should revoke) and not_enforced_on_chain.

2. Switch it on: the same route with session_id, delegation_signature, and authorization_signature (plain wallets) and curve_root_signatures ({delegation_hash: signature}, curve tokens). Orblivion recovers each signature, the co-signer registers the root after checking it against the binding itself, and the session is active.

Trade in a session#

POST /api/v1/live/session/trade with the executor's (or the wallet's) API key:

JSON
{"session_id": "ses_...", "token_in": "ETH", "token_out": "USDG", "notional_usd": 5}

It also takes amount_in, slippage_bps, min_amount_out, route_id and fund. It doesn't take a fee or risk terms: those are the binding's, and a request that names other accept_risks, shield or policies is refused (SESSION_TERMS).

Orblivion checks the session (each a checks entry, and a refusal with that code when it fails):

CheckRefuses when
SESSION_PENDING, SESSION_REVOKED, SESSION_EXPIRED, SESSION_EXPIRYthe session isn't switched on yet, was revoked, or has ended or is about to (409)
SESSION_BOUNDthe stored binding doesn't hash to the root's salt (409)
SESSION_TOKENSa token outside the session
SESSION_TRADES_LEFTthe session's trades are used up
SESSION_MAX_TRADE, SESSION_MAX_DAYover the per-trade cap, or the 24-hour cap counting every co-signed trade, sent or not
SESSION_TARGET, SESSION_RECIPIENTSthe swap isn't a router execute, or pays anyone but the wallet, the router or the fee recipient
SESSION_SETUPa token input without the setup in place

Then the co-signer checks the trade again on its own and co-signs the leaf. The answer carries the executor's transaction (tx: redeemDelegations to the DelegationManager, type 4 on the first trade of a plain wallet), the leaf with its caveats, the decoded execution and swap, its simulation, spend (last_24h_usd, remaining_24h_usd, trades_used, trades_left) and the disclosures. ready is true when it can be sent as it is. The executor signs it and sends it through POST /api/v1/live/submit.

A token still on its Pons curve trades in a session too, through more roots the owner signs at the grant: a curve root, and one fee root for each token its trades pay the fee in. Each trade is one redeemDelegations of two redemptions, which land together or not at all. The fee is paid in the pair (ORBIO or ETH, which outrank the token; see which token pays the fee), so an ORBIO-paired token needs one fee root, in ORBIO: a buy redeems the fee and then the curve call, and a sale the curve call and then the fee, exactly floor(min_out × bps / 10,000) of the sale's guaranteed minimum. The SDKs and the co-signer refuse any other set of roots (CURVE_ROOT_SET).

What the co-signer enforces#

The co-signer is a separate service from the API, with its own key, its own price readings and its own records. It takes structured requests only, never a raw digest, and before each leaf it checks the trade with the Python SDK's own decoder and rules, not the API's code:

  • the call is the pinned Universal Router's execute, and every recipient is the wallet or the router, except exactly one fee payment to the fee recipient, of a fee the binding allows for the leaf's class, which the co-signer reads itself from the leaf's two tokens;
  • that fee is in the token and on the side the fee currency rule gives, and in its place: from the input right after the router receives it and before any swap, or from the output after the last swap (FEE_TOKEN, FEE_SIDE, FEE_ORDER);
  • nothing is left in the router, the tokens and hooks are allowed, and the minimum output is above zero;
  • the minimum output is at most max(slippage, 300 bps) under the input's value at its own Chainlink reading, or, for a token without a feed, at most 1,000 bps (plus the curve's fee and creator tax) under the price the owner signed. With no fresh reading it refuses (PRICE_UNAVAILABLE);
  • the per-trade, per-24-hour and trade-count caps, which it counts itself by root;
  • the risks reported for the trade are all accepted by the binding, and no Shield policy in the binding refuses it.

Anything else is refused and logged. Its operator can freeze it outright, and then it signs nothing.

Risk terms#

An agent can't accept risks on its owner's behalf. In a session, accept_risks, shield and policies are set at the grant and bound into the root's salt:

JSON
{"wallet": "0x...", "executor": "0x...", "max_trade_usd": 20, "max_trades": 10,
 "accept_risks": ["HIGH_PRICE_IMPACT"], "shield": true, "policies": {"verified_only": true}}
  • Every trade takes its terms from the binding, never from the request. Neither a prompt-injected agent nor a stolen executor key can widen them.
  • The fees follow the binding and its Shield mode (below).
  • Sessions granted before binding version 3 keep working with the strictest reading of what was signed: no risk accepted, no Shield, and schedule 1's 10 bps.

Fees#

A version 4 binding names the fees the session may pay, resolved for its Shield mode:

JSON
"fees": {"major_max_bps": 25, "major_min_bps": 10, "other_bps": 50}

With Shield they are {"major_max_bps": 35, "major_min_bps": 20, "other_bps": 60}. Any other value is refused.

  • A trade is major when both of its tokens are majors, and other otherwise (see Fees and gas). The co-signer reads each leaf's class itself, from the leaf's two tokens and its own on-chain stock check, and ignores the server's.
  • A major leaf pays one of the pinned tiers between major_min_bps and major_max_bps: the wallet's volume tier, from 0.25% down to 0.10% (0.35% down to 0.20% with Shield).
  • An other leaf pays exactly other_bps.
  • A curve token is always other, so a curve session's fee root caps its fee transfers at other_bps.
  • The SDKs, the relay and the co-signer each check the fee against the binding.

Sessions granted before schedule 2 (binding versions 1 to 3) keep schedule 1's one fee on every leaf until they expire: 10 bps for versions 1 and 2, and for version 3 its fee_bps, 10 or 15 with Shield.

Limits#

LimitValueEnforced by
ETH per tradethe per-trade cap at the grant's ETH pricethe chain (ValueLte)
ETH per 24-hour windowthe day cap, in fixed windows from the grantthe chain (NativeTokenPeriodTransfer)
ERC-20sthe setup budget, for the whole sessionthe chain (Permit2)
Number of trades, expiry, target, method, redeemeras signedthe chain
Dollars per trade and per 24 hoursas signedOrblivion and the co-signer, each counting
Recipient and minimum outputthe co-signed leaf's exact callthe chain, through the leaf
Priceat most max(slippage, 300 bps) under its own referencethe co-signer
A leafone exact call, at most two minutes, oncethe chain
Session length30 days; 24 hours with a token without a Chainlink feedOrblivion and the co-signer

The grant's not_enforced_on_chain lists what the chain alone doesn't bind: the output's recipient and the minimum output (the co-signed leaf binds them), the dollar cap per day for anything but ETH, the ERC-20 amount per trade, and which tokens are bought.

Revoking#

POST /api/v1/live/session/revoke with session_id (the wallet's key):

  • At once, at Orblivion: the co-signer co-signs nothing more for the session. A leaf already issued still works, once, until its two minutes run out.
  • On chain, the answer's on_chain steps for the main key to send: disableDelegation(root), which makes the DelegationManager refuse every later redemption, and the setup's approvals (and any curve allowances) back to 0. With reset_code: true it also describes the authorization that resets the wallet's code.

Send the on-chain steps whenever you suspect a key. They work whatever happens at Orblivion, even if it is down.

await owner.sessions.revoke({ sessionId: grant.sessionId })   // checked, signed and sent with the main key
await owner.wallet.resetCode()                                 // optional: back to a plain wallet

If a key is stolen#

StolenWhat the thief can move
The executor keynothing beyond trades the co-signer co-signs: each leaf is one exact call, once, for two minutes, paying the wallet. The executor's own ETH (its gas money) is exposed.
The co-signer's keynothing: only the executor can redeem.
Bothper 24-hour window, at most min(ETH per trade × trades left, the day cap) of ETH, plus the ERC-20 budget for the whole session, to any recipient and at any price, until the expiry. The windows are fixed from the grant, so up to twice the day cap can move across a window boundary. A curve session adds trades on that token's own curve, always paying the wallet but at any price, up to its curve approval.
The main keyeverything in the wallet. A session doesn't change that.

Plainly: no deployed contract can check the recipient or the minimum output of a Universal Router swap. The co-signer's leaf binds them, so the executor key alone gets nothing. With both keys stolen, the root's caveats are the bound. So keep sessions short and caps small, keep the executor's gas money small, and revoke on any suspicion.

Costs#

A session trade costs more gas than a direct one: about 557k gas against 207k, roughly two cents at today's prices, because the DelegationManager checks every caveat. The first trade from a plain wallet adds a little for the authorization. The executor pays the gas.