Extending the Arbiter
This guide covers two topics: how to write effective user intents for validation, and how to add custom validation logic.
Writing Effective Intents
What Is an Intent
An intent is free-form natural language that states what the user wants the transaction to do.
Examples:
- “Swap 500 USDC for ETH with max slippage 0.5%.”
- “Repay my USDC borrow on Compound, gas under 8 gwei.”
- “Approve Permit2 to spend 1000 USDC.”
How the Arbiter Processes an Intent
At validation time, the Arbiter:
- Detects the protocol (
proposed_tx.protocolif provided, otherwise heuristics). - Extracts structured fields from the intent using a protocol-specific schema.
- Runs HARD validation nodes (deterministic checks).
- Runs SEMANTIC validation nodes (LLM-based reasoning where applicable).
- Produces a final decision (PASS or REJECT) with node-level audit details.
Authoring High-Quality Intents
Include the following details whenever possible:
- Action: transfer, swap, borrow, repay, withdraw, approve/permit, etc.
- Asset/token: explicit symbol (e.g., USDC, WETH, ETH).
- Amount: explicit amount and unit (token units or USD-denominated value).
- Execution constraints: slippage, deadline/timing, gas preference, gas limit.
- Counterparty/target when relevant: recipient address or label, spender label, protocol name.
Recommended Phrasing Patterns
- Transfer: “Send 0.1 ETH to
<address>.” or “Transfer 100 USDC to<address>.” - Uniswap swap: “Buy ETH with 500 USDC, max slippage 0.5%, execute within 30 minutes.”
- CoW order: “Sell 1000 USDC for ETH, no partial fills, max slippage 1%, by 5pm.”
- Compound: “Repay 250 USDC borrow on Compound, gas under 8 gwei.”
- Precondition/approval: “Approve Permit2 to spend 1000 USDC.”
Ambiguous vs Clear Intents
Less reliable:
- “Do the swap quickly.”
- “Handle my position.”
Better:
- “Swap 500 USDC for ETH on Uniswap with max slippage 0.5% within 20 minutes.”
- “Repay 200 USDC borrow on Compound, gas under 10 gwei, no other operation.”
Parsing Behavior and Limitations
- Extraction is protocol-specific and uses strict JSON outputs internally.
- If a field is unclear, extractors typically leave it empty or null instead of guessing.
- Protocol auto-detection can fail on ambiguous transactions. Setting
proposed_tx.protocolis strongly recommended. - Intent-only parsing does not fetch wallet history, balances, allowance history, or prior transactions.
- Validation is request-scoped and mostly stateless (except context you pass via
metadata).
Known Inference Rules
- Compound extraction maps many synonyms to canonical actions (e.g., “pay off” becomes repay).
- Some USD-only Compound intents may default asset inference to USDC in specific action contexts (repay/withdraw).
- Uniswap extraction normalizes slippage to a fraction (0.5% becomes 0.005).
- Deadline text like “in 30 minutes” is converted to seconds when recognized.
Request Checklist
Before calling POST /validate, confirm that:
human_intentclearly states the action, asset, and amount.proposed_txreflects the same operation.proposed_tx.protocolis set whenever possible.- Optional
metadata/contextis included if your policy needs extra facts.
Adding Custom Validation Logic
The Arbiter is protocol-driven. Rules live in protocol-specific prompters and a shared graph-of-operations (GoO) plan.
What It Does Today
- Validates a single proposed transaction against a user intent.
- Runs a protocol-specific validation plan (nodes) driven by the detected or explicit protocol.
- Uses a mix of deterministic checks (HARD) and LLM-based checks (SEMANTIC).
- Supports protocols:
compound,uniswap,cow,transfer,precondition. - Accepts optional
metadatafor extra context, but does not fetch or maintain wallet history itself.
What It Does Not Do Yet
- No native support for wallet transaction history or balance-aware validation.
- No domain-level rule DSL or rule engine separate from protocol code.
- No persistent state across validations unless you pass it in
metadataand handle it in your own nodes.
Where Custom Rules Live
Custom rules are implemented as validation nodes wired into the GoO plan:
- Node catalog and dependencies:
src/arbiter_core/arbiter/goo.py - Validation orchestration:
src/arbiter_core/arbiter/engine.py - Protocol-specific implementations:
src/arbiter_core/nodes/*.py - Protocol detection and prompter selection:
src/arbiter_core/protocols/tool.py
Examples of Existing HARD Rules
These deterministic checks already exist in the codebase:
- Fee limits and gas caps: if the intent specifies a max gas price or a total fee cap, validation fails when the transaction exceeds it.
- Gas reasonableness: rejects obviously incorrect gas limits for certain protocols when a gas limit is provided.
- Amount alignment: compares intent amount vs on-chain amounts within tolerance, with stablecoin cent-level handling.
- Token alignment: checks that the token symbol or address in the transaction matches the intent.
- Deadline/TTL consistency: enforces that a transaction’s deadline aligns with the user’s timing preference or extracted TTL.
- Slippage guard (protocol-specific): for swaps, verifies a guard is present and that implied slippage does not exceed the user’s cap.
- Precondition checks: validates approval/permit transactions against selector and token whitelist constraints.
Note: balance sufficiency is not a global core guarantee. If you need strict balance policy across your domain, pass balances in metadata/context and implement protocol-specific HARD nodes for it.
How to Add a Domain-Specific Rule
Use this flow to add a new rule for an existing protocol (or a new one).
1. Define a node in the GoO catalog
Add a NodeSpec with a unique node_id, a type (HARD or SEMANTIC), dependencies, and protocol list in src/arbiter_core/arbiter/goo.py.
Guidance:
- Use
HARDwhen you can implement a deterministic check. - Use
SEMANTICwhen the rule needs LLM reasoning. - Keep node IDs consistent with the prompter method names.
- Review
PROFILE_OVERRIDESto disable or add dependencies per protocol.
2. Implement the node in the protocol prompter
In the relevant prompter (src/arbiter_core/nodes/<protocol>.py), add a check_* method and route it in run_hard_node or run_semantic_node.
Guidance:
- Return
PASS,FAIL, orSKIPwith confidence and details. - Use utilities in
src/arbiter_core/nodes/universal.pyfor shared checks (tokens, fee/deadline/slippage logic, sanctions hooks).
3. Wire the protocol
- If the protocol already exists, update only its prompter.
- If it is new, add a new prompter class and register it in
src/arbiter_core/protocols/tool.py(_get_prompterand_detect_protocol). - If you rely on auto-detection, update
_detect_protocol. Otherwise, require explicitproposed_tx.protocol.
4. Test via the API
Use POST /validate with representative human_intent and proposed_tx fixtures for both positive and negative outcomes.
5. Confirm stop behavior
Early-stop behavior is policy-driven when policy files are active. Verify your new rule’s priority and stop semantics in policy/ if your deployment uses custom policy configuration.
Custom Logic vs Policy Configuration
The policy system in policy/ controls when validation stops after failures. Node definitions in GoO/prompters control what gets validated.
Transaction History and Wallet Context
Arbiter-Core validates a single proposed transaction. If you need history-aware checks (e.g., “block repeated transfers” or “match past approvals”):
- Pass external context via
metadatain the/validaterequest. - Implement nodes that read that metadata and enforce the rule.
This keeps the core stateless while allowing domain-specific logic when you provide the data.