logos-layer
v0.3.0
Published
Logos Layer: an MCP server that lets AI agents trade inside a mandate-guarded VaultV2. The mandate is read from the chain, so any vault works; compliant orders execute, violations revert on-chain.
Readme
logos-layer
An MCP server that lets an AI agent trade inside a mandate-guarded VaultV2. The vault's on-chain guard enforces the mandate on every order: compliant orders execute, violations revert naming the exact rule they broke. The guard does not care how smart the agent is.
Nothing about the mandate is compiled in. The asset count, the symbols, the prices and all three rule lists are read from the vault at startup and decoded into the verified interpreter's AST, so this server works against any VaultV2 — however many assets it holds, whatever rules its mandate states.
Tools
| Tool | What it does |
| --- | --- |
| get_portfolio | Live book: per-asset symbol, position, price and value, plus cash, NAV, high-water mark, owner, mandate hash and active certificate id |
| get_mandate | The vault's actual rule list, decoded from the chain and rendered one line per rule, addressed the way a revert addresses it |
| check_order | Dry-run. Runs the golden-vector-pinned TypeScript port and the contract's own preview(), reports both, and flags any disagreement |
| get_admissible_window | The range of whole-share quantities the mandate admits for one asset, in closed form and against the gate the vault runs — so "none" means none exists, not none was found |
| execute_order | Sends the trade, refusing locally first if the checker rejects it |
Why check_order runs two checkers
The local checker is a line-by-line port of a Lean decision procedure proved correct by Aristotle; the contract runs an independent Solidity port of the same procedure. check_order runs both against live state on every call and says whether they agree. Agreement is the product. A disagreement would be a real finding, and the server says so rather than silently preferring one.
What get_admissible_window decides, and what it does not
The analyzer decides all three rule sorts — state, order and drawdown (high-water) — in closed form, against checkSR, the exact gate execute() and preview() route through. It is sound with no hypothesis at all (every notional in the window is admissible, and at a mark the reduction cannot use it declines rather than guesses) and complete given every mark strictly positive (if any order at that name is admissible, the window is non-empty and contains it). A negative answer is a theorem, not a failed search. Windows are reported in whole shares, because execute takes an int256 quantity and a range whose only interior points are fractional admits nothing you can actually send.
An earlier release projected the drawdown rules out, and against a vault in drawdown breach it returned a window of −17 … 17 shares whose both endpoints the vault refused with MandateViolation(3, 0). That is closed: the tool now reports the toward-zero band the gate actually admits, and names the breached rule as the reason the window is one-sided. test/window.test.ts replays those numbers.
Four things stay outside the model, and every response says so:
- Today's marks and today's high-water mark. No price path, no correlation model.
hwmUpdateruns only on an executed trade, so a repricing alone never moves the mark — but a move up followed by any executed trade ratchets it and changes every later answer. - One order at one name. Not a claim about sequences of orders, and not a claim about combinations across names.
- A non-positive mark is declined, not decided. "No answer" and "no admissible order" are different findings.
- Emptiness is not where a drawdown rule shows up. Under
checkSRthe null order survives any breach, so the book-wide yes/no is provably equal to the drawdown-blind one; a breach narrows the window and can never empty it. Read the endpoints, not the bit — which is exactly why the original defect was invisible to a feasibility check.
Every response still grounds itself by calling the contract's preview() at both endpoints of the window and at zero. That is what caught the original defect, and a completeness proof is a reason to trust the window more, not a reason to stop checking it.
Setup
Add to your MCP client config (Claude Desktop, Claude Code, or any MCP client):
{
"mcpServers": {
"logos-layer": {
"command": "npx",
"args": ["-y", "[email protected]"],
"env": {
"VAULT_ADDRESS": "<your-vaultv2-address>",
"RPC_URL": "https://rpc.testnet.chain.robinhood.com",
"PRIVATE_KEY_FILE": "/absolute/path/to/key"
}
}
}
}Requires Node 20+.
| Env | Required | Meaning |
| --- | --- | --- |
| VAULT_ADDRESS | yes | The deployed VaultV2 the agent trades inside |
| RPC_URL | yes | RPC endpoint for any EVM chain; the chain id is read from it |
| PRIVATE_KEY_FILE | no | Path to a file containing the agent wallet key; preferred over inline. Without any key the server runs in spectator mode |
| PRIVATE_KEY | no | The agent wallet key, inline |
| EXPLORER_URL | no | Explorer base URL for transaction links |
| SETTLEMENT_CONFIG | no | Path to the physical-settlement JSON (see Physical settlement); unset means ledger-only — no real token ever moves. Requires a key: settlement with no key is a startup error |
Spectator mode: no key, no funds, still real
Leave both key variables unset and the server starts read-only against any
vault. Four of the five tools work exactly as they do for the owner —
get_portfolio, get_mandate, check_order and get_admissible_window all
read the live chain — and execute_order answers with what WOULD have
happened instead of signing. Two env vars are the whole setup, so anyone can
point an agent at the reference vault and interrogate a live mandate without
touching a wallet. A key that IS set but unreadable is still a fatal error:
that is a broken configuration, not a request for spectator mode.
Versions before 0.3.0 do not include physical settlement at all: [email protected] ignores SETTLEMENT_CONFIG and is ledger-only, whatever the config says. It also requires a key even for reads; spectator mode is 0.3.0+.
At startup the server prints the vault's asset universe, its rule counts and its mandate hash. It also re-encodes the rules it decoded and checks that they reproduce the hash the vault stores — if they do, the mandate it is describing to the agent is the same one the certificate was issued for, byte for byte.
Version 1 vaults are not supported
A v1 Vault has a fixed five-asset ABI (int256[5] positions, mandate() returning three thresholds, execute(uint8,int256)) and no rule lists to read, so there is nothing here to decode. Point this server at one and it says so at startup rather than failing with a decode error. Use [email protected] for a v1 vault.
Physical settlement (opt-in)
By default this server is ledger-only: VaultV2.execute moves an internal book (cash, positions) and never touches an ERC20. Setting SETTLEMENT_CONFIG turns on physical settlement: every order the mandate ADMITS and the vault has RECORDED is then mirrored by a real swap on a Uniswap v3 pool — a ledger BUY swaps the cash token for exactly the ordered shares (exact-output), a ledger SELL swaps exactly those shares back into cash (exact-input).
Ordering is one-way: the mandate gate runs first, on-chain, and the swap is sent only after execute()'s receipt confirms the order was admitted. A refused order never reaches a pool. The converse is NOT guaranteed — a swap can fail after the ledger has moved — and every failure mode is reported next to the ledger result, never hidden, with the response distinguishing "no swap executed" from "swap broadcast, outcome unknown" from "swap succeeded".
The vault stays a paper ledger
Even with settlement on, the vault custodies nothing. Its cash (the deploy script's CASH, default 100000) is a synthetic number; the real tokens sit in the agent wallet, and nothing on chain enforces any relation between the two. The mandate's cash floor and caps bound the ledger, not the wallet. When a settlement failure is reported, the book and the wallet have diverged — reconcile before trading further.
SETTLEMENT_CONFIG schema
The env var is a path to a JSON file:
{
"router": "0xcaf681a66d020601342297493863e78c959e5cb2",
"quoter": "0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7",
"cash": { "token": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "decimals": 6, "symbol": "USDG" },
"slippageBps": 100,
"maxMarkDeviationBps": 300,
"assets": {
"TSLA": { "token": "0x322F0929c4625eD5bAd873c95208D54E1c003b2d", "decimals": 18, "fee": 3000 }
}
}| Field | Meaning |
| --- | --- |
| router | SwapRouter02 — the no-deadline-in-struct variant (selectors 04e45aaf/5023b4df) |
| quoter | QuoterV2, called via eth_call to quote before every swap |
| cash | The cash-leg ERC20: token, decimals, optional symbol |
| slippageBps | Optional, default 100 (1%), max 1000: how far the mined swap may do worse than the quote |
| maxMarkDeviationBps | Optional, default 300 (3%): the swap is refused outright when the pool's quote diverges further than this from the vault's own mark. Cannot be disabled |
| assets | Vault symbol (or bare index) → { token, decimals, fee }. Assets absent from the map settle as an explicit "(no settlement venue configured …; ledger-only trade)" |
Asset keys are resolved against the vault's own symbol list at startup, so a typo'd symbol is fatal at startup, not mid-trade. A malformed file is also fatal at startup.
Fractional units. A venue's decimals defines what one ledger share IS in raw token units, and setting it below the token's own decimals makes the vault trade fractions: "decimals": 17 on an 18-decimal token means one ledger share = 0.1 token. This is how a small book trades assets whose whole-share price exceeds its per-order throttle: a $200 vault cannot admit a ~$342 TSLA share at any throttle, but at decimals: 17 (and THROTTLE_BPS=2000 in the deploy) one ~$34 tenth-share clears cleanly. The vault's MARK and the keeper's pool-source decimals must describe the SAME unit, or the mark-deviation guard will refuse every swap — which is that guard doing its job. The whole fractional profile is exercised end to end by UNIT_DECIMALS=17 CASH=200 THROTTLE_BPS=2000 bash test/settlement-fork.sh.
Guards on every swap
- Mark-deviation guard. Slippage alone bounds only quote-to-execution movement — it says nothing about the quote itself. So the pool's quote is first checked against the vault's current mark and the swap is refused when it diverges more than
maxMarkDeviationBps: a thin or pre-manipulated pool cannot be paid an unbounded premium to the price the ledger books the trade at. - Slippage bound.
amountInMaximum/amountOutMinimumare set from the QuoterV2 quote ±slippageBps(capped at 10%), so the mined swap cannot do worse than the quote by more than the bound. - Exact-amount approvals, never unlimited. The router is approved for precisely what one swap may spend, and any residual allowance is reset to zero after the swap. The config file is therefore never a standing authorization to drain the wallet: a mistyped or hostile router address is bounded to one trade's worth.
- Receipt identity. Every transaction wait verifies the mined receipt is for the hash that was sent, so a same-nonce replacement can never be mistaken for success (of the ledger trade or of the swap).
Verified chain-4663 addresses
Settlement trusts the addresses in the config file, and anyone can deploy a token named "TSLA" — never resolve a token, router or quoter by symbol or from a search result. The genuine Robinhood Chain (chain id 4663) addresses, verified by on-chain probe:
| What | Address |
| --- | --- |
| TSLA (tokenised stock, 18 decimals) | 0x322F0929c4625eD5bAd873c95208D54E1c003b2d |
| USDG (cash token, 6 decimals) | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| TSLA/USDG 0.30% pool | 0xf4ACdAEEB7022862A763C9B1B885e11191c889E3 |
| SwapRouter02 (no-deadline structs) | 0xcaf681a66d020601342297493863e78c959e5cb2 |
| QuoterV2 | 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7 |
packages/mcp/test/settlement-fork.sh exercises exactly these addresses on a fork and is the reference for how they were probed.
Funding a settled trade
Settlement swaps come from the agent wallet — the same key that owns the vault. Before a settled BUY that wallet must already hold, on chain 4663:
- USDG: up to
quote × (1 + slippageBps/10000)for the order, i.e. roughlymark × shares(~$350 per TSLA share at a $342 mark). The server checks the balance and refuses the swap up front when it is short. - Gas ETH: for the ledger transaction, the exact-amount approval, the swap and the allowance reset.
A settled SELL needs the wallet to hold the actual asset tokens being sold (shares × 10^decimals), normally acquired by earlier settled buys. There is no faucet for real USDG — you must transfer it in from wherever you legitimately hold funds on Robinhood Chain, which makes the settlement key a key that holds real money. See Safety below.
Issuer powers, and what is not audited
The tokenised stocks on Robinhood Chain are issuer-controlled: they carry blocklist, pause and admin-burn powers. If the issuer blocklists the agent wallet or pauses the token, every settlement leg fails — or an already-settled position is stranded in the wallet — while the paper ledger keeps trading. Settlement failures caused this way are permanent until the issuer acts, not retryable errors. None of the contracts in this repo, and none of this settlement code, have been audited. Size any real funds accordingly.
Keeping marks honest
The mandate gate prices orders at the vault's marks; settlement pays the pool's price. The mark-deviation guard refuses to settle when the two diverge past the bound, which means stale marks stop settlement. Run the keeper (packages/keeper) with its pool source (--source pool:<file>) so the vault's marks track the same pool settlement trades against; the same formula supplies MARK when deploying contracts/script/DeployReal.s.sol.
Key handling
The key never leaves your machine. This server reads it locally, signs locally, and sends only signed transactions to the RPC; there is no backend and nothing is ever transmitted to anyone else. The AI agent can call the trading tools but can never read the key.
Prefer PRIVATE_KEY_FILE pointing at a chmod 600 file over an inline PRIVATE_KEY, so the key does not sit inside an MCP config that cloud backups and dotfile syncs pick up:
printf '%s' '0xYOUR_KEY' > ~/.logos-layer-key && chmod 600 ~/.logos-layer-key"env": {
"VAULT_ADDRESS": "<your-vaultv2-address>",
"RPC_URL": "https://rpc.testnet.chain.robinhood.com",
"PRIVATE_KEY_FILE": "/Users/you/.logos-layer-key"
}Note: use an absolute path; ~ is not expanded by all MCP clients.
Safety
Ledger-only mode (no SETTLEMENT_CONFIG): generate a FRESH burner wallet for the agent and fund it with faucet ETH only. The agent wallet must be the vault owner to trade, so deploy the vault with that same burner — get_portfolio warns if it is not. Never use a key that holds real funds; testnet instruments only.
Settlement mode necessarily breaks the faucet-only rule: settling real swaps requires the owner key to hold real USDG and gas ETH on mainnet chain 4663 (see Funding a settled trade). Use a dedicated wallet holding ONLY what settlement needs — never a general-purpose account — fund it just before trading and sweep it when done. The per-swap guards (exact-amount approvals, mark-deviation, slippage) bound what any single trade can lose, but the key still controls real money and the token issuers retain blocklist/pause/burn powers over the stock tokens. Unaudited software; this is a research demo, not investment advice.
The version in the config is pinned; review the changelog before moving to a newer release.
This package ships as a compiled artifact.
