Skip to main content

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:

  1. Detects the protocol (proposed_tx.protocol if provided, otherwise heuristics).
  2. Extracts structured fields from the intent using a protocol-specific schema.
  3. Runs HARD validation nodes (deterministic checks).
  4. Runs SEMANTIC validation nodes (LLM-based reasoning where applicable).
  5. 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.
  • 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.protocol is 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_intent clearly states the action, asset, and amount.
  • proposed_tx reflects the same operation.
  • proposed_tx.protocol is set whenever possible.
  • Optional metadata/context is 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 metadata for 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 metadata and 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 HARD when you can implement a deterministic check.
  • Use SEMANTIC when the rule needs LLM reasoning.
  • Keep node IDs consistent with the prompter method names.
  • Review PROFILE_OVERRIDES to 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, or SKIP with confidence and details.
  • Use utilities in src/arbiter_core/nodes/universal.py for 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_prompter and _detect_protocol).
  • If you rely on auto-detection, update _detect_protocol. Otherwise, require explicit proposed_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 metadata in the /validate request.
  • 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.