diff --git a/README.md b/README.md index 99ad6d9..9bbdfd8 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,120 @@ # OmniClaw -**Policy-controlled payments for AI agents and machine services.** +*One install. Every payment rail. Policy enforced by default.* [![CI](https://github.com/omnuron/omniclaw/actions/workflows/ci.yml/badge.svg)](https://github.com/omnuron/omniclaw/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/omniclaw.svg)](https://pypi.org/project/omniclaw/) [![Python](https://img.shields.io/pypi/pyversions/omniclaw.svg)](https://pypi.org/project/omniclaw/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -OmniClaw lets software agents pay, earn, and access paid APIs without giving the agent unrestricted wallet authority. +`lablab.ai Agentic Commerce Hackathon on ARC (1st place) · Top 11 finalist, C.R.I.S.P Agentic AI Ideation Challenge` -The owner runs a **Financial Policy Engine**. Agents and applications execute through constrained interfaces. Every payment is checked against policy before funds move. +OmniClaw is the control layer for agent money. -## Why It Exists +It lets teams ship autonomous payments without giving software unrestricted wallet authority. -AI agents can browse, reason, call APIs, and run workflows. The hard part is money movement. +Wallets give keys. Facilitators settle payments. OmniClaw governs whether an agent is allowed to pay, routes the right rail, and gives vendors and infrastructure teams a usable product around that control model. -OmniClaw solves the control problem: +Buyers get policy. Vendors get paid endpoints. Infrastructure teams get self-hosted exact settlement on Arc, Base, and Ethereum. -- agents can pay for services without receiving raw wallet control -- sellers can monetize APIs through x402-compatible payment gates -- operators can enforce budgets, recipient rules, confirmations, and route selection -- payments can settle through Circle Gateway, standard x402 exact settlement, or a self-hosted exact facilitator +## Proven In Public -## Core Surfaces +- Arc Testnet exact settlement proof: `https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219` +- 1st place at `lablab.ai Agentic Commerce Hackathon on ARC` +- Top 11 finalist in the `C.R.I.S.P Agentic AI Ideation Challenge` +- Shipped buyer CLI, buyer SDK, seller SDK, ERC-8004 trust checks, Circle Gateway nanopayments, and a self-hosted `x402 exact` facilitator runtime -| Surface | Used By | Purpose | -| --- | --- | --- | -| Financial Policy Engine | owner / operator | Enforces policy, signs allowed actions, exposes the control API | -| `omniclaw-cli` | agents / automation | Executes buyer payments through the policy engine without direct key access | -| Python SDK | developers / vendors | Embeds buyer payments and seller monetization into Python applications | -| Seller middleware | vendors / enterprises | Turns production HTTP routes into paid x402 endpoints | -| Exact facilitator | operators | Optional self-hosted x402 exact settlement for supported EVM networks | +If you want the fastest proof, run: + +```bash +bash scripts/start_arc_marketplace_showcase_docker.sh +``` + +Then open `http://127.0.0.1:8020` and complete the browser buyer flow. + +## Why Teams Use OmniClaw + +| If you are... | OmniClaw gives you... | +| --- | --- | +| Building an agent buyer | Policy-controlled payments without giving the agent raw wallet authority | +| Monetizing an API or service | Paid routes through `client.sell(...)` and x402-compatible seller flows | +| Running payment infrastructure | Route selection across Circle Gateway and standard `x402 exact`, plus self-hosted exact settlement when hosted coverage stops short | + +## Pick A Path In 60 Seconds + +One product, three adoption paths: autonomous buyers, paid vendors, and self-hosted settlement infrastructure. + +| I want to... | Run this first | Success looks like... | Jump to... | +| --- | --- | --- | --- | +| Try the full Arc flow now | `bash scripts/start_arc_marketplace_showcase_docker.sh` | browser kiosk + buyer flow + self-hosted exact settlement | [Arc Marketplace Showcase](examples/arc-marketplace-showcase/README.md) | +| Let an agent buy from paid APIs | `omniclaw server` + `omniclaw-cli pay` | agent pays through policy, not a raw key | [Buyer: Agent CLI](#buyer-agent-cli) | +| Pay programmatically from Python | `OmniClaw().pay(...)` | Python service buys from paid APIs | [Buyer: Python SDK](#buyer-python-sdk) | +| Monetize a vendor API | `OmniClaw().sell(...)` | FastAPI route returns `402` until paid | [Seller: Vendor / Enterprise SDK](#seller-vendor--enterprise-sdk) | +| Run your own exact facilitator | `omniclaw facilitator exact` | self-hosted `verify` / `settle` on supported EVM networks | [Self-Hosted Exact Facilitator](#self-hosted-exact-facilitator) | + +## The Problem + +AI agents can browse, reason, call APIs, and execute workflows autonomously. -## Install +The dangerous part is money. + +Give an agent a private key and a single hallucination, prompt injection, or bad tool call can drain a treasury in seconds. Existing solutions usually hand the agent a wallet and hope for the best. + +OmniClaw solves this by separating authority from execution. The owner defines policy. The agent executes within it. Every payment is checked before funds move. + +In one sentence: OmniClaw is the economic execution and control layer for agentic systems. + +## Prerequisites + +| Path | What you need | +| --- | --- | +| Arc showcase | Docker | +| Buyer CLI | Python 3.11+, funded EVM key, RPC URL | +| Buyer SDK | Python 3.11+, RPC URL | +| Seller SDK | Python 3.11+, optional Circle credentials for Gateway flows | +| Self-hosted facilitator | Python 3.11+, funded EVM key, RPC URL | + +## Install And Run ```bash pip install omniclaw ``` -For local development: +Package development: ```bash uv add omniclaw ``` -## Choose The Right Path +## Wallets vs Facilitators vs OmniClaw -| If you are building... | Use... | Why | +| Layer | What it does | What it does not do | | --- | --- | --- | -| An agent that needs to buy services | Financial Policy Engine + `omniclaw-cli` | The agent can pay without holding raw wallet authority | -| A backend service that buys from paid APIs | Python SDK `client.pay(...)` | Programmatic payments inside your own app | -| A vendor or enterprise API | Python SDK `client.sell(...)` | Production paid endpoints inside your application | -| A temporary local paid agent service | `omniclaw-cli serve` | Fast agent-owned/local monetization, not the enterprise seller path | -| Custom or Arc exact settlement infrastructure | `omniclaw facilitator exact` | Self-hosted standard x402 `verify` / `settle` | +| Wallets | Hold keys and sign | Decide whether an agent should be allowed to spend | +| Facilitators | Verify and settle supported payment payloads | Govern financial authority before money moves | +| OmniClaw | Enforces policy before payment, routes the right rail, supports buyer and seller flows, and can self-host exact settlement when needed | Replace every settlement provider or blockchain rail | + +OmniClaw is a policy-controlled payment layer for agents and vendors. It lets agents pay through approved rails, lets vendors monetize routes, and lets infrastructure teams run or self-host settlement when hosted facilitators stop short. + +Core shipped surfaces: + +- Financial Policy Engine for payment authority, limits, approvals, trust checks, and execution control +- `omniclaw-cli` for agent-side buyer execution +- Python SDK for buyer payments and seller monetization +- Circle Gateway nanopayments for gasless microflows +- Standard `x402 exact` buyer flow with direct-wallet signing +- Self-hosted `x402 exact` facilitator for Arc Testnet, Base Sepolia, Ethereum Sepolia, Base mainnet, and Ethereum mainnet + +> OmniClaw governs financial authority. Facilitators settle supported x402 payment payloads. These are separate concerns. ## Credential Model OmniClaw has two different key surfaces: -- `OMNICLAW_PRIVATE_KEY` is the EOA key used for direct x402 exact settlement and Circle Gateway nanopayment signing. +- `OMNICLAW_PRIVATE_KEY` is the EOA key used for direct `x402 exact` settlement and Circle Gateway nanopayment signing. - `ENTITY_SECRET` is Circle's developer-controlled wallet encryption secret. -If your Circle account/API key already has an Entity Secret, set it directly. Circle allows one active Entity Secret per account/API key. OmniClaw only auto-generates and registers a new one when no existing secret is provided or found in its managed local credential store. +If your Circle account or API key already has an Entity Secret, set it directly. Circle allows one active Entity Secret per account and API key. OmniClaw only auto-generates and registers a new one when no existing secret is provided or found in its managed local credential store. ```bash export CIRCLE_API_KEY="..." @@ -75,11 +128,18 @@ For a non-interactive local setup: omniclaw setup --api-key "$CIRCLE_API_KEY" --entity-secret "$ENTITY_SECRET" ``` +## Default Product Shapes + +- Agent buyer: run the Financial Policy Engine, then pay with `omniclaw-cli` +- Application buyer: integrate `client.pay(...)` in Python +- Vendor seller: monetize routes with `client.sell(...)` +- Infrastructure operator: run `omniclaw facilitator exact` for self-hosted exact settlement + ## Buyer: Agent CLI -Use this when an autonomous agent or script should pay through the **Financial Policy Engine** (run via the `omniclaw server` command). +Use this when an autonomous agent or script should pay through the Financial Policy Engine. -Start the owner-side policy engine: +Start the policy engine: ```bash export OMNICLAW_PRIVATE_KEY="0x..." @@ -116,6 +176,8 @@ omniclaw-cli pay \ --idempotency-key job-123 ``` +The same CLI surface can also inspect balances, ledger entries, and paid endpoint requirements without exposing private keys to the agent. + ## Buyer: Python SDK Use this when a Python service should pay programmatically. @@ -131,6 +193,7 @@ result = await client.pay( amount="1.00", purpose="compute job", idempotency_key="job-123", + check_trust=True, ) print(result.status, result.blockchain_tx or result.transaction_id) @@ -138,6 +201,8 @@ print(result.status, result.blockchain_tx or result.transaction_id) For x402 URLs, `amount` acts as the maximum spend allowed for that request. The seller's x402 requirements define the exact amount to settle. +When trust is enabled, OmniClaw can evaluate ERC-8004 identity and reputation signals before the payment is allowed to proceed. + ## Seller: Vendor / Enterprise SDK Use this when a vendor, enterprise, or application team wants to monetize API routes. This is the default seller path for real products. @@ -162,64 +227,128 @@ async def premium_data( The route returns `402 Payment Required` until the buyer submits a valid x402 payment. After verification and settlement, the handler executes and returns the paid response. -## Seller: Agent-Owned Local Service +`omniclaw-cli serve` remains the agent-facing seller/runtime surface. Use it when an agent needs to expose a paid endpoint for other agents or automation. Use the SDK seller path when a vendor or enterprise team is embedding paid routes directly into an application. + +## Self-Hosted Exact Facilitator + +Hosted facilitators do not support every chain, every flow, or every developer workflow. Some require managed accounts, signup gates, or hosted onboarding before you can even run a demo. + +OmniClaw ships a self-hosted `x402 exact` facilitator so you can run standard `verify` and `settle` yourself. + +What it does: -Use this only when an agent or local automation wants to expose a temporary paid service. It is not the recommended integration path for vendor or enterprise APIs. +- runs a standard `x402 exact` facilitator runtime +- verifies signed payment payloads +- settles payments on supported EVM profiles +- removes dependency on hosted onboarding for unsupported flows + +Supported out of the box: + +- Arc Testnet +- Base Sepolia +- Ethereum Sepolia +- Base mainnet +- Ethereum mainnet + +Start it with one command: ```bash -omniclaw-cli serve \ - --price 0.25 \ - --endpoint /compute \ - --exec "python compute_job.py" \ - --port 8000 +omniclaw facilitator exact --network-profile ARC-TESTNET --port 4022 ``` -For vendor and enterprise APIs, use the Python SDK middleware so payments are part of the application itself. +Or use the helper script: -## Settlement Paths +```bash +bash scripts/start_arc_exact_facilitator.sh +``` -OmniClaw is settlement-rail aware and policy-first. The buyer uses one execution path while the seller advertises the x402 requirements it supports. +Arc Testnet notes: -| Path | Status | Notes | -| --- | --- | --- | -| Circle Gateway `GatewayWalletBatched` | supported | Gasless nanopayments through Circle Gateway | -| Standard x402 exact via x402.org | supported (Base Sepolia) | External exact facilitator validation | -| OmniClaw self-hosted exact facilitator | supported (Arc Testnet) | Self-hosted `verify` and `settle` for supported EVM profiles | -| Thirdweb x402 HTTP facilitator | supported | Managed Thirdweb account required | +- Arc Testnet uses native USDC for gas +- the exact settlement path calls Arc USDC `transferWithAuthorization` +- the result is visible on ArcScan like any other on-chain proof -Current capabilities: +Latest public Arc proof transaction: -- Base Sepolia external x402 exact settlement -- Arc Testnet self-hosted exact settlement -- buyer/seller wallet separation -- policy-controlled buyer route through `/api/v1/pay` +```text +https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 +``` + +Full Arc marketplace showcase: + +```bash +bash scripts/start_arc_marketplace_showcase_docker.sh +``` + +That launcher starts the vendor kiosk, buyer policy engine, and self-hosted facilitator together so the entire buyer-to-seller flow can be demonstrated from one browser page. ## Examples | Example | Demonstrates | | --- | --- | -| [B2B SDK Integration](examples/b2b-sdk-integration/README.md) | Enterprise buyer/seller SDK integration with multiple facilitators | +| [B2B SDK Integration](examples/b2b-sdk-integration/README.md) | Enterprise buyer and seller SDK integration with multiple facilitators | | [Machine to Machine](examples/machine-to-machine/README.md) | One machine service paying another | | [Machine to Vendor](examples/machine-to-vendor/README.md) | Agent buyer paying a vendor-owned API | | [Vendor Integration](examples/vendor-integration/README.md) | Vendor-side paid API integration | | [Business Compute](examples/business-compute/README.md) | Payment-gated compute service | -| [Local Economy](examples/local-economy/README.md) | Local buyer/seller economy with Docker | +| [Local Economy](examples/local-economy/README.md) | Local buyer and seller economy with Docker | | [External x402 Facilitator](examples/external-x402-facilitator/README.md) | x402.org Base Sepolia validation | | [Thirdweb HTTP Facilitator](examples/thirdweb-http-facilitator/README.md) | Thirdweb HTTP API validation | +| [Arc Marketplace Showcase](examples/arc-marketplace-showcase/README.md) | Visual vendor kiosk with Arc Testnet x402 exact settlement | + +## Architecture + +```mermaid +flowchart TD + A[Agent / CLI / App] --> B[Financial Policy Engine] + B --> B1[Guards] + B --> B2[ERC-8004 Trust Gate] + B --> B3[Ledger] + B --> B4[Fund Locks] + B --> B5[Payment Router] + B5 --> C1[Circle Gateway Nanopayments] + B5 --> C2[x402 Exact] + B5 --> C3[Self-Hosted Facilitator Runtime] + C1 --> D[Blockchain / Settlement Network] + C2 --> D + C3 --> D + D --> E[Arc / Base / Ethereum / Other Supported Rails] +``` + +## Execution Pipeline + +Every `client.pay()` call runs through: + +1. Argument validation +2. Trust evaluation when enabled +3. Ledger entry creation +4. Guard reservation +5. Wallet fund lock acquisition +6. Balance verification after reservations +7. Router and adapter selection +8. Guard commit or release +9. Ledger status update +10. Wallet lock release + +This is why OmniClaw is not just a thin wallet wrapper. The payment call is a controlled execution pipeline, not a raw transfer helper. ## Documentation | Start Here | Use Case | | --- | --- | | [Documentation Index](docs/README.md) | Complete docs map | +| [Architecture and Features](docs/FEATURES.md) | Financial Policy Engine design and subsystem responsibilities | | [Developer Guide](docs/developer-guide.md) | Python SDK buyer and seller integration | | [Agent Getting Started](docs/agent-getting-started.md) | Agent CLI setup and usage | | [CLI Reference](docs/cli-reference.md) | Generated `omniclaw-cli` reference | -| [Operator CLI](docs/operator-cli.md) | `omniclaw server`, setup, policy, facilitator commands | +| [Operator CLI](docs/operator-cli.md) | `omniclaw server`, setup, policy, and facilitator commands | | [Policy Reference](docs/POLICY_REFERENCE.md) | Policy file structure and controls | | [Facilitators](docs/facilitators.md) | x402 facilitator model and deployment paths | | [Production Readiness](docs/production-readiness.md) | Proof status and release checklist | | [API Reference](docs/API_REFERENCE.md) | Python SDK and API details | +| [ERC-8004 Trust Notes](docs/erc_804_spec.md) | Trust-layer notes and registry framing | + +[Star history](https://star-history.com/#omnuron/omniclaw&Date) ## Development @@ -236,7 +365,7 @@ Release verification: ## Security -OmniClaw is designed around separation of authority: agents do not need unrestricted wallet access. Production deployments should still use restricted keys, policy limits, confirmation thresholds, hardened secrets, and audited infrastructure. +OmniClaw is designed around separation of authority. Agents do not need unrestricted wallet access. Production deployments should still use restricted keys, policy limits, confirmation thresholds, hardened secrets, audited infrastructure, and real operational review. Report vulnerabilities through [SECURITY.md](SECURITY.md). diff --git a/docs/CCTP_USAGE.md b/docs/CCTP_USAGE.md index 930d1f8..eacf33c 100644 --- a/docs/CCTP_USAGE.md +++ b/docs/CCTP_USAGE.md @@ -48,7 +48,7 @@ sim = await client.simulate( - Cross-chain flows depend on the source and destination networks being supported by the configured gateway/CCTP path. - Same-chain transfers should not specify a different `destination_chain`. - Use `wait_for_completion=True` only when the caller is prepared to block for provider-side polling. -- For launch usage, verify network support with Circle’s current chain support before relying on a pair in production. +- Before production use, verify network support with Circle’s current chain support before relying on a pair in production. ## Result Metadata diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 828391b..a1e266b 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -78,9 +78,9 @@ Supported deployment modes: - Circle Gateway `GatewayWalletBatched` for gasless nanopayment settlement - self-hosted OmniClaw exact facilitator, started with `omniclaw facilitator exact` -Thirdweb is a priority external integration path because it provides broad EVM facilitator coverage and gas-sponsored settlement. OmniClaw adds buyer-side policy, route selection, SDK/CLI execution surfaces, and operator visibility. +Thirdweb is one supported external integration path for teams that want broad EVM facilitator coverage and gas-sponsored settlement. OmniClaw adds buyer-side policy, route selection, SDK and CLI execution surfaces, and payment visibility on top. -OmniClaw added the self-hosted exact facilitator so teams can support networks and proof environments before they are available through their selected hosted facilitator. This is how Arc Testnet is handled: it remains standard x402 `exact` settlement, with OmniClaw providing the network profile, asset metadata, RPC, and facilitator runtime. +The self-hosted exact facilitator exists so teams can support networks and deployments before they are available through a hosted provider. Arc Testnet is the clearest example: it remains standard x402 `exact` settlement, with OmniClaw providing the network profile, asset metadata, RPC, and facilitator runtime. See [facilitators.md](facilitators.md) for deployment details. @@ -116,10 +116,10 @@ The ledger in [ledger/](../src/omniclaw/ledger) tracks payment records and statu Typical use cases: -- internal observability +- payment observability - transaction lookup - reconciliation -- launch debugging +- operational debugging ### Payment Intents diff --git a/docs/PRODUCTION_HARDENING.md b/docs/PRODUCTION_HARDENING.md index 92e2d8b..62b4f9d 100644 --- a/docs/PRODUCTION_HARDENING.md +++ b/docs/PRODUCTION_HARDENING.md @@ -53,9 +53,9 @@ OmniClaw is facilitator-agnostic. Production deployments should choose the settl - Thirdweb-backed x402 facilitator for managed gas-sponsored exact settlement across broad EVM coverage - Circle Gateway `GatewayWalletBatched` for gasless batched nanopayments - external standard x402 facilitator where the seller already uses one -- self-hosted OmniClaw exact facilitator only when local proof, custom network support, or enterprise self-hosting is required +- self-hosted OmniClaw exact facilitator when local proof, custom network support, or enterprise self-hosting is required -Do not run a self-hosted facilitator by default if a managed facilitator already supports the target flow. The self-hosted path is operational infrastructure, not the primary product wedge. +Use a self-hosted facilitator when it fits the network and operational model. Use a managed facilitator when it already cleanly supports the target flow. Before production traffic, validate the exact seller path with: diff --git a/docs/README.md b/docs/README.md index 8839272..cb37301 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,8 +11,8 @@ Use this index to choose the right integration path. | Agent buyer | [Agent Getting Started](agent-getting-started.md) | An agent that pays through `omniclaw-cli` | | Python buyer | [Developer Guide](developer-guide.md) | A backend service that pays programmatically | | Vendor / enterprise seller | [Developer Guide](developer-guide.md) | A FastAPI service with paid endpoints through the SDK | -| Operator | [Operator CLI](operator-cli.md) | Financial Policy Engine, policies, and facilitators | -| Release owner | [Production Readiness](production-readiness.md) | Proof checklist and ship status | +| Infrastructure team | [Operator CLI](operator-cli.md) | Financial Policy Engine, policies, and facilitators | +| Maintainer | [Production Readiness](production-readiness.md) | Proof checklist and ship status | ## Buyer Guides @@ -31,7 +31,7 @@ Use this index to choose the right integration path. | [B2B SDK Integration](../examples/b2b-sdk-integration/README.md) | Enterprise SDK deployment with Circle, Thirdweb, or self-hosted exact | | [Vendor Integration](../examples/vendor-integration/README.md) | Production-style vendor API integration | | [Business Compute](../examples/business-compute/README.md) | Payment-gated compute service | -| [CLI Reference](cli-reference.md) | Agent-owned local service flow with `omniclaw-cli serve` | +| [CLI Reference](cli-reference.md) | Agent-facing paid service flow with `omniclaw-cli serve` | ## Machine Payment Examples @@ -43,6 +43,30 @@ Use this index to choose the right integration path. | [Local Economy](../examples/local-economy/README.md) | Local buyer/seller stack with Docker | | [External x402 Facilitator](../examples/external-x402-facilitator/README.md) | x402.org exact settlement on Base Sepolia | | [Thirdweb HTTP Facilitator](../examples/thirdweb-http-facilitator/README.md) | Thirdweb HTTP facilitator integration | +| [Arc Marketplace Showcase](../examples/arc-marketplace-showcase/README.md) | Visual vendor kiosk with Arc Testnet x402 exact settlement | + +## Arc Testnet Quickstart + +Run the full Arc marketplace showcase with Docker-reachable service IPs: + +```bash +bash scripts/start_arc_marketplace_showcase_docker.sh +``` + +The buyer key must hold Arc Testnet USDC for the selected paid product, and the seller/facilitator key must hold Arc Testnet gas. The launcher prints balances, product URLs, the exact OmniClaw config, and a lower-cost `$0.10` proof endpoint when the buyer wallet is not funded for the `$0.25` product. + +The showcase UI also has a built-in mini buyer agent, so the full demo can run directly from the browser. The kiosk backend proxies inspect/pay actions into the buyer Financial Policy Engine while keeping the policy token server-side. + +Defaults: + +| Service | URL | +| --- | --- | +| Browser UI | `http://127.0.0.1:8020` | +| Vendor kiosk | `http://172.18.0.51:8020` | +| Buyer policy engine | `http://172.18.0.52:8080` | +| Exact facilitator | `http://172.18.0.50:4022` | + +For setup details and ArcLens submission notes, see [Arc Marketplace Showcase](../examples/arc-marketplace-showcase/README.md). ## Operator and Production Docs diff --git a/docs/agent-getting-started.md b/docs/agent-getting-started.md index 6bf1255..37597a2 100644 --- a/docs/agent-getting-started.md +++ b/docs/agent-getting-started.md @@ -8,9 +8,9 @@ In that system: - the owner runs the **Financial Policy Engine** - the agent uses `omniclaw-cli` as the **zero-trust execution layer** - buyers pay with `omniclaw-cli pay` -- agents can expose temporary local paid services with `omniclaw-cli serve` +- agents can expose paid services for other agents or automation with `omniclaw-cli serve` -For vendor, SaaS, or enterprise APIs, use the Python SDK seller middleware instead of `omniclaw-cli serve`. See the [Developer Guide](developer-guide.md). +For vendor, SaaS, or enterprise APIs embedded directly in an application, use the Python SDK seller middleware instead of `omniclaw-cli serve`. See the [Developer Guide](developer-guide.md). --- @@ -218,7 +218,7 @@ omniclaw-cli serve \ This opens `http://localhost:8000/api/data` that requires a USDC payment to execute `my_service.py` and return its output. -> **Web developer or vendor:** For real API or SaaS products, use the Python SDK inside your FastAPI application instead of `omniclaw-cli serve`. See the [Developer Guide](developer-guide.md). +> **Web developer or vendor:** If the paid route lives inside your application, use the Python SDK inside your FastAPI application instead of `omniclaw-cli serve`. Use `serve` when the seller surface itself is agent-run. See the [Developer Guide](developer-guide.md). --- diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 4301b45..4728726 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -19,7 +19,7 @@ That full system is larger than the CLI alone: It is the same CLI for agent-side economic execution: - buyer side: `omniclaw-cli pay` -- temporary/local seller side: `omniclaw-cli serve` +- seller side for agent-run paid endpoints: `omniclaw-cli serve` Vendor and enterprise seller APIs should use the Python SDK with `client.sell(...)`. diff --git a/docs/erc_804_spec.md b/docs/erc_804_spec.md index fca6197..1e2e24f 100644 --- a/docs/erc_804_spec.md +++ b/docs/erc_804_spec.md @@ -1,26 +1,26 @@ # ERC-8004 Trust Notes -This file is a historical design note for OmniClaw's trust-layer direction. It is not the canonical API reference and should not be treated as a precise description of the current Financial Policy Engine implementation. +This document explains how ERC-8004 concepts show up in OmniClaw today. -Use these docs for the current product surface instead: +It is a trust-layer overview, not the canonical SDK or API reference. For the main product surface, use: - [README](../README.md) -- [Developer Guide (SDK)](developer-guide.md) +- [Developer Guide](developer-guide.md) - [API Reference](API_REFERENCE.md) - [Architecture and Features](FEATURES.md) -## Current Reality +## How OmniClaw Uses ERC-8004 -OmniClaw already exposes a trust layer through the Financial Policy Engine: +OmniClaw exposes trust evaluation through the Financial Policy Engine: - trust checks can run during `pay()` and `simulate()` - trust behavior is controlled by `check_trust` - explicit trust checks require a real `OMNICLAW_RPC_URL` - trust evaluation can approve, hold, or block payment execution -## What This Note Represents +## What The Trust Layer Covers -The earlier internal design work explored: +The trust system is built around: - ERC-8004 identity lookup - reputation scoring and weighted trust signals @@ -28,12 +28,12 @@ The earlier internal design work explored: - trust-aware payment execution - auditability of trust decisions -Those themes still matter, but the exact content of the original internal draft no longer maps cleanly to the current codebase. +## Practical Guidance -## Recommendation +Treat this file as conceptual background. -If this repo keeps evolving quickly, treat trust docs the same way as the rest of the Financial Policy Engine docs: +Use: -- keep implementation details in code and tests -- keep user-facing behavior in the API reference and usage guide -- keep speculative product thinking in the roadmap, not in protocol documentation \ No newline at end of file +- the SDK and API docs for integration details +- the code and tests for implementation behavior +- the roadmap for future trust-layer expansion diff --git a/docs/facilitators.md b/docs/facilitators.md index 46d514c..b9c8462 100644 --- a/docs/facilitators.md +++ b/docs/facilitators.md @@ -48,7 +48,7 @@ OmniClaw's product responsibility: - enforce buyer policy before money moves - choose a fundable route - sign only the allowed action -- preserve logs, limits, and operator visibility +- preserve logs, limits, and payment visibility That means a seller can use managed facilitator coverage, while the buyer still uses OmniClaw as the policy-controlled execution layer. @@ -120,6 +120,7 @@ Current route priority: - use `exact` when the seller supports standard x402 exact settlement - if the seller supports both and Gateway is not ready, use `exact` - if no supported route is available, fail clearly before spending +- for direct exact payments, inspect checks the buyer's direct-wallet token balance when the selected EVM network and RPC are known ## Self-Host An Exact Facilitator @@ -298,6 +299,74 @@ So the operational requirement for an already configured profile is only: That is a deployment requirement, not a missing architecture requirement. +For Arc Testnet, the buyer key must hold Arc Testnet USDC. The seller/facilitator key must hold Arc Testnet gas because it submits the x402 exact settlement transaction to the USDC contract. + +To run only the Arc self-hosted exact facilitator: + +```bash +export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKeyWithArcGas" +bash scripts/start_arc_exact_facilitator.sh +``` + +Equivalent installed CLI: + +```bash +omniclaw facilitator exact \ + --network-profile ARC-TESTNET \ + --network eip155:5042002 \ + --rpc-url https://rpc.testnet.arc.network \ + --port 4022 +``` + +The facilitator exposes: + +| Endpoint | Purpose | +| --- | --- | +| `GET /supported` | Advertise supported x402 schemes and networks | +| `POST /verify` | Verify a signed x402 payment payload | +| `POST /settle` | Submit settlement on Arc Testnet | + +For a visual Arc vendor demo, use the Arc marketplace showcase: + +```bash +bash scripts/start_arc_marketplace_showcase_docker.sh +``` + +Runbook: [../examples/arc-marketplace-showcase/README.md](../examples/arc-marketplace-showcase/README.md). + +The showcase includes a browser mini buyer agent. It calls the kiosk backend, and the kiosk backend calls the buyer Financial Policy Engine using `ARC_MARKETPLACE_BUYER_ENGINE_URL` and `ARC_MARKETPLACE_BUYER_TOKEN`. This keeps the browser flow simple while the Financial Policy Engine remains the payment authority boundary. + +The Docker launcher starts: + +| Service | URL | +| --- | --- | +| Browser UI | `http://127.0.0.1:8020` | +| Vendor kiosk | `http://172.18.0.51:8020` | +| Buyer policy engine | `http://172.18.0.52:8080` | +| Exact facilitator | `http://172.18.0.50:4022` | + +It also prints the buyer Arc USDC balance, seller Arc gas balance, and the paid product URLs: + +| Product | Price | URL | +| --- | --- | --- | +| Prime Market Scan | `$0.25` | `http://172.18.0.51:8020/buy/prime-market-scan` | +| Risk Oracle Brief | `$0.15` | `http://172.18.0.51:8020/buy/risk-oracle-brief` | +| Settlement Receipt Kit | `$0.10` | `http://172.18.0.51:8020/buy/settlement-receipt-kit` | + +For ecosystem forms that require a contract address, use the Arc Testnet USDC contract used by x402 exact settlement: + +```text +0x3600000000000000000000000000000000000000 +``` + +OmniClaw does not require a custom application contract for this flow. The settlement transaction calls `transferWithAuthorization` on Arc Testnet USDC. + +Latest public proof transaction: + +```text +https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 +``` + ## External Facilitators External facilitators remain first-class. If a seller advertises an `exact` payment requirement using another facilitator, OmniClaw's buyer flow can still pay through the standard x402 SDK path as long as: diff --git a/docs/omniclaw-cli-skill/SKILL.md b/docs/omniclaw-cli-skill/SKILL.md index 7896b56..976fcb6 100644 --- a/docs/omniclaw-cli-skill/SKILL.md +++ b/docs/omniclaw-cli-skill/SKILL.md @@ -3,7 +3,7 @@ name: omniclaw description: > Use this skill whenever an agent needs to pay for an x402 URL, transfer USDC to an address, inspect OmniClaw balances or ledger entries, or expose a - temporary agent-owned paid service with omniclaw-cli serve. OmniClaw is the + paid endpoint for other agents or automation with omniclaw-cli serve. OmniClaw is the Economic Execution and Control Layer for Agentic Systems. The CLI is the zero-trust execution layer for agents. Use this skill for the CLI execution path only, not for vendor SDK integration, owner setup, policy editing, wallet @@ -31,7 +31,7 @@ Use `omniclaw-cli` only when the task is directly about one of these actions: - transfer USDC to an address - inspect wallet, Gateway, or Circle balances - inspect transaction history -- expose a temporary agent-owned paid endpoint with `serve` +- expose a paid endpoint for other agents or automation with `serve` Do not use this skill for: @@ -55,7 +55,7 @@ This skill is specifically about the CLI execution surface. The same CLI has two agent-side economic roles: - buyer role: `omniclaw-cli pay` -- temporary/local seller role: `omniclaw-cli serve` +- seller role for agent-run paid endpoints: `omniclaw-cli serve` Vendor and enterprise seller APIs should use the Python SDK with `client.sell(...)`, not this CLI skill. @@ -105,7 +105,7 @@ Do not invent or search for them yourself. 1. Use `omniclaw-cli pay --recipient <0xaddress> --amount `. 2. Always include `--purpose`. -### For agent-owned local seller tasks +### For agent-run seller tasks 1. Inspect current state with `balance-detail`. 2. Start the paid endpoint with `omniclaw-cli serve`. diff --git a/docs/operator-cli.md b/docs/operator-cli.md index 68fe01a..d6bba1a 100644 --- a/docs/operator-cli.md +++ b/docs/operator-cli.md @@ -2,10 +2,10 @@ OmniClaw ships two command surfaces: -- `omniclaw` for owner/operator infrastructure +- `omniclaw` for infrastructure and control-plane services - `omniclaw-cli` for agent-side financial execution -Use `omniclaw` when you are running infrastructure: +Use `omniclaw` when you are running the policy engine, setup flow, or facilitator infrastructure: ```bash omniclaw setup --api-key "$CIRCLE_API_KEY" --entity-secret "$ENTITY_SECRET" @@ -27,11 +27,11 @@ omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-ke omniclaw-cli serve --price 0.01 --endpoint /api/data --exec "python app.py" ``` -`omniclaw-cli serve` is for agent-owned or local paid services. Vendor and enterprise APIs should use the Python SDK seller middleware (`client.sell(...)`) inside the application. +`omniclaw-cli serve` is the agent-facing seller surface. Use it when an agent needs to expose a paid endpoint for other agents or automation. Vendor and enterprise APIs that live inside application code should use the Python SDK seller middleware (`client.sell(...)`) instead. ## Responsibility Split -The operator CLI manages trusted infrastructure and configuration. The agent CLI executes through the Financial Policy Engine and never needs raw owner authority. +The infrastructure CLI manages trusted configuration and settlement services. The agent CLI executes through the Financial Policy Engine and never needs raw wallet authority. This split is central to OmniClaw: diff --git a/docs/production-readiness.md b/docs/production-readiness.md index bec1e4a..69c8f8d 100644 --- a/docs/production-readiness.md +++ b/docs/production-readiness.md @@ -2,7 +2,7 @@ This is the short production checklist for OmniClaw. -Use it before publishing a release, running a pilot, or demoing a real external facilitator flow. +Use it before publishing a release, running a pilot, or validating a real external payment flow. ## What OmniClaw Is @@ -13,7 +13,7 @@ It does four things: - inspects what a seller accepts - enforces buyer policy before money moves - routes to a compatible payment rail -- records what happened for operators +- records what happened for audit and operations ## Facilitator Strategy @@ -25,7 +25,7 @@ Supported facilitator paths: - Thirdweb can provide broad gas-sponsored x402 settlement - Circle Gateway can provide batched gasless nanopayments - x402.org or other facilitators can support standard exact settlement -- OmniClaw self-hosted exact facilitator is optional infrastructure for proof, Arc/custom networks, and self-hosted control +- OmniClaw self-hosted exact facilitator is available for Arc, custom networks, and self-hosted control ## Validating Deployment Readiness @@ -40,9 +40,9 @@ For any production environment deployment, we recommend verifying: Ensure this validation checklist is complete before moving to production. -## Buyer Lock +## Buyer Readiness -The buyer path is locked when: +The buyer path is ready when: - `omniclaw-cli can-pay` works - `omniclaw-cli inspect-x402` reports the selected route @@ -51,24 +51,24 @@ The buyer path is locked when: - exact x402 payments use the standard x402 SDK path - Gateway payments require Gateway readiness before selecting `GatewayWalletBatched` -## Seller Lock +## Seller Readiness -The seller path is locked when: +The seller path is ready when: - seller advertises correct x402 requirements - seller does not leak Gateway metadata into non-Gateway exact flows - paid response unlocks only after settlement - settlement status is visible in logs and response metadata -## Facilitator Lock +## Facilitator Strategy -The facilitator strategy is locked as: +Recommended facilitator strategy: - x402.org first for external exact validation on Base Sepolia - Thirdweb next for managed external x402 validation once account access is available - Circle Gateway for batched nanopayments - external exact facilitators where seller requirements support them -- OmniClaw self-hosted exact facilitator only for proof, custom networks, Arc, and self-hosted enterprise deployments +- OmniClaw self-hosted exact facilitator for Arc, custom networks, and self-hosted enterprise deployments Operational split: @@ -76,7 +76,7 @@ Operational split: - facilitator verifies and settles - buyer policy engine decides whether payment is allowed and which route is selected -Keep these layers separate in deployment docs and product claims. +Keep these layers separate in deployment docs, system design, and product claims. ## Current Supported Capabilities @@ -112,8 +112,7 @@ python3 -m py_compile \ python3 scripts/release_verify.sh ``` -If you are validating exact-flow pilot coverage, run the current smoke slice after syncing -dependencies: +If you are validating exact-flow deployment coverage, run the current smoke slice after syncing dependencies: ```bash uv run pytest \ diff --git a/examples/arc-marketplace-showcase/README.md b/examples/arc-marketplace-showcase/README.md new file mode 100644 index 0000000..bd55cfb --- /dev/null +++ b/examples/arc-marketplace-showcase/README.md @@ -0,0 +1,263 @@ +# Arc Marketplace Showcase + +This showcase is a visual vendor marketplace for Arc Testnet settlement. + +It is intentionally not a text-heavy demo. The browser UI represents the vendor as a kiosk with paid services. A buyer agent selects a paid URL, OmniClaw enforces buyer policy, x402 `exact` settles on Arc Testnet, and the vendor unlocks the result. + +## What It Proves + +- A vendor can expose multiple paid services from one marketplace surface. +- The seller advertises standard x402 `exact` requirements for Arc Testnet. +- The buyer pays through OmniClaw policy control instead of raw wallet access. +- The self-hosted OmniClaw exact facilitator verifies and settles on Arc. +- The settlement transaction can be opened on ArcScan. + +## Components + +| Component | Role | +| --- | --- | +| Kiosk vendor app | Marketplace UI and paid product endpoints | +| OmniClaw exact facilitator | Self-hosted x402 `verify` and `settle` service | +| Buyer Financial Policy Engine | Policy-controlled payment executor for OpenClaw or `omniclaw-cli` | +| ArcScan | External proof that settlement happened on Arc Testnet | + +## Standalone Facilitator + +Run this when you only need the self-hosted x402 exact facilitator for Arc: + +```bash +export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKeyWithArcGas" +bash scripts/start_arc_exact_facilitator.sh +``` + +Equivalent installed CLI: + +```bash +omniclaw facilitator exact \ + --network-profile ARC-TESTNET \ + --network eip155:5042002 \ + --rpc-url https://rpc.testnet.arc.network \ + --port 4022 +``` + +The facilitator exposes `GET /supported`, `POST /verify`, and `POST /settle`. + +## Start The Vendor Kiosk + +### Recommended: Docker Clean Slate + +Use the Docker launcher when testing with OpenClaw, Telegram agents, or other containerized buyers. It starts all services on the same Docker bridge network with stable `172.18.0.x` addresses. + +Required env: + +```bash +export BUYER_OMNICLAW_PRIVATE_KEY="0xBuyerKeyWithArcTestnetUSDC" +export SELLER_OMNICLAW_PRIVATE_KEY="0xSellerKey" +export BUYER_CIRCLE_API_KEY="..." +export BUYER_ENTITY_SECRET="..." +``` + +Funding requirements: + +- buyer key: Arc Testnet USDC for the selected product +- seller/facilitator key: Arc Testnet gas for settlement submission + +Start: + +```bash +bash scripts/start_arc_marketplace_showcase_docker.sh +``` + +Default runtime addresses: + +| Service | URL | +| --- | --- | +| Browser UI | `http://127.0.0.1:8020` | +| Facilitator | `http://172.18.0.50:4022` | +| Vendor kiosk | `http://172.18.0.51:8020` | +| Buyer policy engine | `http://172.18.0.52:8080` | + +Paid products: + +| Product | Price | URL | +| --- | --- | --- | +| Prime Market Scan | `$0.25` | `http://172.18.0.51:8020/buy/prime-market-scan` | +| Risk Oracle Brief | `$0.15` | `http://172.18.0.51:8020/buy/risk-oracle-brief` | +| Settlement Receipt Kit | `$0.10` | `http://172.18.0.51:8020/buy/settlement-receipt-kit` | + +OpenClaw config: + +```bash +export OMNICLAW_SERVER_URL="http://172.18.0.52:8080" +export OMNICLAW_TOKEN="payment-agent-token" +``` + +Browser-only flow: + +1. Open `http://127.0.0.1:8020`. +2. Use the `Built-In Buyer Agent` panel. +3. Select a product. +4. Click `Inspect` to verify route, network, buyer readiness, and amount. +5. Click `Pay & Unlock` to execute the policy-controlled x402 payment. +6. Open the returned settlement transaction on ArcScan. + +The browser never receives the policy token. The kiosk backend proxies the action to the buyer Financial Policy Engine configured by `ARC_MARKETPLACE_BUYER_ENGINE_URL` and `ARC_MARKETPLACE_BUYER_TOKEN`. + +OpenClaw prompt: + +```text +pay for this url: http://172.18.0.51:8020/buy/prime-market-scan +``` + +If the buyer has less than `$0.25` Arc Testnet USDC, use: + +```text +pay for this url: http://172.18.0.51:8020/buy/settlement-receipt-kit +``` + +Host-side CLI test: + +```bash +OMNICLAW_SERVER_URL=http://127.0.0.1:8080 \ +OMNICLAW_TOKEN=payment-agent-token \ +omniclaw-cli inspect-x402 \ + --recipient "http://172.18.0.51:8020/buy/prime-market-scan" + +OMNICLAW_SERVER_URL=http://127.0.0.1:8080 \ +OMNICLAW_TOKEN=payment-agent-token \ +omniclaw-cli pay \ + --recipient "http://172.18.0.51:8020/buy/prime-market-scan" \ + --idempotency-key "arc-kiosk-001" +``` + +### Host-Only Local Mode + +Use this only when the buyer also runs on the host. Containerized buyers cannot use `127.0.0.1` for the vendor URL. + +Required seller/facilitator env: + +```bash +export OMNICLAW_PRIVATE_KEY="0xSellerOrFacilitatorKey" +export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKey" +``` + +If both roles use the same funded test key, `OMNICLAW_X402_FACILITATOR_PRIVATE_KEY` can be omitted and the launcher will reuse `OMNICLAW_PRIVATE_KEY`. + +Start the showcase: + +```bash +bash scripts/start_arc_marketplace_showcase.sh +``` + +Open: + +```text +http://127.0.0.1:8020 +``` + +The kiosk displays three vendor services: + +- Prime Market Scan +- Risk Oracle Brief +- Settlement Receipt Kit + +Each card exposes a paid URL for OpenClaw or `omniclaw-cli`. + +## Buyer Flow + +Point the agent CLI at the buyer Financial Policy Engine: + +```bash +export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" +export OMNICLAW_TOKEN="buyer-agent-token" +``` + +Inspect the seller requirements: + +```bash +omniclaw-cli inspect-x402 \ + --recipient "http://127.0.0.1:8020/buy/prime-market-scan" +``` + +Pay: + +```bash +omniclaw-cli pay \ + --recipient "http://127.0.0.1:8020/buy/prime-market-scan" \ + --idempotency-key "arc-kiosk-001" +``` + +OpenClaw prompt: + +```text +pay for this url: http://127.0.0.1:8020/buy/prime-market-scan +``` + +## ArcScan Proof + +The buyer payment response should include the settlement transaction hash. Open it with: + +```text +https://testnet.arcscan.app/tx/ +``` + +Capture these proof assets: + +- kiosk UI before payment +- `inspect-x402` output showing `exact` and Arc `eip155:5042002` +- `pay` output showing settled status and transaction hash +- ArcScan transaction page +- kiosk fulfillment feed after unlock + +Known verified proof transaction: + +```text +https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 +``` + +This transaction shows `transferWithAuthorization` on Arc Testnet USDC. That is expected for standard x402 `exact`: the buyer signs a USDC authorization, the facilitator verifies it, and settlement submits the authorization to the USDC contract. + +## ArcLens Ecosystem Submission + +OmniClaw does not deploy a custom Arc contract for this showcase. The on-chain contract used by the demo is Arc Testnet USDC: + +```text +0x3600000000000000000000000000000000000000 +``` + +If ArcLens asks for a contract address and the field is required, use the Arc Testnet USDC contract above and explain: + +```text +OmniClaw does not require a custom application contract for this demo. The Arc integration settles x402 exact payments through Arc Testnet USDC using transferWithAuthorization. Buyer agents pay vendor services through OmniClaw policy control, and settlement is visible on ArcScan. +``` + +Use this proof transaction in the submission: + +```text +https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 +``` + +## Environment Overrides + +```bash +export ARC_MARKETPLACE_PORT=8020 +export ARC_MARKETPLACE_PUBLIC_BASE_URL="http://127.0.0.1:8020" +export ARC_MARKETPLACE_BUYER_BASE_URL="http://172.18.0.51:8020" +export ARC_MARKETPLACE_BUYER_ENGINE_URL="http://172.18.0.52:8080" +export ARC_MARKETPLACE_BUYER_TOKEN="payment-agent-token" +export ARC_MARKETPLACE_EXPLORER_BASE_URL="https://testnet.arcscan.app/tx/" + +export OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" +export OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="ARC-TESTNET" +export OMNICLAW_X402_FACILITATOR_RPC_URL="https://rpc.testnet.arc.network" +export OMNICLAW_X402_FACILITATOR_NETWORKS="eip155:5042002" +export OMNICLAW_X402_EXACT_FACILITATOR_URL="http://127.0.0.1:4022" +``` + +## Product Framing + +The demo should be explained in one line: + +```text +An agent buys from an Arc vendor kiosk through OmniClaw policy control, and x402 exact settlement is confirmed on ArcScan. +``` diff --git a/examples/arc-marketplace-showcase/app.py b/examples/arc-marketplace-showcase/app.py new file mode 100644 index 0000000..8bdfd2f --- /dev/null +++ b/examples/arc-marketplace-showcase/app.py @@ -0,0 +1,1400 @@ +from __future__ import annotations + +import os +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from math import isqrt +from typing import Any + +import httpx +from eth_account import Account +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import HTMLResponse, JSONResponse +from web3 import Web3 + +from omniclaw.facilitator.networks import ( + build_exact_asset_amount, + resolve_exact_settlement_network_profile, +) +from omniclaw.protocols.x402_compat import patch_x402_web3_compat + +patch_x402_web3_compat() + +from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption # noqa: E402 +from x402.http.middleware.fastapi import PaymentMiddlewareASGI # noqa: E402 +from x402.http.types import RouteConfig # noqa: E402 +from x402.mechanisms.evm.exact import ExactEvmServerScheme # noqa: E402 +from x402.server import x402ResourceServer # noqa: E402 + + +def _env(name: str, default: str = "") -> str: + value = os.environ.get(name, "").strip() + return value or default + + +def _resolve_pay_to() -> str: + explicit = os.environ.get("OMNICLAW_X402_EXACT_PAY_TO", "").strip() + if explicit: + return Web3.to_checksum_address(explicit) + + private_key = os.environ.get("OMNICLAW_PRIVATE_KEY", "").strip() + if private_key: + return Account.from_key(private_key).address + + raise RuntimeError( + "Set OMNICLAW_X402_EXACT_PAY_TO or OMNICLAW_PRIVATE_KEY before starting the seller" + ) + + +APP_PORT = int(_env("ARC_MARKETPLACE_PORT", "8020")) +NETWORK_PROFILE = resolve_exact_settlement_network_profile( + _env("OMNICLAW_X402_EXACT_NETWORK_PROFILE", _env("OMNICLAW_NETWORK", "ARC-TESTNET")) +) +NETWORK = _env("OMNICLAW_X402_EXACT_NETWORK", NETWORK_PROFILE.caip2) +FACILITATOR_URL = _env("OMNICLAW_X402_EXACT_FACILITATOR_URL", "http://127.0.0.1:4022") +PUBLIC_BASE_URL = _env("ARC_MARKETPLACE_PUBLIC_BASE_URL", f"http://127.0.0.1:{APP_PORT}") +BUYER_BASE_URL = _env("ARC_MARKETPLACE_BUYER_BASE_URL", PUBLIC_BASE_URL) +BUYER_ENGINE_URL = _env("ARC_MARKETPLACE_BUYER_ENGINE_URL") +BUYER_ENGINE_TOKEN = _env("ARC_MARKETPLACE_BUYER_TOKEN") +EXPLORER_BASE_URL = _env( + "ARC_MARKETPLACE_EXPLORER_BASE_URL", + NETWORK_PROFILE.explorer_base_url or "https://testnet.arcscan.app/tx/", +) +PAY_TO = _resolve_pay_to() + + +@dataclass(frozen=True) +class KioskProduct: + slug: str + label: str + price: str + lane: str + description: str + endpoint: str + accent: str + + +PRODUCTS = ( + KioskProduct( + slug="prime-market-scan", + label="Prime Market Scan", + price="$0.25", + lane="compute", + description="Runs a deterministic prime-count job for a buyer agent.", + endpoint="/buy/prime-market-scan", + accent="amber", + ), + KioskProduct( + slug="risk-oracle-brief", + label="Risk Oracle Brief", + price="$0.15", + lane="data", + description="Returns a compact vendor-risk signal for an autonomous workflow.", + endpoint="/buy/risk-oracle-brief", + accent="blue", + ), + KioskProduct( + slug="settlement-receipt-kit", + label="Settlement Receipt Kit", + price="$0.10", + lane="proof", + description="Packages the paid response fields needed for an ArcScan proof.", + endpoint="/buy/settlement-receipt-kit", + accent="green", + ), +) + +EVENTS: list[dict[str, Any]] = [] +FULFILLMENTS: list[dict[str, Any]] = [] + + +def _now() -> str: + return datetime.now(UTC).isoformat(timespec="seconds") + + +def _record(stage: str, message: str, *, product: str | None = None) -> None: + EVENTS.insert( + 0, + { + "time": _now(), + "stage": stage, + "message": message, + "product": product, + }, + ) + del EVENTS[80:] + + +def _extract_buyer(request: Request) -> str | None: + """Extract the buyer address from x402 payment state set by the middleware.""" + try: + payload = getattr(request.state, "payment_payload", None) + if payload is None: + return None + + def _search(obj: Any, depth: int = 0) -> str | None: + if depth > 5: + return None + if isinstance(obj, dict): + for key in ("from", "from_address", "payer", "sender", "fromAddress"): + if key in obj and obj[key]: + return str(obj[key]) + for val in obj.values(): + if isinstance(val, (dict, list)): + result = _search(val, depth + 1) + if result: + return result + elif isinstance(obj, (list, tuple)): + for item in obj: + result = _search(item, depth + 1) + if result: + return result + elif hasattr(obj, "__dict__"): + for attr_name in ("from_address", "payer", "sender", "fromAddress"): + val = getattr(obj, attr_name, None) + if val: + return str(val) + for val in vars(obj).values(): + if isinstance(val, (dict, list)) or hasattr(val, "__dict__"): + result = _search(val, depth + 1) + if result: + return result + return None + + if hasattr(payload, "to_dict"): + as_dict = payload.to_dict() + result = _search(as_dict) + if result: + return result + if hasattr(payload, "model_dump"): + as_dict = payload.model_dump() + result = _search(as_dict) + if result: + return result + + if isinstance(payload, dict): + return _search(payload) + + if hasattr(payload, "__dict__"): + return _search(payload) + + return None + except Exception: + return None + + +def _prime_count(limit: int) -> int: + if limit < 2: + return 0 + sieve = bytearray(b"\x01") * (limit + 1) + sieve[0:2] = b"\x00\x00" + for value in range(2, isqrt(limit) + 1): + if sieve[value]: + start = value * value + sieve[start : limit + 1 : value] = b"\x00" * (((limit - start) // value) + 1) + return int(sum(sieve)) + + +def _product_by_slug(slug: str) -> KioskProduct: + for product in PRODUCTS: + if product.slug == slug: + return product + raise KeyError(slug) + + +def _paid_url(product: KioskProduct, *, public: bool = False) -> str: + base = PUBLIC_BASE_URL if public else BUYER_BASE_URL + return f"{base.rstrip('/')}{product.endpoint}" + + +app = FastAPI(title="OmniClaw Arc Marketplace Showcase") + +facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL)) +server = x402ResourceServer(facilitator) +exact_scheme = ExactEvmServerScheme() +exact_scheme.register_money_parser( + lambda amount, network: build_exact_asset_amount( + profile=NETWORK_PROFILE, + decimal_amount=amount, + network=str(network), + ) +) +server.register("eip155:*", exact_scheme) + +routes = { + f"GET {product.endpoint}": RouteConfig( + accepts=[ + PaymentOption( + scheme="exact", + price=product.price, + network=NETWORK, + pay_to=PAY_TO, + ) + ], + description=f"{product.label} on {NETWORK_PROFILE.label}", + mime_type="application/json", + ) + for product in PRODUCTS +} +app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) + + +@app.on_event("startup") +async def startup() -> None: + _record("kiosk", f"Arc marketplace online with {len(PRODUCTS)} paid vendor services") + + +@app.get("/", response_class=HTMLResponse) +async def index() -> str: + return HTML + + +@app.get("/api/catalog") +async def catalog() -> dict[str, Any]: + return { + "network_profile": NETWORK_PROFILE.label, + "network": NETWORK, + "asset": NETWORK_PROFILE.default_asset_address, + "asset_symbol": NETWORK_PROFILE.default_asset_name, + "pay_to": PAY_TO, + "facilitator_url": FACILITATOR_URL, + "buyer_engine_configured": bool(BUYER_ENGINE_URL and BUYER_ENGINE_TOKEN), + "buyer_engine_url": BUYER_ENGINE_URL, + "explorer_base_url": EXPLORER_BASE_URL, + "products": [ + { + **asdict(product), + "pay_url": _paid_url(product), + "public_pay_url": _paid_url(product, public=True), + } + for product in PRODUCTS + ], + } + + +async def _call_buyer_engine(path: str, payload: dict[str, Any]) -> dict[str, Any]: + if not BUYER_ENGINE_URL or not BUYER_ENGINE_TOKEN: + return { + "ok": False, + "status_code": 503, + "error": "Buyer Financial Policy Engine is not configured for this kiosk.", + } + + url = f"{BUYER_ENGINE_URL.rstrip('/')}{path}" + headers = {"Authorization": f"Bearer {BUYER_ENGINE_TOKEN}"} + try: + async with httpx.AsyncClient(timeout=90.0) as client: + response = await client.post(url, json=payload, headers=headers) + try: + data: Any = response.json() + except Exception: + data = {"raw": response.text} + return { + "ok": 200 <= response.status_code < 300, + "status_code": response.status_code, + "data": data, + } + except Exception as exc: + return {"ok": False, "status_code": 502, "error": str(exc)} + + +@app.post("/api/agent/inspect/{slug}") +async def mini_agent_inspect(slug: str) -> dict[str, Any]: + try: + product = _product_by_slug(slug) + except KeyError as exc: + raise HTTPException(status_code=404, detail="Unknown product") from exc + + _record("buyer-agent", f"Mini buyer agent inspecting {product.label}", product=product.slug) + return await _call_buyer_engine( + "/api/v1/x402/inspect", + { + "url": _paid_url(product), + "method": "GET", + }, + ) + + +@app.post("/api/agent/pay/{slug}") +async def mini_agent_pay(slug: str) -> dict[str, Any]: + try: + product = _product_by_slug(slug) + except KeyError as exc: + raise HTTPException(status_code=404, detail="Unknown product") from exc + + idempotency_key = f"arc-ui-{product.slug}-{datetime.now(UTC).strftime('%Y%m%d%H%M%S%f')}" + _record("buyer-agent", f"Mini buyer agent paying {product.label}", product=product.slug) + result = await _call_buyer_engine( + "/api/v1/pay", + { + "recipient": _paid_url(product), + "idempotency_key": idempotency_key, + "purpose": f"Arc marketplace showcase purchase: {product.label}", + "method": "GET", + }, + ) + data = result.get("data") + if result.get("ok") and isinstance(data, dict) and data.get("success"): + tx = data.get("blockchain_tx") or data.get("transaction_id") + tx_text = str(tx) + if tx_text and not tx_text.startswith("0x"): + tx_text = f"0x{tx_text}" + suffix = f" ({tx_text[:10]}...)" if tx else "" + _record("buyer-agent", f"Mini buyer agent settled {product.label}{suffix}", product=slug) + else: + message = "" + if isinstance(data, dict): + message = str(data.get("error") or data.get("detail") or "") + _record( + "buyer-agent", + f"Mini buyer agent could not pay {product.label}: {message or result.get('error')}", + product=slug, + ) + return result + + +@app.get("/api/events") +async def events() -> dict[str, Any]: + total_revenue = sum(float(f["price"].strip("$")) for f in FULFILLMENTS) + return { + "events": EVENTS, + "fulfillments": FULFILLMENTS[:20], + "revenue_usdc": f"{total_revenue:.2f}", + "total_settlements": len(FULFILLMENTS), + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "explorer_base_url": EXPLORER_BASE_URL, + "pay_to": PAY_TO, + "asset_symbol": NETWORK_PROFILE.default_asset_name, + } + + +@app.get("/buy/prime-market-scan") +async def buy_prime_market_scan(request: Request) -> JSONResponse: + product = _product_by_slug("prime-market-scan") + buyer = _extract_buyer(request) + result = { + "service": "arc-marketplace-kiosk", + "product": product.slug, + "label": product.label, + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "result": { + "job": "prime-count", + "input": {"size": 70000}, + "prime_count": _prime_count(70000), + }, + "proof": _proof_fields(product), + } + _fulfill(product, result, buyer_address=buyer) + return JSONResponse(result) + + +@app.get("/buy/risk-oracle-brief") +async def buy_risk_oracle_brief(request: Request) -> JSONResponse: + product = _product_by_slug("risk-oracle-brief") + buyer = _extract_buyer(request) + result = { + "service": "arc-marketplace-kiosk", + "product": product.slug, + "label": product.label, + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "result": { + "vendor_score": 92, + "policy_signal": "allow-listed vendor, bounded spend, exact settlement", + "recommended_action": "fulfill request", + }, + "proof": _proof_fields(product), + } + _fulfill(product, result, buyer_address=buyer) + return JSONResponse(result) + + +@app.get("/buy/settlement-receipt-kit") +async def buy_settlement_receipt_kit(request: Request) -> JSONResponse: + product = _product_by_slug("settlement-receipt-kit") + buyer = _extract_buyer(request) + result = { + "service": "arc-marketplace-kiosk", + "product": product.slug, + "label": product.label, + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "result": { + "receipt_fields": [ + "seller_url", + "payment_scheme", + "network", + "pay_to", + "asset", + "settlement_tx", + "arcscan_url", + ], + "message": "Use the settlement transaction returned by the buyer CLI to open ArcScan.", + }, + "proof": _proof_fields(product), + } + _fulfill(product, result, buyer_address=buyer) + return JSONResponse(result) + + +def _proof_fields(product: KioskProduct) -> dict[str, Any]: + return { + "seller_url": _paid_url(product), + "scheme": "exact", + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "asset": NETWORK_PROFILE.default_asset_address, + "asset_symbol": NETWORK_PROFILE.default_asset_name, + "pay_to": PAY_TO, + "facilitator": FACILITATOR_URL, + "explorer_base_url": EXPLORER_BASE_URL, + "arcscan_note": "Append the settlement transaction hash returned by the buyer to explorer_base_url.", + } + + +def _fulfill( + product: KioskProduct, payload: dict[str, Any], *, buyer_address: str | None = None +) -> None: + record = { + "time": _now(), + "slug": product.slug, + "label": product.label, + "price": product.price, + "lane": product.lane, + "accent": product.accent, + "network": NETWORK, + "network_profile": NETWORK_PROFILE.label, + "asset_symbol": NETWORK_PROFILE.default_asset_name, + "pay_to": PAY_TO, + "buyer_address": buyer_address, + "scheme": "exact", + "explorer_base_url": EXPLORER_BASE_URL, + } + FULFILLMENTS.insert(0, record) + del FULFILLMENTS[40:] + buyer_label = f" by {buyer_address[:10]}…" if buyer_address else "" + _record( + "fulfilled", + f"{product.label} unlocked{buyer_label} after x402 exact settlement", + product=product.slug, + ) + + +HTML = """ + + + + + + OmniClaw Arc Kiosk — Agent Marketplace + + + + + + + +
+ + + + +
+

