Metadata-Version: 2.4
Name: orblivion
Version: 0.1.4
Summary: Trade on Robinhood Chain through Orblivion from an Orbio agent: every transaction is decoded and checked locally before the agent's key signs it.
License-Expression: MIT
Project-URL: Documentation, https://orblivion.com/docs/
Keywords: orblivion,orbio,robinhood-chain,trading,agents,uniswap,sdk
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: eth-account>=0.13.6
Requires-Dist: eth-abi>=5
Dynamic: license-file

# orblivion

Trade on Robinhood Chain (chain 4663) through Orblivion, the trading layer for Orbio agents, from Python. Orblivion
finds the route, builds the transaction and simulates it. Your agent's own key signs it, and Orblivion's relay sends it.

**Nothing the server returns is signed until this SDK has decoded and checked it locally**, and every mined transaction
is checked again against what was signed. The decoder and the checks are this package's own: the same rules, rule names
and error codes as the TypeScript SDK (`@orblivion/sdk`), proven against the same recorded answers.

Python 3.10 or later. Depends on `eth-account` and `eth-abi` only; HTTP uses the standard library.

## Install

```sh
pip install orblivion
```

## Quickstart

```python
import os
from eth_account import Account
from orblivion import OrblivionClient

client = OrblivionClient(account=Account.from_key(os.environ["AGENT_KEY"]),
                         max_notional_usd=25)    # the client refuses anything bigger, whatever the server says

client.sign_in()                                  # optional: the first call signs in for you
quote = client.quote(sell="ETH", buy="USDG", amount_usd=10)
plan = client.plan(sell="ETH", buy="USDG", amount_usd=10)                 # every check, nothing signed
dry = client.trade(sell="ETH", buy="USDG", amount_usd=10, dry_run=True)
sent = client.trade(sell="ETH", buy="USDG", amount_usd=10)                # checked, signed, sent, checked again
print(sent.tx_hash, sent.amount_out, sent.min_out, sent.fee)
```

The client talks to `https://api.orblivion.com` unless told otherwise: pass another server first, or leave it out
(`OrblivionClient(account=...)`) and set `ORBLIVION_API_URL`. Plain `http` is accepted only for `127.0.0.1`,
`localhost` and `::1`; anything else must be `https`.

`headers={...}` adds headers to every request to the Orblivion API (never to the RPC or the sequencer); their values
are never shown in an error or a repr.

## Signing in

`sign_in()` asks the server for an EIP-4361 (Sign-In with Ethereum) message, checks it names this server's host, chain
4663, this wallet and a short expiry, signs it, and receives an API key bound to that wallet, kept in memory only.
Pass `api_key=` to reuse one; the client checks it is bound to its wallet first.

## Quote, plan, trade

| Call | What it does |
|---|---|
| `quote(sell=, buy=, amount_usd= \| amount_in=, slippage_bps=None, shield=False)` | The server's best route and what it discloses. Read-only; the server's word. |
| `plan(sell=, buy=, amount_usd= \| amount_in=, ...)` | The trade's plan, checked exactly as `trade` checks it, with **nothing signed**. A sale of an ERC-20 token whose swap is built only around a Permit2 signature stops after the permit's checks (`complete=False`). |
| `trade(sell=, buy=, amount_usd= \| amount_in=, slippage_bps=None, approval="budget", dry_run=False, accept_risks=None, shield=False, policies=None)` | Prepare, check, sign, send through the relay, wait for the receipt, check what was mined. With `dry_run=True` it stops after signing and sends nothing. |
| `search_tokens(q, tier=None, kind=None, limit=20, shield=False)` | Search the token registry. Symbols are copied by impersonators: pick a token by its address. |
| `inspect_token(token, shield=False)` | One token as a trade would see it, checked against the SDK's own reads of the chain, refusing nothing. |
| `wallet.balances(tokens=None, who=None)` | ETH and token balances, read from the chain. |
| `approvals.status()`, `approvals.revoke()` | The wallet's Permit2 approvals; take them back. |

Core tokens are named by symbol (`ETH`, `WETH`, `USDG`, `cbBTC`, `ORBIO`, `NVDA`, `SPY`); every other token by its
contract address. Amounts are `int` in the token's smallest unit (`parse_token("1.5", "USDG")`), or `amount_usd`.

## Fast mode

For an agent that holds its own key and trades a major pair, `fast()` cuts the path from decision to relay to one call:

```python
fast = client.fast().start()          # keeps its Chainlink marks, nonce, base fee, allowances and fee tier warm
r = fast.trade("ETH", "USDG", amount_usd=10)                 # or client.fast_trade(...)
print(r.path, r.tx_hash, r.timings["decision_to_accepted_ms"])
```

