Metadata-Version: 2.4
Name: orblivion
Version: 0.1.2
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.revoke(grant.session_id)
```

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 Orbio balance needs Sign in with Orbio: `hosted.ai_connect()` returns Orbio's consent
page, refused (`AI_AUTHORIZE_URL`) unless it is `https://www.orbio.so/oauth/authorize` with PKCE S256 asking for exactly
Orblivion's grant: its client id (pass `client_id=` to pin it; until Orbio registers the app, the one the server's AI
options name, which its answer must repeat), the server's own `/orbio-callback.html` as the redirect, and the scope
`openid inference`, each named once. Orblivion's redirect page finishes it. No answer carries an Orbio credential.
Sign in with Orbio isn't open on the public API yet.

## 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).