Vendor services
for autonomous agents.

+

A buyer agent selects a service, OmniClaw enforces policy constraints, x402 settles on-chain, and the vendor unlocks the result. Every step is verifiable.

+
+
🛒 Select Service
+ +
🛡️ Policy Check
+ +
💳 x402 Payment
+ +
⛓️ On-Chain Settlement
+ +
Fulfill & Verify
+
+
+ + +
+
Total Revenue
$0.00
+
Settlements
0
+
Network
+
Asset
+
+ + +
Paid Vendor Services
+
+ + +
Built-In Buyer Agent
+
+
+
🤖
+
+

Mini buyer agent

+

Run the whole Arc showcase from this page. The browser asks the kiosk backend, the backend calls the buyer Financial Policy Engine, policy is enforced, and settlement still happens through x402 exact on Arc.

+
+
+ +
+ + +
+
+
+
+
+
Buyer Agent Console
+
+
+ Select a product, inspect the x402 requirement, then pay. No OpenClaw prompt is required for this browser demo. +
+
+
+ + +
+
+
+
Agent Commands
+
+
+
+
+
+
Settlement Proof Surface
+
+
+
+
+ + + + + +
+
Live Event Feed
+
+
+
+
+ +
+ OmniClaw — Programmable agent economy infrastructure. + Settlement verified on ArcScan. +
+
+ +
Copied
+ + + + +""" diff --git a/scripts/start_arc_exact_facilitator.sh b/scripts/start_arc_exact_facilitator.sh new file mode 100755 index 0000000..b983d86 --- /dev/null +++ b/scripts/start_arc_exact_facilitator.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +if [[ -f .env ]]; then + set -a + source .env + set +a +fi + +export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="${OMNICLAW_X402_FACILITATOR_PRIVATE_KEY:-${SELLER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY:-}}}" +export OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="${OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE:-ARC-TESTNET}" +export OMNICLAW_X402_FACILITATOR_RPC_URL="${OMNICLAW_X402_FACILITATOR_RPC_URL:-https://rpc.testnet.arc.network}" +export OMNICLAW_X402_FACILITATOR_NETWORKS="${OMNICLAW_X402_FACILITATOR_NETWORKS:-eip155:5042002}" +export OMNICLAW_X402_FACILITATOR_PORT="${OMNICLAW_X402_FACILITATOR_PORT:-4022}" +export OMNICLAW_X402_FACILITATOR_HOST="${OMNICLAW_X402_FACILITATOR_HOST:-0.0.0.0}" + +if [[ -z "$OMNICLAW_X402_FACILITATOR_PRIVATE_KEY" ]]; then + echo "Missing facilitator key. Set OMNICLAW_X402_FACILITATOR_PRIVATE_KEY, SELLER_OMNICLAW_PRIVATE_KEY, or OMNICLAW_PRIVATE_KEY." + exit 1 +fi + +cat </dev/null 2>&1 || true + fi + if [[ -f "$RUNTIME_DIR/kiosk.pid" ]]; then + kill "$(cat "$RUNTIME_DIR/kiosk.pid")" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +cleanup + +uv run python scripts/start_x402_exact_testnet_facilitator.py \ + >"$RUNTIME_DIR/facilitator.log" 2>&1 & +echo "$!" > "$RUNTIME_DIR/facilitator.pid" + +sleep 2 + +uv run uvicorn app:app \ + --app-dir examples/arc-marketplace-showcase \ + --host 0.0.0.0 \ + --port "$ARC_MARKETPLACE_PORT" \ + >"$RUNTIME_DIR/kiosk.log" 2>&1 & +echo "$!" > "$RUNTIME_DIR/kiosk.pid" + +sleep 2 + +cat < + +Services: + Kiosk UI: http://127.0.0.1:$ARC_MARKETPLACE_PORT + Facilitator: $OMNICLAW_X402_EXACT_FACILITATOR_URL + +Paid URLs: + $ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan + $ARC_MARKETPLACE_BUYER_BASE_URL/buy/risk-oracle-brief + $ARC_MARKETPLACE_BUYER_BASE_URL/buy/settlement-receipt-kit + +OpenClaw prompt: + pay for this url: $ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan + +Buyer CLI equivalent: + omniclaw-cli inspect-x402 --recipient "$ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan" + omniclaw-cli pay --recipient "$ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan" --idempotency-key "arc-kiosk-\$(date +%s)" + +Logs: + tail -f $RUNTIME_DIR/facilitator.log + tail -f $RUNTIME_DIR/kiosk.log + +Press Ctrl+C to stop both services. + +EOF + +tail -f "$RUNTIME_DIR/facilitator.log" "$RUNTIME_DIR/kiosk.log" diff --git a/scripts/start_arc_marketplace_showcase_docker.sh b/scripts/start_arc_marketplace_showcase_docker.sh new file mode 100755 index 0000000..ff6ad97 --- /dev/null +++ b/scripts/start_arc_marketplace_showcase_docker.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +if [[ -f .env ]]; then + set -a + source .env + set +a +fi + +IMAGE_TAG="${OMNICLAW_AGENT_IMAGE:-omniclaw-agent:local}" +NETWORK_NAME="${ARC_MARKETPLACE_DOCKER_NETWORK:-omniclaw-buyer_default}" +SUBNET="${ARC_MARKETPLACE_DOCKER_SUBNET:-172.18.0.0/16}" +FACILITATOR_IP="${ARC_MARKETPLACE_FACILITATOR_IP:-172.18.0.50}" +KIOSK_IP="${ARC_MARKETPLACE_KIOSK_IP:-172.18.0.51}" +BUYER_IP="${ARC_MARKETPLACE_BUYER_IP:-172.18.0.52}" +ARC_RPC_URL="${ARC_MARKETPLACE_RPC_URL:-https://rpc.testnet.arc.network}" +ARC_USDC_ADDRESS="${ARC_MARKETPLACE_USDC_ADDRESS:-0x3600000000000000000000000000000000000000}" +SELLER_KEY="${SELLER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_X402_FACILITATOR_PRIVATE_KEY:-}}" +BUYER_KEY="${BUYER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY:-}}" +BUYER_TOKEN="${ARC_MARKETPLACE_BUYER_TOKEN:-payment-agent-token}" +POLICY_PATH="$ROOT/.runtime/arc-marketplace-showcase/buyer.policy.json" + +if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then + echo "Missing Docker image $IMAGE_TAG" + echo "Build it with: DOCKER_BUILDKIT=0 docker build -t $IMAGE_TAG -f Dockerfile.agent ." + exit 1 +fi + +if [[ -z "$SELLER_KEY" ]]; then + echo "Missing SELLER_OMNICLAW_PRIVATE_KEY or OMNICLAW_X402_FACILITATOR_PRIVATE_KEY" + exit 1 +fi + +if [[ -z "$BUYER_KEY" ]]; then + echo "Missing BUYER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" + exit 1 +fi + +if [[ -z "${BUYER_CIRCLE_API_KEY:-${CIRCLE_API_KEY:-}}" ]]; then + echo "Missing BUYER_CIRCLE_API_KEY or CIRCLE_API_KEY" + exit 1 +fi + +if [[ -z "${BUYER_ENTITY_SECRET:-${ENTITY_SECRET:-}}" ]]; then + echo "Missing BUYER_ENTITY_SECRET or ENTITY_SECRET" + exit 1 +fi + +if ! docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then + docker network create --subnet "$SUBNET" "$NETWORK_NAME" >/dev/null +fi + +wait_for_http() { + local label="$1" + local url="$2" + local container="$3" + local deadline=$((SECONDS + 180)) + + while ((SECONDS < deadline)); do + if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + + echo "Timed out waiting for $label at $url" + echo "Recent $container logs:" + docker logs --tail 80 "$container" || true + return 1 +} + +mkdir -p "$(dirname "$POLICY_PATH")" + +SELLER_ADDR=$(SELLER_KEY="$SELLER_KEY" uv run python - <<'PY' +from eth_account import Account +import os +print(Account.from_key(os.environ["SELLER_KEY"]).address) +PY +) + +BUYER_ADDR=$(BUYER_KEY="$BUYER_KEY" uv run python - <<'PY' +from eth_account import Account +import os +print(Account.from_key(os.environ["BUYER_KEY"]).address) +PY +) + +BUYER_USDC_BALANCE=$(BUYER_ADDR="$BUYER_ADDR" ARC_RPC_URL="$ARC_RPC_URL" ARC_USDC_ADDRESS="$ARC_USDC_ADDRESS" uv run python - <<'PY' +import os +from decimal import Decimal + +try: + from web3 import Web3 + + w3 = Web3(Web3.HTTPProvider(os.environ["ARC_RPC_URL"], request_kwargs={"timeout": 5})) + token = w3.eth.contract( + address=Web3.to_checksum_address(os.environ["ARC_USDC_ADDRESS"]), + abi=[ + { + "inputs": [{"name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"type": "uint256"}], + "stateMutability": "view", + "type": "function", + } + ], + ) + balance = token.functions.balanceOf(Web3.to_checksum_address(os.environ["BUYER_ADDR"])).call() + print(Decimal(balance) / Decimal(10**6)) +except Exception: + print("unknown") +PY +) + +SELLER_NATIVE_BALANCE=$(SELLER_ADDR="$SELLER_ADDR" ARC_RPC_URL="$ARC_RPC_URL" uv run python - <<'PY' +import os +from decimal import Decimal + +try: + from web3 import Web3 + + w3 = Web3(Web3.HTTPProvider(os.environ["ARC_RPC_URL"], request_kwargs={"timeout": 5})) + balance = w3.eth.get_balance(Web3.to_checksum_address(os.environ["SELLER_ADDR"])) + print(Decimal(balance) / Decimal(10**18)) +except Exception: + print("unknown") +PY +) + +BUYER_ADDR="$BUYER_ADDR" BUYER_TOKEN="$BUYER_TOKEN" KIOSK_IP="$KIOSK_IP" python3 - <<'PY' +import json +import os +from pathlib import Path + +policy = { + "version": "2.0", + "tokens": { + os.environ["BUYER_TOKEN"]: { + "wallet_alias": "payment-agent", + "active": True, + "label": "Arc Marketplace Buyer Agent", + } + }, + "wallets": { + "payment-agent": { + "name": "Arc Marketplace Buyer Agent", + "address": os.environ["BUYER_ADDR"], + "limits": { + "daily_max": "10.00", + "hourly_max": "5.00", + "per_tx_max": "1.00", + "per_tx_min": "0.01", + }, + "rate_limits": {"per_minute": 10, "per_hour": 100}, + "recipients": { + "mode": "whitelist", + "addresses": [], + "domains": [ + "localhost", + "127.0.0.1", + os.environ["KIOSK_IP"], + "omniclaw-arc-kiosk", + ], + }, + "confirm_threshold": None, + } + }, +} + +path = Path(".runtime/arc-marketplace-showcase/buyer.policy.json") +path.write_text(json.dumps(policy, indent=2) + "\n") +PY + +docker rm -f omniclaw-arc-facilitator omniclaw-arc-kiosk omniclaw-arc-buyer >/dev/null 2>&1 || true + +docker run -d \ + --name omniclaw-arc-facilitator \ + --network "$NETWORK_NAME" \ + --ip "$FACILITATOR_IP" \ + -p 4022:4022 \ + -v "$ROOT:/workspace" \ + -w /workspace \ + -e OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="$SELLER_KEY" \ + -e OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="ARC-TESTNET" \ + -e OMNICLAW_X402_FACILITATOR_RPC_URL="$ARC_RPC_URL" \ + -e OMNICLAW_X402_FACILITATOR_NETWORKS="eip155:5042002" \ + -e OMNICLAW_X402_FACILITATOR_PORT="4022" \ + -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-facilitator-venv" \ + "$IMAGE_TAG" \ + sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run python scripts/start_x402_exact_testnet_facilitator.py' >/dev/null + +docker run -d \ + --name omniclaw-arc-kiosk \ + --network "$NETWORK_NAME" \ + --ip "$KIOSK_IP" \ + -p 8020:8020 \ + -v "$ROOT:/workspace" \ + -w /workspace \ + -e OMNICLAW_X402_EXACT_PAY_TO="$SELLER_ADDR" \ + -e OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" \ + -e OMNICLAW_X402_EXACT_NETWORK="eip155:5042002" \ + -e OMNICLAW_X402_EXACT_FACILITATOR_URL="http://$FACILITATOR_IP:4022" \ + -e ARC_MARKETPLACE_PORT="8020" \ + -e ARC_MARKETPLACE_PUBLIC_BASE_URL="http://127.0.0.1:8020" \ + -e ARC_MARKETPLACE_BUYER_BASE_URL="http://$KIOSK_IP:8020" \ + -e ARC_MARKETPLACE_BUYER_ENGINE_URL="http://$BUYER_IP:8080" \ + -e ARC_MARKETPLACE_BUYER_TOKEN="$BUYER_TOKEN" \ + -e ARC_MARKETPLACE_EXPLORER_BASE_URL="https://testnet.arcscan.app/tx/" \ + -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-kiosk-venv" \ + "$IMAGE_TAG" \ + sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run uvicorn app:app --app-dir examples/arc-marketplace-showcase --host 0.0.0.0 --port 8020' >/dev/null + +docker run -d \ + --name omniclaw-arc-buyer \ + --network "$NETWORK_NAME" \ + --ip "$BUYER_IP" \ + -p 8080:8080 \ + -v "$ROOT:/workspace" \ + -w /workspace \ + -e CIRCLE_API_KEY="${BUYER_CIRCLE_API_KEY:-$CIRCLE_API_KEY}" \ + -e ENTITY_SECRET="${BUYER_ENTITY_SECRET:-$ENTITY_SECRET}" \ + -e OMNICLAW_PRIVATE_KEY="$BUYER_KEY" \ + -e OMNICLAW_NETWORK="ARC-TESTNET" \ + -e OMNICLAW_RPC_URL="$ARC_RPC_URL" \ + -e OMNICLAW_AGENT_POLICY_PATH="/workspace/.runtime/arc-marketplace-showcase/buyer.policy.json" \ + -e OMNICLAW_AGENT_TOKEN="$BUYER_TOKEN" \ + -e OMNICLAW_STORAGE_BACKEND="memory" \ + -e OMNICLAW_POLICY_RELOAD_INTERVAL="0" \ + -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-buyer-venv" \ + "$IMAGE_TAG" \ + sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 8080 --log-level info' >/dev/null + +wait_for_http "facilitator" "http://127.0.0.1:4022/supported" "omniclaw-arc-facilitator" +wait_for_http "vendor kiosk" "http://127.0.0.1:8020/api/catalog" "omniclaw-arc-kiosk" +wait_for_http "buyer policy engine" "http://127.0.0.1:8080/api/v1/health" "omniclaw-arc-buyer" + +printf '\nOmniClaw Arc Marketplace Docker showcase is ready.\n\n' +printf 'Network: %s\n' "$NETWORK_NAME" +printf 'Facilitator: http://%s:4022\n' "$FACILITATOR_IP" +printf 'Vendor kiosk: http://%s:8020\n' "$KIOSK_IP" +printf 'Buyer engine: http://%s:8080\n' "$BUYER_IP" +printf 'Browser UI: http://127.0.0.1:8020\n' +printf 'Buyer address: %s\n' "$BUYER_ADDR" +printf 'Seller address: %s\n' "$SELLER_ADDR" +printf 'Buyer Arc USDC: %s\n' "$BUYER_USDC_BALANCE" +printf 'Seller Arc gas: %s\n' "$SELLER_NATIVE_BALANCE" +printf '\nPaid products:\n' +printf ' Prime Market Scan: $0.25 http://%s:8020/buy/prime-market-scan\n' "$KIOSK_IP" +printf ' Risk Oracle Brief: $0.15 http://%s:8020/buy/risk-oracle-brief\n' "$KIOSK_IP" +printf ' Settlement Receipt Kit: $0.10 http://%s:8020/buy/settlement-receipt-kit\n' "$KIOSK_IP" +printf '\nOpenClaw config:\n' +printf ' OMNICLAW_SERVER_URL=http://%s:8080\n' "$BUYER_IP" +printf ' OMNICLAW_TOKEN=%s\n' "$BUYER_TOKEN" +printf '\nOpenClaw prompt:\n' +printf ' pay for this url: http://%s:8020/buy/prime-market-scan\n' "$KIOSK_IP" +printf '\nCLI test:\n' +printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli inspect-x402 --recipient "http://%s:8020/buy/prime-market-scan"\n' "$BUYER_TOKEN" "$KIOSK_IP" +printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli pay --recipient "http://%s:8020/buy/prime-market-scan" --idempotency-key "arc-kiosk-$(date +%%s)"\n' "$BUYER_TOKEN" "$KIOSK_IP" +printf '\nIf buyer Arc USDC is below $0.25, test the $0.10 endpoint first:\n' +printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli pay --recipient "http://%s:8020/buy/settlement-receipt-kit" --idempotency-key "arc-kiosk-$(date +%%s)"\n' "$BUYER_TOKEN" "$KIOSK_IP" +printf '\nLogs:\n' +printf ' docker logs -f omniclaw-arc-facilitator\n' +printf ' docker logs -f omniclaw-arc-kiosk\n' +printf ' docker logs -f omniclaw-arc-buyer\n\n' diff --git a/src/omniclaw/facilitator/exact.py b/src/omniclaw/facilitator/exact.py index ea0dcbe..5596f28 100644 --- a/src/omniclaw/facilitator/exact.py +++ b/src/omniclaw/facilitator/exact.py @@ -36,6 +36,11 @@ def _required_env(*names: str) -> str: raise RuntimeError(f"Missing required environment variable. Set one of: {missing}") +def _normalize_tx_hash(tx_hash: Any) -> str: + value = tx_hash.hex() if hasattr(tx_hash, "hex") else str(tx_hash) + return value if value.startswith("0x") else f"0x{value}" + + @dataclass(frozen=True) class ExactFacilitatorConfig: private_key: str @@ -74,7 +79,7 @@ def write_contract( ) signed_tx = self._account.sign_transaction(tx) tx_hash = self._w3.eth.send_raw_transaction(get_signed_raw_transaction_bytes(signed_tx)) - return tx_hash.hex() + return _normalize_tx_hash(tx_hash) def send_transaction(self, to: str, data: bytes) -> str: from web3 import Web3 @@ -89,7 +94,7 @@ def send_transaction(self, to: str, data: bytes) -> str: } signed_tx = self._account.sign_transaction(tx) tx_hash = self._w3.eth.send_raw_transaction(get_signed_raw_transaction_bytes(signed_tx)) - return tx_hash.hex() + return _normalize_tx_hash(tx_hash) class FacilitatorRequest(BaseModel): diff --git a/src/omniclaw/protocols/x402.py b/src/omniclaw/protocols/x402.py index c020503..84ac425 100644 --- a/src/omniclaw/protocols/x402.py +++ b/src/omniclaw/protocols/x402.py @@ -476,6 +476,15 @@ def _decode_response_body(response: httpx.Response) -> Any: except Exception: return response.text + @staticmethod + def _requirement_value(requirements: Any, *names: str) -> Any: + for name in names: + if isinstance(requirements, dict) and name in requirements: + return requirements[name] + if hasattr(requirements, name): + return getattr(requirements, name) + return None + def _resolve_agent_network( self, wallet_id: str, @@ -535,6 +544,99 @@ async def _capture_selection(ctx: Any) -> None: x402_client.on_after_payment_creation(_capture_selection) return x402HTTPClient(x402_client), selection_state + def _resolve_exact_balance_rpc_url(self, selected_network: Network | None) -> str | None: + config_rpc_url = getattr(self._config, "rpc_url", None) + config_network = _resolve_network(str(getattr(self._config, "network", "") or "")) + if config_rpc_url and (selected_network is None or config_network == selected_network): + return str(config_rpc_url) + + if selected_network is None: + return str(config_rpc_url) if config_rpc_url else None + + try: + from omniclaw.facilitator.networks import resolve_exact_settlement_network_profile + + profile = resolve_exact_settlement_network_profile(selected_network) + return profile.default_rpc_url + except Exception: + return str(config_rpc_url) if config_rpc_url else None + + def _check_direct_exact_balance(self, selected_requirements: Any) -> dict[str, Any]: + """ + Best-effort direct-wallet token balance check for x402 exact payments. + + This is used by simulate/inspect only. It prevents readiness reports from + claiming a buyer is ready when the signer can produce a payload but the + wallet cannot actually settle the selected requirement. + """ + if os.environ.get("OMNICLAW_X402_BALANCE_CHECK", "true").lower() in { + "0", + "false", + "no", + "off", + }: + return {"balance_check": "skipped", "balance_check_reason": "disabled"} + + network_value = str(self._requirement_value(selected_requirements, "network") or "") + asset = self._requirement_value(selected_requirements, "asset") + amount_value = self._requirement_value(selected_requirements, "amount") + selected_network = _resolve_network(network_value) + if not selected_network or not asset or amount_value is None: + return { + "balance_check": "skipped", + "balance_check_reason": "missing selected network, asset, or amount", + } + + rpc_url = self._resolve_exact_balance_rpc_url(selected_network) + if not rpc_url: + return { + "balance_check": "skipped", + "balance_check_reason": f"no RPC URL configured for {network_value}", + } + + try: + private_key = self._get_generic_x402_private_key() + + from eth_account import Account + from web3 import Web3 + + account = Account.from_key(private_key) + w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 5})) + token = w3.eth.contract( + address=Web3.to_checksum_address(str(asset)), + abi=[ + { + "inputs": [{"name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"type": "uint256"}], + "stateMutability": "view", + "type": "function", + } + ], + ) + balance_atomic = int( + token.functions.balanceOf(Web3.to_checksum_address(account.address)).call() + ) + required_atomic = int(str(amount_value)) + balance_decimal = self._atomic_to_decimal(str(balance_atomic)) + required_decimal = self._atomic_to_decimal(str(required_atomic)) + has_enough = balance_atomic >= required_atomic + return { + "balance_check": "passed" if has_enough else "failed", + "buyer_address": account.address, + "direct_wallet_balance_atomic": str(balance_atomic), + "direct_wallet_balance": str(balance_decimal), + "direct_wallet_required_atomic": str(required_atomic), + "direct_wallet_required": str(required_decimal), + "direct_wallet_has_enough": has_enough, + "direct_wallet_rpc_url": rpc_url, + } + except Exception as exc: + return { + "balance_check": "skipped", + "balance_check_reason": f"balance read failed: {exc}", + } + async def execute( self, wallet_id: str, @@ -784,10 +886,27 @@ async def simulate( result["payment_network"] = str(selected_requirements.network) result["payment_asset"] = str(selected_requirements.asset) result["scheme"] = str(selected_requirements.scheme) - result["reason"] = ( - "Buyer can create a valid x402 payment payload. " - "Execution still depends on seller-side settlement and on-chain balance." - ) + balance_check = self._check_direct_exact_balance(selected_requirements) + result.update(balance_check) + if balance_check.get("balance_check") == "failed": + result["would_succeed"] = False + result["reason"] = ( + "Buyer can create a valid x402 payment payload, but the direct wallet " + f"has {balance_check.get('direct_wallet_balance')} USDC and needs " + f"{balance_check.get('direct_wallet_required')} USDC on " + f"{selected_requirements.network}." + ) + elif balance_check.get("balance_check") == "passed": + result["reason"] = ( + "Buyer can create a valid x402 payment payload and has enough direct " + "wallet USDC for the selected requirement." + ) + else: + result["reason"] = ( + "Buyer can create a valid x402 payment payload. " + "Direct wallet balance check was skipped; execution still depends on " + "seller-side settlement and on-chain balance." + ) except Exception as e: result["would_succeed"] = False diff --git a/tests/test_arc_marketplace_showcase.py b/tests/test_arc_marketplace_showcase.py new file mode 100644 index 0000000..b10d182 --- /dev/null +++ b/tests/test_arc_marketplace_showcase.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path +from uuid import uuid4 + +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parents[1] +SHOWCASE_APP = ROOT / "examples" / "arc-marketplace-showcase" / "app.py" +DUMMY_PRIVATE_KEY = "0x" + "11" * 32 + + +def _load_showcase_module(monkeypatch): + monkeypatch.setenv("OMNICLAW_PRIVATE_KEY", DUMMY_PRIVATE_KEY) + monkeypatch.delenv("OMNICLAW_X402_EXACT_PAY_TO", raising=False) + monkeypatch.setenv("OMNICLAW_X402_EXACT_NETWORK_PROFILE", "ARC-TESTNET") + monkeypatch.setenv("OMNICLAW_X402_EXACT_FACILITATOR_URL", "http://127.0.0.1:4022") + monkeypatch.setenv("ARC_MARKETPLACE_PUBLIC_BASE_URL", "http://127.0.0.1:8020") + monkeypatch.setenv("ARC_MARKETPLACE_BUYER_BASE_URL", "http://buyer.local:8020") + + module_name = f"arc_showcase_{uuid4().hex}" + spec = importlib.util.spec_from_file_location(module_name, SHOWCASE_APP) + assert spec is not None + assert spec.loader is not None + + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def test_arc_marketplace_catalog_uses_arc_exact_profile(monkeypatch): + module = _load_showcase_module(monkeypatch) + + with TestClient(module.app) as client: + response = client.get("/api/catalog") + + assert response.status_code == 200 + catalog = response.json() + assert catalog["network_profile"] == "ARC-TESTNET" + assert catalog["network"] == "eip155:5042002" + assert catalog["asset"] == "0x3600000000000000000000000000000000000000" + assert catalog["facilitator_url"] == "http://127.0.0.1:4022" + assert catalog["explorer_base_url"] == "https://testnet.arcscan.app/tx/" + assert catalog["buyer_engine_configured"] is False + assert [product["slug"] for product in catalog["products"]] == [ + "prime-market-scan", + "risk-oracle-brief", + "settlement-receipt-kit", + ] + assert catalog["products"][0]["pay_url"] == "http://buyer.local:8020/buy/prime-market-scan" + + +def test_arc_marketplace_paid_routes_advertise_arc_exact(monkeypatch): + module = _load_showcase_module(monkeypatch) + + route = module.routes["GET /buy/prime-market-scan"] + payment_option = route.accepts[0] + + assert payment_option.scheme == "exact" + assert payment_option.price == "$0.25" + assert payment_option.network == "eip155:5042002" + assert payment_option.pay_to == module.PAY_TO + + +def test_arc_marketplace_mini_agent_reports_missing_buyer_engine(monkeypatch): + module = _load_showcase_module(monkeypatch) + + with TestClient(module.app) as client: + response = client.post("/api/agent/inspect/prime-market-scan") + + assert response.status_code == 200 + body = response.json() + assert body["ok"] is False + assert body["status_code"] == 503 + assert "Buyer Financial Policy Engine" in body["error"] diff --git a/tests/test_exact_facilitator_app.py b/tests/test_exact_facilitator_app.py index 166dafb..e30c749 100644 --- a/tests/test_exact_facilitator_app.py +++ b/tests/test_exact_facilitator_app.py @@ -1,10 +1,12 @@ from __future__ import annotations -from types import SimpleNamespace - from fastapi.testclient import TestClient -from omniclaw.facilitator.exact import ExactFacilitatorConfig, create_exact_facilitator_app +from omniclaw.facilitator.exact import ( + ExactFacilitatorConfig, + _normalize_tx_hash, + create_exact_facilitator_app, +) class _FakeResult: @@ -26,6 +28,11 @@ async def settle(self, payload, requirements): return _FakeResult({"success": True, "transaction": "0xsettled"}) +def test_normalize_tx_hash_adds_prefix_when_missing(): + assert _normalize_tx_hash("abc123") == "0xabc123" + assert _normalize_tx_hash("0xabc123") == "0xabc123" + + def test_create_exact_facilitator_app_registers_networks(): recorded = {} diff --git a/tests/test_x402_sdk_adapter.py b/tests/test_x402_sdk_adapter.py index d1cdc49..9054a63 100644 --- a/tests/test_x402_sdk_adapter.py +++ b/tests/test_x402_sdk_adapter.py @@ -142,7 +142,50 @@ async def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_pay_route_uses_seller_declared_amount_for_url_payments(monkeypatch: pytest.MonkeyPatch): +async def test_simulate_reports_insufficient_direct_wallet_balance( + monkeypatch: pytest.MonkeyPatch, +): + url = "https://seller.example/compute" + + async def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response( + 402, + headers={"PAYMENT-REQUIRED": _make_payment_required_header(url)}, + json={"error": "Payment Required"}, + ) + + adapter = _make_adapter(httpx.MockTransport(handler)) + monkeypatch.setattr( + adapter, + "_check_direct_exact_balance", + lambda selected_requirements: { + "balance_check": "failed", + "buyer_address": "0xa6b9b6244A5AD5FC2eF2BEB67ce04b75A0dB91D7", + "direct_wallet_balance_atomic": "150000", + "direct_wallet_balance": "0.15", + "direct_wallet_required_atomic": "250000", + "direct_wallet_required": "0.25", + "direct_wallet_has_enough": False, + }, + ) + + result = await adapter.simulate( + wallet_id="buyer-wallet", + recipient=url, + amount=Decimal("0.25"), + ) + + assert result["would_succeed"] is False + assert result["balance_check"] == "failed" + assert result["direct_wallet_balance"] == "0.15" + assert result["direct_wallet_required"] == "0.25" + assert "needs 0.25 USDC" in result["reason"] + + +@pytest.mark.asyncio +async def test_pay_route_uses_seller_declared_amount_for_url_payments( + monkeypatch: pytest.MonkeyPatch, +): payment_result = PaymentResult( success=True, transaction_id="tx-2",