The SDK builds the transaction itself on a pinned route (`FAST_ROUTES`: ETH, WETH, USDG, NVDA and SPY; never cbBTC,
which takes the normal path before anything is signed), with `min_out` from its own quote of that route (kept warm; the
tolerance at most the server's pair default), your fee tier in ETH, WETH or USDG by the fee currency rule, and the
output to your wallet; checks it with `verify_fast` (the server's own rules), signs it and sends it to
`POST /api/v1/live/fast`, which rebuilds the calldata and relays only an exact match.

- Anything stale, an ineligible pair (ORBIO, launchpad and long-tail tokens, a stock token after hours), a token you
  haven't approved to Permit2 yet, a pool further under the Chainlink marks than its fee and the pair's band (75 bps
  against USDG, 125 for a cross pair), or marks from your RPC more than 3% from the server's falls back to `trade()`
  before anything is signed (`r.path == "fallback"`, `r.fallback_reason`); `fallback=False` raises instead.
- One `trade()` never lets two transactions land. After a refusal the server answers before relaying, the SDK asks your
  own RPC about the signed transaction first: on chain, it is the result (`r.note`); its nonce taken by another
  transaction, the next one takes a fresh nonce; otherwise the retry or the fallback signs at the same nonce. A refusal
  that may follow a relay (`NONCE` without that proof, `IN_FLIGHT`, `SUBMIT_REJECTED`, `SEQUENCER_UNREACHABLE`, no
  answer) raises with the hash. `trade(nonce=n)` signs the normal path's first transaction at `n` (and refuses when the
  wallet's pending nonce isn't `n`).
- `tolerance_bps` tightens the minimum (it can't exceed the server's cap); `min_out` sets your own when it is higher.
  The slippage table is pinned in the SDK (`FAST_TOLERANCES`); the server's published one can only tighten it.
- `simulate=False` skips the server's simulation of the exact signed transaction (one round trip to the node); a
  transaction that would revert then costs its gas. `dry_run=True` has the server check and simulate it, and relay
  nothing.
- Run the agent in US East, near the chain's sequencer: from far away its round trip dominates every trade.

## Risks and Dossier Shield

Any token with a route trades; the server discloses what it found in `risks` (`[{code, severity, detail}]`).

- **Severe risks trade only when you accept them by name** in `accept_risks`: `HONEYPOT`, `TRANSFER_TAX`, `LOOKALIKE`,
  `AMOUNT_HOOK`, `HIGH_PRICE_IMPACT`, `UNSCREENED`, `IMPERSONATOR`. The SDK never accepts a risk for you, and accepting
  one loosens none of its price or minimum-output checks.
- **Dossier Shield** (`shield=True`): 10 bps on top of the trade's fee, Dossier's verdict on the token, and `policies`
  (`block_impersonators`, on by default, `verified_only`, `block_risks`).

```python
r = client.trade(sell="ETH", buy="0x…", amount_usd=5, accept_risks=["TRANSFER_TAX"])
s = client.trade(sell="ETH", buy="0x…", amount_usd=5, shield=True, policies={"verified_only": True})
s.dossier   # {"verdict": "verified", "url": "…"}
s.fee_bps, s.fee_class, s.fee_tier   # 60, "other", 50
```

## Fees

Orblivion's fee depends on the two tokens a trade swaps (fee schedule 2, pinned in `constants`):

- **Major trades**: both tokens are majors (`ETH`, `WETH`, `USDG`, `cbBTC`, `ORBIO`, or a Robinhood stock token). They
  pay the wallet's volume tier: 25 bps, or 15 or 10 after enough 30-day volume.
- **Every other trade** pays exactly 50 bps.
- **Dossier Shield** adds 10 bps to either.

The SDK works out the class itself, from its own pinned majors and, for a token named by address, its own read of the
chain (the stock tokens' beacon). It never takes the server's word for the class. A plan whose `fee_class` differs is
refused (`FEE_CLASS`). The plan's `fee_bps` must be one the class and Shield mode allow, `fee_tier` must be that fee
without Shield's 10, and the calldata must pay exactly `fee_bps` (`FEE_BPS`). The SDK can't know the wallet's volume, so
it accepts any tier up to the posted 25 for a major trade. `max_fee_bps=` on the client sets a hard cap on top (`None`, the
default, leaves the schedule to bound the fee). Every quote, plan and trade result carries `fee_bps`, `fee_class` and
`fee_tier`.

The default slippage covers the trade's posted fee (40 bps plus the fee, within the pair's tolerance plus the fee).

## Sessions and hosted agents

```python
owner = OrblivionClient(url, wallet_account)
executor = OrblivionClient(url, hot_account)
grant = owner.sessions.grant(executor=executor.address, max_trade_usd=20, max_day_usd=60, max_trades=10, hours=12,
                             allow_7702=True)
owner.sessions.setup(grant)   # the USDG setup, and on a plain wallet its code (one transaction from it to itself)
executor.sessions.trade(grant.session_id, sell="ETH", buy="USDG", amount_usd=5, session=grant)
owner.sessions.top_up(grant)  # later: the budget back to one day's cap, when the agent pauses for SESSION_BUDGET
record = grant.revocation_record()   # keep it (JSON, no secret): it pins top-ups, and is the off switch
owner.sessions.revoke(grant.session_id, record=record)
```

Each session trade spends the router's allowance of its input, and nothing refills it. `sessions.top_up(grant)` sets
each short token's allowances back to its cap: a reset, never an addition. A bot that restarts without its `Grant`
passes the session's revocation record instead, which it keeps next to its key (the starter bot writes it into
session.json):

