From ba498be93588949db2d0a1fe1c4b1fafae31ca43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=92=E5=A5=80?= Date: Thu, 5 Mar 2026 19:39:12 +0800 Subject: [PATCH] feat: add escrow:arbitrate CLI command for manual arbitration Add nla escrow:arbitrate with support for a specific escrow UID or "all" to scan for unarbitrated fulfillments demanding the user as oracle. Supports interactive mode (approve/reject/skip) and --auto mode using the LLM specified in the escrow's demand. Update nla-arbitrate skill to reference the new CLI command instead of raw SDK scripts. Co-Authored-By: Claude Opus 4.6 --- README.md | 1 + cli/client/arbitrate-escrow.ts | 381 +++++++++++++++++++++++++++++++++ cli/index.ts | 15 ++ skills/nla-arbitrate/SKILL.md | 168 +++++---------- 4 files changed, 448 insertions(+), 117 deletions(-) create mode 100644 cli/client/arbitrate-escrow.ts diff --git a/README.md b/README.md index 8616715..e90cf51 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ nla stop | `nla escrow:fulfill [options]` | Submit fulfillment for an escrow | | `nla escrow:collect [options]` | Collect approved escrow funds | | `nla escrow:status --escrow-uid ` | Check escrow status | +| `nla escrow:arbitrate [options]` | Manually arbitrate escrow fulfillments | ### Environment Management diff --git a/cli/client/arbitrate-escrow.ts b/cli/client/arbitrate-escrow.ts new file mode 100644 index 0000000..43221e4 --- /dev/null +++ b/cli/client/arbitrate-escrow.ts @@ -0,0 +1,381 @@ +#!/usr/bin/env node +/** + * CLI tool to manually arbitrate NLA escrow fulfillments + * + * This allows an oracle operator to manually review and submit + * arbitration decisions for escrow fulfillments, as an alternative + * to the automated oracle listener. + */ + +import { parseArgs } from "util"; +import { createWalletClient, createPublicClient, http, publicActions, fromHex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { makeClient } from "alkahest-ts"; +import { contracts } from "alkahest-ts"; +import { makeLLMClient } from "../.."; +import { ProviderName } from "../../nla"; +import { + getCurrentEnvironment, + getChainFromNetwork, + loadDeploymentWithDefaults, + getPrivateKey, + loadEnvFile, +} from "../utils.js"; +import { existsSync } from "fs"; +import { resolve } from "path"; +import { createInterface } from "readline"; + +// Helper function to display usage +function displayHelp() { + console.log(` +Natural Language Agreement - Arbitrate Escrow Fulfillments + +Manually review and submit arbitration decisions for escrow fulfillments. +Your wallet address must match the oracle specified in the escrow. + +Usage: + nla escrow:arbitrate [options] + +Options: + --escrow-uid Escrow UID to arbitrate, or "all" to scan for pending requests (required) + --private-key Oracle operator's private key + --deployment Load addresses from deployment file (optional) + --rpc-url RPC URL (optional, from deployment file) + --auto Auto-arbitrate using LLM (skip interactive confirmation) + --openai-api-key OpenAI API key (for auto mode) + --anthropic-api-key Anthropic API key (for auto mode) + --openrouter-api-key OpenRouter API key (for auto mode) + --perplexity-api-key Perplexity API key (for auto mode) + --env Path to .env file (default: .env) + --help, -h Display this help message + +Examples: + # Arbitrate a specific escrow (interactive) + nla escrow:arbitrate --escrow-uid 0x... + + # Scan for all pending arbitration requests + nla escrow:arbitrate --escrow-uid all + + # Auto-arbitrate using LLM (non-interactive) + nla escrow:arbitrate --escrow-uid 0x... --auto +`); +} + +// Parse command line arguments +function parseCliArgs() { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + "escrow-uid": { type: "string" }, + "private-key": { type: "string" }, + "deployment": { type: "string" }, + "rpc-url": { type: "string" }, + "auto": { type: "boolean" }, + "openai-api-key": { type: "string" }, + "anthropic-api-key": { type: "string" }, + "openrouter-api-key": { type: "string" }, + "perplexity-api-key": { type: "string" }, + "env": { type: "string" }, + "help": { type: "boolean", short: "h" }, + }, + strict: true, + }); + + return values; +} + +function prompt(question: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +type PendingArbitration = { + escrowUid: `0x${string}`; + fulfillmentUid: `0x${string}`; + arbitrationRequestUid: `0x${string}`; + demand: `0x${string}`; + demandText: string; + fulfillmentText: string; + nlaDemand: { + arbitrationProvider: string; + arbitrationModel: string; + arbitrationPrompt: string; + demand: string; + }; +}; + +async function main() { + try { + const args = parseCliArgs(); + + if (args.help) { + displayHelp(); + process.exit(0); + } + + // Load .env file + const envPath = args.env || ".env"; + const resolvedEnvPath = resolve(process.cwd(), envPath); + if (existsSync(resolvedEnvPath)) { + loadEnvFile(resolvedEnvPath); + } + + const escrowUidArg = args["escrow-uid"]; + const privateKey = args["private-key"] || getPrivateKey(); + const deploymentFile = args["deployment"]; + const autoMode = args["auto"] || false; + + if (!escrowUidArg) { + console.error("āŒ Error: --escrow-uid is required (use a UID or \"all\")"); + console.error("Run with --help for usage information."); + process.exit(1); + } + + if (!privateKey) { + console.error("āŒ Error: Private key is required"); + console.error("\nšŸ’” You can either:"); + console.error(" 1. Set it globally: nla wallet:set --private-key "); + console.error(" 2. Use for this command only: --private-key "); + console.error(" 3. Set PRIVATE_KEY environment variable"); + process.exit(1); + } + + // Load deployment + const deployment = loadDeploymentWithDefaults(deploymentFile); + const rpcUrl = args["rpc-url"] || deployment.rpcUrl; + const chain = getChainFromNetwork(deployment.network); + const addresses = deployment.addresses; + + // Create clients + const account = privateKeyToAccount(privateKey as `0x${string}`); + const walletClient = createWalletClient({ + account, + chain, + transport: http(rpcUrl), + }).extend(publicActions) as any; + + const publicClient = createPublicClient({ + chain, + transport: http(rpcUrl), + }); + + const client = makeClient(walletClient, addresses); + const llmClient = makeLLMClient([]); + + console.log(`āš–ļø NLA Manual Arbitration\n`); + console.log(` Oracle address: ${account.address}`); + console.log(` Network: ${deployment.network}`); + console.log(` RPC URL: ${rpcUrl}\n`); + + // Set up LLM providers for auto mode + if (autoMode) { + const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY; + const anthropicApiKey = args["anthropic-api-key"] || process.env.ANTHROPIC_API_KEY; + const openrouterApiKey = args["openrouter-api-key"] || process.env.OPENROUTER_API_KEY; + const perplexityApiKey = args["perplexity-api-key"] || process.env.PERPLEXITY_API_KEY; + + if (!openaiApiKey && !anthropicApiKey && !openrouterApiKey) { + console.error("āŒ Error: Auto mode requires at least one LLM provider API key."); + console.error(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY"); + process.exit(1); + } + + if (openaiApiKey) { + llmClient.addProvider({ providerName: ProviderName.OpenAI, apiKey: openaiApiKey, perplexityApiKey }); + } + if (anthropicApiKey) { + llmClient.addProvider({ providerName: ProviderName.Anthropic, apiKey: anthropicApiKey, perplexityApiKey }); + } + if (openrouterApiKey) { + llmClient.addProvider({ providerName: ProviderName.OpenRouter, apiKey: openrouterApiKey, perplexityApiKey }); + } + } + + // Find pending arbitration requests + const pending: PendingArbitration[] = []; + + console.log("šŸ” Scanning for arbitration requests...\n"); + + // Get all Attested events from EAS + const filter = await publicClient.createContractEventFilter({ + address: addresses.eas as `0x${string}`, + abi: contracts.IEAS.abi.abi, + eventName: "Attested", + fromBlock: 0n, + }); + const events = await publicClient.getFilterLogs({ filter }); + + // Determine which escrows to check + const scanAll = escrowUidArg.toLowerCase() === "all"; + const targetEscrowUid = scanAll ? null : escrowUidArg as `0x${string}`; + + // Build a map of all attestation UIDs for quick lookup + const attestationUids = new Set(events.map((e: any) => e.args?.uid?.toLowerCase())); + + // Find fulfillments (attestations that reference an escrow) + for (const event of events) { + const fulfillmentUid = (event as any).args?.uid as `0x${string}`; + const refUid = (event as any).args?.refUID as `0x${string}`; + if (!fulfillmentUid || !refUid) continue; + + // Skip if we're targeting a specific escrow and this doesn't match + if (!scanAll && refUid.toLowerCase() !== targetEscrowUid!.toLowerCase()) continue; + + // Check if refUid points to a valid escrow + // A fulfillment references an escrow, and an arbitration decision references a fulfillment + // We need to check if this is a fulfillment (not an arbitration decision) + let escrowAttestation: any; + try { + escrowAttestation = await publicClient.readContract({ + address: addresses.eas as `0x${string}`, + abi: contracts.IEAS.abi.abi, + functionName: "getAttestation", + args: [refUid], + }); + } catch { + continue; + } + + // Try to decode as an escrow obligation to verify it's actually an escrow + let escrowData: any; + try { + escrowData = client.erc20.escrow.nonTierable.decodeObligation(escrowAttestation.data); + } catch { + continue; // Not an escrow + } + + // Decode the demand to check if our address is the oracle + let trustedOracleDemand: any; + let nlaDemand: any; + try { + trustedOracleDemand = client.arbiters.general.trustedOracle.decodeDemand(escrowData.demand); + nlaDemand = llmClient.decodeDemand(trustedOracleDemand.data); + } catch { + continue; // Not an NLA demand or not using trusted oracle arbiter + } + + // Check if we are the oracle for this escrow + if (trustedOracleDemand.oracle.toLowerCase() !== account.address.toLowerCase()) { + continue; + } + + // Check if this fulfillment already has an arbitration decision + const hasDecision = events.some((e) => { + const eRefUid = (e as any).args?.refUID; + return eRefUid && eRefUid.toLowerCase() === fulfillmentUid.toLowerCase(); + }); + + if (hasDecision) continue; + + // Decode the fulfillment text + let fulfillmentText: string; + try { + const fulfillmentAttestation = await publicClient.readContract({ + address: addresses.eas as `0x${string}`, + abi: contracts.IEAS.abi.abi, + functionName: "getAttestation", + args: [fulfillmentUid], + }); + const commitRevealData = client.commitReveal.decode((fulfillmentAttestation as any).data); + fulfillmentText = fromHex(commitRevealData.payload, "string"); + } catch { + continue; // Can't decode fulfillment + } + + pending.push({ + escrowUid: refUid, + fulfillmentUid, + arbitrationRequestUid: fulfillmentUid, + demand: escrowData.demand, + demandText: nlaDemand.demand, + fulfillmentText, + nlaDemand, + }); + } + + if (pending.length === 0) { + if (scanAll) { + console.log("āœ… No pending arbitration requests found for your oracle address.\n"); + } else { + console.log(`āœ… No pending arbitration requests found for escrow ${escrowUidArg}.\n`); + console.log(" This could mean:"); + console.log(" - No fulfillments have been submitted yet"); + console.log(" - All fulfillments have already been arbitrated"); + console.log(" - Your address is not the oracle for this escrow"); + } + process.exit(0); + } + + console.log(`šŸ“‹ Found ${pending.length} pending arbitration request(s):\n`); + + // Process each pending arbitration + for (const item of pending) { + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + console.log(`šŸ“¦ Escrow: ${item.escrowUid}`); + console.log(`šŸ“Ø Fulfillment: ${item.fulfillmentUid}`); + console.log(`šŸ“ Demand: "${item.demandText}"`); + console.log(`šŸ’¬ Fulfillment: "${item.fulfillmentText}"`); + console.log(`šŸ¤– Provider: ${item.nlaDemand.arbitrationProvider} / ${item.nlaDemand.arbitrationModel}`); + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + let decision: boolean; + + if (autoMode) { + // Use LLM to arbitrate + console.log(`šŸ¤” Arbitrating with ${item.nlaDemand.arbitrationProvider}...`); + decision = await llmClient.arbitrate(item.nlaDemand, item.fulfillmentText); + console.log(` LLM decision: ${decision ? "āœ… APPROVE" : "āŒ REJECT"}\n`); + } else { + // Interactive mode - ask the user + const answer = await prompt(" Enter decision (approve/reject): "); + const normalized = answer.toLowerCase(); + if (normalized === "approve" || normalized === "a" || normalized === "yes" || normalized === "y" || normalized === "true") { + decision = true; + } else if (normalized === "reject" || normalized === "r" || normalized === "no" || normalized === "n" || normalized === "false") { + decision = false; + } else if (normalized === "skip" || normalized === "s") { + console.log(" Skipped.\n"); + continue; + } else { + console.log(" Unrecognized input, skipping.\n"); + continue; + } + } + + // Submit the decision on-chain + console.log(`šŸ“¤ Submitting ${decision ? "APPROVE" : "REJECT"} decision on-chain...`); + try { + const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany( + async () => decision, + { + onAfterArbitrate: async (result: any) => { + console.log(` āœ… Decision recorded!`); + console.log(` Decision UID: ${result.attestation.uid}`); + console.log(` Result: ${result.decision ? "APPROVED" : "REJECTED"}\n`); + }, + pollingInterval: 1000, + } + ); + + // Wait briefly for the arbitration to be picked up and processed + await new Promise(resolve => setTimeout(resolve, 3000)); + unwatch(); + } catch (error: any) { + console.error(` āŒ Failed to submit decision: ${error.message}\n`); + } + } + + console.log("✨ Arbitration complete!\n"); + + } catch (error) { + console.error("āŒ Fatal error:", error); + process.exit(1); + } +} + +main(); diff --git a/cli/index.ts b/cli/index.ts index bee2ae7..b62163e 100755 --- a/cli/index.ts +++ b/cli/index.ts @@ -39,6 +39,7 @@ Commands: escrow:fulfill Fulfill an existing escrow escrow:collect Collect an approved escrow escrow:status Check the status of an escrow + escrow:arbitrate Manually arbitrate escrow fulfillments help Display this help message Options (vary by command): @@ -98,6 +99,12 @@ Examples: # Check escrow status nla escrow:status --escrow-uid 0x... + + # Manually arbitrate a specific escrow + nla escrow:arbitrate --escrow-uid 0x... + + # Scan for all pending arbitration requests + nla escrow:arbitrate --escrow-uid all `); } @@ -135,6 +142,11 @@ function parseCliArgs() { "arbitration-prompt": { type: "string" }, "env": { type: "string" }, "environment": { type: "string" }, + "auto": { type: "boolean" }, + "openai-api-key": { type: "string" }, + "anthropic-api-key": { type: "string" }, + "openrouter-api-key": { type: "string" }, + "perplexity-api-key": { type: "string" }, "help": { type: "boolean", short: "h" }, }, strict: command !== "switch" && command !== "network", // Allow positional args for switch command @@ -235,6 +247,9 @@ async function main() { case "escrow:status": scriptPath = "./client/status-escrow.js"; break; + case "escrow:arbitrate": + scriptPath = "./client/arbitrate-escrow.js"; + break; default: console.error(`āŒ Unknown command: ${command}`); console.error("Run 'nla help' for usage information."); diff --git a/skills/nla-arbitrate/SKILL.md b/skills/nla-arbitrate/SKILL.md index ef021cf..2580687 100644 --- a/skills/nla-arbitrate/SKILL.md +++ b/skills/nla-arbitrate/SKILL.md @@ -1,167 +1,101 @@ --- name: nla-arbitrate -description: Manually arbitrate NLA escrows as an alternative to the automated oracle. Use when the user wants to act as an AI arbiter themselves - fetching pending escrows that name the agent as oracle, evaluating demands against fulfillments, and submitting on-chain arbitration decisions via the alkahest TypeScript SDK. +description: Manually arbitrate NLA escrow fulfillments as an alternative to the automated oracle. Use when the user wants to review pending arbitration requests, evaluate demands against fulfillments, and submit on-chain decisions. Supports both interactive and LLM-auto modes. metadata: author: arkhai version: "1.0" -compatibility: Requires bun or node. Requires alkahest-ts and nla packages. Requires a funded Ethereum wallet whose address matches the oracle specified in escrows. -allowed-tools: Bash Read Write +compatibility: Requires nla CLI installed (npm install -g nla). Requires a funded Ethereum wallet whose address matches the oracle specified in escrows. +allowed-tools: Bash(nla:*) Read --- # Manual NLA Arbitration -Act as an AI arbiter for NLA escrows, bypassing the automated oracle listener. Fetch escrow data, evaluate demands against fulfillments, and submit arbitration decisions on-chain. +Manually arbitrate escrow fulfillments using the `nla escrow:arbitrate` CLI command, bypassing the automated oracle listener. ## When to use this -- The user wants to manually review and decide on escrow fulfillments instead of relying on the automated oracle +- The user wants to manually review and decide on escrow fulfillments - The user is the oracle (their wallet address was specified as the oracle when escrows were created) - The automated oracle is not running, or the user wants more control over decisions ## Step-by-step instructions -### 1. Determine the oracle address +### 1. Verify oracle identity -The user's wallet address must be the oracle specified in the escrow. Check: +The user's wallet must be the oracle address specified in the escrow: ```bash nla wallet:show ``` -### 2. Fetch pending arbitration requests +### 2a. Arbitrate a specific escrow -Write and run a TypeScript script to find escrows pending arbitration. The script uses the alkahest-ts SDK to query on-chain state. +To review fulfillments for a known escrow UID: -```typescript -import { createWalletClient, createPublicClient, http, publicActions, fromHex } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { makeClient } from "alkahest-ts"; -import { contracts } from "alkahest-ts"; -import { makeLLMClient } from "nla"; +```bash +# Interactive mode - prompts for approve/reject +nla escrow:arbitrate --escrow-uid -// Setup - adjust these for the user's environment -const deployment = JSON.parse(require("fs").readFileSync("", "utf-8")); -// Deployment files are at: cli/deployments/anvil.json, sepolia.json, base-sepolia.json, mainnet.json -// Or detect automatically using nla's utils - -const chain = /* appropriate viem chain */; -const account = privateKeyToAccount("" as `0x${string}`); -const walletClient = createWalletClient({ - account, - chain, - transport: http(deployment.rpcUrl), -}).extend(publicActions); - -const client = makeClient(walletClient as any, deployment.addresses); -const llmClient = makeLLMClient([]); - -const publicClient = createPublicClient({ - chain, - transport: http(deployment.rpcUrl), -}); - -// Query all Attested events from EAS -const filter = await publicClient.createContractEventFilter({ - address: deployment.addresses.eas as `0x${string}`, - abi: contracts.IEAS.abi.abi, - eventName: "Attested", - fromBlock: 0n, -}); -const events = await publicClient.getFilterLogs({ filter }); - -// For each event, check if it's an arbitration request targeting our oracle address -// Arbitration requests reference a fulfillment UID which in turn references an escrow UID +# Auto mode - uses the LLM specified in the escrow's demand +nla escrow:arbitrate --escrow-uid --auto ``` -### 3. Decode escrow and fulfillment data +### 2b. Scan for all pending requests -For a specific escrow UID: +To find all unarbitrated fulfillments where the user is the oracle: -```typescript -// Get the escrow attestation -const escrow = await client.getAttestation(escrowUid); -const escrowData = client.erc20.escrow.nonTierable.decodeObligation(escrow.data); +```bash +# Interactive mode +nla escrow:arbitrate --escrow-uid all -// The demand is double-encoded: -// Layer 1: TrustedOracleArbiter demand (contains oracle address + inner data) -const trustedOracleDemand = client.arbiters.general.trustedOracle.decodeDemand(escrowData.demand); -// trustedOracleDemand.oracle - the oracle address (should match user's address) -// trustedOracleDemand.data - the inner NLA demand data - -// Layer 2: NLA LLM demand (contains provider, model, prompt, demand text) -const nlaDemand = llmClient.decodeDemand(trustedOracleDemand.data); -// nlaDemand.demand - the natural language demand text -// nlaDemand.arbitrationProvider - e.g. "OpenAI" -// nlaDemand.arbitrationModel - e.g. "gpt-4o-mini" -// nlaDemand.arbitrationPrompt - the prompt template with {{demand}} and {{obligation}} placeholders - -// Get fulfillment data (fulfillments use CommitRevealObligation) -const fulfillment = await client.getAttestation(fulfillmentUid); -const commitRevealData = client.commitReveal.decode(fulfillment.data); -const fulfillmentText = fromHex(commitRevealData.payload, "string"); +# Auto mode +nla escrow:arbitrate --escrow-uid all --auto ``` -### 4. Evaluate the fulfillment +### 3. Review and decide -Present the decoded data to the user (or evaluate it yourself as the AI agent): +In **interactive mode**, the command displays each pending fulfillment with: +- Escrow UID and fulfillment UID +- The demand text +- The fulfillment text +- The arbitration provider/model specified -- **Demand**: The natural language condition -- **Fulfillment**: The submitted text -- **Arbitration prompt**: The template that guides evaluation -- **Model/Provider**: What was originally specified (informational for manual arbitration) +Then prompts for a decision: `approve`, `reject`, or `skip`. -Apply the arbitration prompt logic: substitute `{{demand}}` and `{{obligation}}` with actual values, then determine if the fulfillment satisfies the demand. The result is a boolean: `true` (approved) or `false` (rejected). +In **auto mode** (`--auto`), the command uses the LLM provider/model specified in the escrow's demand to arbitrate automatically. Requires at least one LLM API key via environment variables or flags (`--openai-api-key`, `--anthropic-api-key`, `--openrouter-api-key`). -### 5. Submit the arbitration decision +### 4. Verify -Use the `arbitrateMany` callback or submit directly: - -```typescript -// Option A: Use arbitrateMany with a custom callback (handles polling and submission) -const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany( - async ({ attestation, demand }) => { - // decode and evaluate as shown above - // return true or false - return decision; - }, - { - onAfterArbitrate: async (result) => { - console.log(`Decision UID: ${result.attestation.uid}`); - console.log(`Result: ${result.decision ? "APPROVED" : "REJECTED"}`); - }, - pollingInterval: 1000, - } -); - -// Option B: For one-shot arbitration, use arbitrateMany with a short polling interval -// and call unwatch() after the decision is submitted -``` - -### 6. Verify the decision +After arbitration, check the result: ```bash nla escrow:status --escrow-uid ``` -Confirm the arbitration decision appears in the status output. - ## Key details -- The user's wallet address MUST match the oracle address in the escrow's demand - otherwise the on-chain arbiter contract will reject the decision -- Demands are double-encoded: TrustedOracleArbiter wraps NLA LLM demand data -- Fulfillments use CommitRevealObligation - decode with `client.commitReveal.decode()`, then `fromHex(payload, "string")` -- The `arbitrateMany` method handles both polling for pending requests and submitting decisions on-chain -- Each arbitration decision is recorded as an on-chain attestation (permanent and immutable) -- Deployment files with contract addresses are at `cli/deployments/.json` -- Use `nla network` to check which network is currently active +- The user's wallet address MUST match the oracle address in the escrow - otherwise the on-chain contract rejects the decision +- Each arbitration decision is recorded as a permanent on-chain attestation +- In interactive mode, type `skip` or `s` to skip a fulfillment without deciding +- Auto mode reads LLM API keys from environment variables (OPENAI_API_KEY, etc.) or CLI flags +- If no pending requests are found, the command explains possible reasons (no fulfillments yet, already arbitrated, or wrong oracle address) -## Alternative: use nla escrow:status for read-only inspection +## Prerequisites -If the user just wants to inspect escrow state without submitting decisions: +- `nla` CLI installed and configured +- Private key set via `nla wallet:set`, `--private-key` flag, or `PRIVATE_KEY` env var +- ETH in the oracle's account for gas (submitting decisions costs gas) +- For auto mode: at least one LLM provider API key + +## Examples ```bash -# View escrow details, fulfillments, and existing arbitration results -nla escrow:status --escrow-uid -``` +# Scan for all pending requests, decide interactively +nla escrow:arbitrate --escrow-uid all -This does not require being the oracle. +# Auto-arbitrate a specific escrow using LLM +nla escrow:arbitrate --escrow-uid 0xabc123... --auto + +# Auto-arbitrate all pending, with explicit API key +nla escrow:arbitrate --escrow-uid all --auto --openai-api-key sk-... +```