```python
import json
from orblivion import load_revocation_record

saved = json.load(open("session.json"))
rec = load_revocation_record(saved["revocation_record"], wallet=owner.address)
owner.sessions.top_up(rec.session_id, record=rec)   # or record= the dict or its JSON text, or root_hash=
```

A top-up that sends needs one of these pins (`TOPUP_PIN`). The session id isn't signed by the wallet, so without a pin a
server could answer with another of the wallet's live sessions (a bigger cap, a later end) under this session's id. A
`dry_run=True` needs no pin; its `pinned` says whether it had one. The SDK decodes every step itself and holds it to
what the wallet signed, not to the server's numbers:

- The root must be the pinned one and carry the wallet's signature, and its salt must hash the binding shown, which
  must be the Grant's or the record's (`TOPUP_ROOT`, `TOPUP_BINDING`, `TOPUP_SESSION`).
- The root is read on every RPC the client has (`DelegationManager.disabledDelegations`). A session any RPC reads as
  revoked, or one no RPC can read, isn't topped up (`TOPUP_REVOKED`).
- Each step must be the token's `approve(Permit2, amount)` or `Permit2.approve(token, router, amount, expiration)` for
  a token the session trades through the router. Tokens it trades on their own Pons curve are left out, and so is
  every token when fewer than two go through the router. Each amount may be at most one day's cap (`topup_day_caps`:
  the per-trade cap x max_day_usd / max_trade_usd), and the expiry no later than the session's end (`TOPUP_*` rules).
  With the `Grant`, nothing may exceed what its setup approved.

A wallet running the DeleGator sends the steps as one transaction, a batch the SDK builds itself and compares byte for
byte with the server's (`TOPUP_BATCH`). After a send, `after` and `confirmed` show what the chain reads.

Top-ups are paced. A second top-up of a session within 24 hours of the last one this client sent is refused before
anything is asked or signed (`TOPUP_AGAIN`), unless you pass `again=True`. Each top-up restores at most one day's cap,
so the bound is one day's cap per confirmed top-up. Orblivion paces them too, from the top-ups it relayed or reads on
chain: its refusal is an `OrblivionApiError` with code `TOPUP_AGAIN` and the last top-up in `extra["last_top_up"]`.

The router's allowance is per wallet and token, not per session: one Permit2 allowance per wallet, token and router
serves every live session of that wallet, and holds the last cap a top-up set. When other live sessions share it,
Orblivion refuses with `SESSION_BUDGET_SHARED` (`extra["shared"]` lists them). Pass
`confirm_shared=e.extra["confirm_shared"]` to top up anyway.

`sessions.revoke(session_id, record=record)` first builds the on-chain steps itself from the record and the chain's
state (`disableDelegation` of each root, every approval the session's binding names back to 0, Permit2 locked down for
the router) and sends them straight to the sequencer, each on its own, sized, priced and numbered from every RPC rather
than the first that answers; then asks Orblivion to stop co-signing, within a deadline (`api_error` says if it
couldn't). It reads every root and approval back on each RPC (`on_chain["status"]`: `confirmed`, which needs two
different RPC hosts, or `unconfirmed`; `on_chain["from_record"]` is what the record alone says, unless Orblivion's
answer carries a live root this wallet signed, `on_chain["signed_live"]`, which `disable_signed=True` disables too), and
compares Orblivion's own steps with the record
(`api_steps`). `sessions.disable_onchain(record)` does the on-chain part with no call to Orblivion at all. Keep the record
where nobody else can edit it: the SDK can't tell a root or approval removed from it, or an older session's record.
Without a record, `revoke` sends Orblivion's steps (`server_built`), and a disable only of a root Orblivion's answer
names as the session's, signed by this wallet, whose only redeemer is the session's executor (`REVOKE_ROOT`); its outcome
is at best `server_reported`, Orblivion's word.

The owner's signed root delegation carries caveats the chain enforces (the router only, ETH per trade and per day, the
expiry, the number of trades, the executor as the only redeemer), and its salt binds the limits, the accepted risks,
Shield and the fees, so neither the server nor the executor can widen them. Every new grant is binding version 4, with
`fees` exactly the schedule's for its Shield mode (`verify.fee_terms`); the owner's SDK refuses any other version or
fees (`GRANT_LIMITS`). A session trade pays what the binding allows for the trade's own class, which the executor's SDK
and Orblivion's co-signer each read themselves. Sessions granted before version 4 keep their one fee. An executor trading without
the `Grant` can state the session's Shield mode with `session_shield=`. Otherwise its SDK holds the trade to the
schedule without Shield. A plain wallet's code is set to MetaMask's EIP7702StatelessDeleGator only with
`allow_7702=True`: the grant signs an authorization the first trade can carry, but only while the wallet sends nothing
before it, so `sessions.setup` sets the code with one transaction from the wallet to itself, and `wallet.delegate_code()`
does that on its own (`wallet.reset_code()` undoes it). `client.hosted` runs an agent's loop on the Orblivion server; a live hosted agent's executor
must carry the co-signer's attestation, which the SDK verifies before the owner signs anything.

An AI agent (`brain="orbio_llm"`) decides each tick with a model on Orbio's gateway from the owner's own strategy prompt:
`client.hosted.create(brain="orbio_llm", params=ai_params("Follow the trend; keep half in USDG.",
"anthropic/claude-haiku-5.5"), interval_s=900, pairs=["ETH-USDG"], limits=...)`: Orblivion's own Orbio credits pay
first (a fair daily share per owner wallet, cheap models, 15-minute ticks), then the owner's own Orbio balance.
`hosted.ai_options()` lists the templates, models and prices, the pool and this wallet's share; `hosted.spend(id)` its
model calls and cost.

The owner's own Orbio balance needs Sign in with Orbio, once per wallet: every agent of that wallet with funding
`"own"` (or `fallback_own`) uses the connection. Connect on the agents page (https://orblivion.com/hosted.html), or
from a script:

```python
start = client.hosted.ai_connect()       # a claim link, checked: open start["claim_url"] in a browser
own = client.hosted.ai_wait_connected()   # returns funding.own once a new sign-in has finished (10 minutes by default)
```

The SDK pins Orblivion's app id at Orbio (`ORBIO_CLIENT_ID` in `constants.py`, `orbio_app_c90TaP2MdpS5yOeykiaPIw`) and holds
every sign-in to it: a server whose AI options or answer name another app is refused (`VerificationError`
`AI_AUTHORIZE_URL`) before anything is asked of it. The server's own word can't stand in for the pin, since a look-alike
app on Orbio's genuine consent page would pass. Pass `client_id=` only to sign in through another app, such as a
loopback development server's (127.0.0.1, localhost, ::1).

Open `claim_url` in a browser tab signed in to orblivion.com with this same wallet. The page asks you to continue, then
opens Orbio's page; approve Orblivion's app there. It asks for model calls only, and Orbio shows Orblivion's app fee.
Orbio sends the tab back to Orblivion's redirect page, which finishes the sign-in. The page claims the sign-in for its
own API key, so it works only for the same wallet; a script can't finish a sign-in itself. `ai_wait_connected` returns
only for a sign-in newer than the connection the wallet had when `ai_connect` started (or pass `baseline=`), so an older
connection doesn't count. With `handoff=False`, `ai_connect` returns Orbio's address (`authorize_url`) instead, which
only the script's own key may finish, through `hosted.ai_connect_finish(code, state, iss)`.

The claim link must be exactly `<origin of the redirect page>/hosted.html#orbio=<claim>.<signature>`, and Orbio's
address (without handoff) exactly `https://www.orbio.so/oauth/authorize` carrying `response_type=code`, Orblivion's
client id (the pinned one: `client_id=`, else `ORBIO_CLIENT_ID`; with neither, a loopback server's own, which its AI
options and its answer must both name), the redirect page, the scope `openid inference`, a state and a nonce of 32-128
base64url characters, a PKCE S256 challenge and `prompt=consent`, each once and nothing else (`AI_AUTHORIZE_URL`). The
redirect page is the one the server states (its AI options and its answer must agree), and it must be exactly the
server's own `/orbio-callback.html` or, for an `api.` host, the site's beside it: `https://api.orblivion.com` comes back
to `https://orblivion.com/orbio-callback.html`. Every answer carries the co-signer's attestation of the sign-in: its
signature over this wallet, the sign-in's state and claim, that redirect page, the client id, the PKCE challenge, the
nonce and an expiry at most 15 minutes away, which must recover to the pinned co-signer (`SIGNIN_ATTESTATION`,
`verify_signin_attestation`). The SDK rebuilds the signed text from its own values: the client id it trusts and, without
handoff, the state, challenge and nonce of Orbio's address itself, so a server can't swap in another app or rewrite the
challenge. With handoff the SDK sees no address and takes the challenge and nonce from the attestation (checked for
form); the browser tab that claims the sign-in checks them against the address it opens. The claim link carries the
same signature. A host that doesn't offer Sign in with Orbio answers 503 `CONNECT_OFF`. No answer carries an Orbio
credential.

## Your account

```python
page = client.me.trades(limit=25)              # newest first; refused=True adds refused calls and unmined trades
for t in page.trades:
    print(t.time, t.path, t.token_in.token, t.amount_in, t.token_out.token, t.amount_out, t.amount_out_kind, t.status)
older = client.me.trades(limit=25, before=page.next_before) if page.next_before else None

p = client.me.portfolio()                      # balances at independent marks; p.total_usd counts priced ones only
print(p.total_usd, p.fee_tier.tier_bps, p.fee_tier.volume_to_next_usd)
s = client.me.sessions()                       # active sessions and hosted agents in summary
```

The signed-in wallet's own trades through Orblivion (agent, session, hosted or fast; `amount_out` is what the wallet
received once mined, `amount_out_kind` `"actual"`, else the plan's minimum, `"minimum"`), its balances read from the
chain and valued at the independent marks Orblivion's volume tier uses (a token without one is listed with its amount,
`priced=False`), its fee tier and the volume to the next, and its active sessions and hosted agents. Read-only: these
calls sign nothing and change nothing. The server answers for the key's own wallet only, and the SDK still checks every
answer: its shape (`ACCOUNT_SHAPE`) and that it is this client's wallet's (`ACCOUNT_WALLET`). Information only: nothing
in these answers feeds a signing decision, which always rests on the SDK's own reads. No fee figure is returned.
Token symbols and names are their deployer's word: show them as text.

## What the SDK verifies

The same rules as the TypeScript SDK, by the same names: the router, chain, sender and transaction type; every Universal
Router command decoded and re-encoded byte for byte; the value and what is pulled; the tokens and hops; every recipient
(output only to the wallet, one fee payment only); the fee (its class, count, size, token, recipient); the minimum output against
the quote, the simulation and Chainlink prices the SDK reads itself, or its own quote of the exact route for tokens
without a feed; the size, the deadline and the gas; Permit2 permits and approvals; token records against the chain
(lookalikes of core assets refused unless accepted); and, after mining, that what was mined is what was signed.

## Pinned values

The chain, the contracts, the core tokens, the fee schedule and the session co-signer (`constants.COSIGNER`) are pinned;
the server's config must agree (`ConfigMismatchError`). Its `fee` must publish schedule 2 exactly: `schedule`,
`major_symbols`, `major_tiers`, `other_bps`, `shield_add_bps` and `max_bps`, and none of schedule 1's `bps`,
`shield_bps` or `default_bps`. The fee recipient is pinned the same way: Orblivion's platform wallet
(`constants.FEE_RECIPIENT`, `0x46eb64c06379ac0d8ea3db040974278199e1e6e9`). The $ORBLIVION token's contract is pinned too
(`constants.ORBLIVION_TOKEN`).

## Errors

`VerificationError` (`VERIFICATION_FAILED`, with `rule`, `failures`, `checks`: nothing was signed),
`OrblivionApiError` (the server's code), `ConfigMismatchError`, `MinedMismatchError`, `TxRevertedError`,
`ConsentRequiredError`, `InsufficientFundsError`, and `OrblivionError` with `ACCOUNT_SHAPE` or `ACCOUNT_WALLET` for an
account answer (`client.me`) of the wrong shape or for another wallet. `error.retryable` is true for rate limits,
unreachable nodes, receipt timeouts and 5xx answers. Every one is an `OrblivionError`; an API that can't be reached
is `ORBLIVION_UNREACHABLE`.

## License

MIT. See [LICENSE](LICENSE).
