Skill: openclaw-consensus-bot/x402janus-acp (version 2.0.0)

--- name: x402janus-acp description: > ACP buyer skill — hire x402janus for wallet security scans via the Virtuals ACP marketplace. Creates a job on the ACP marketplace targeting the x402janus agent, waits for completion, and returns the scan results. Pays with $VIRTUAL tokens. metadata: emoji: 🔗 homepage: https://x402janus.com requires: bins: [node, npx] env: [ACP_API_KEY] --- # x402janus-acp — Hire Janus via ACP Marketplace Buy wallet security scans from x402janus through the Virtuals Agent Commerce Protocol. ## When to Use This vs. the Direct Skill | Scenario | Use | |----------|-----| | Your agent has USDC on Base | Use `x402janus` (direct x402 payment, faster) | | Your agent has $VIRTUAL tokens | Use `x402janus-acp` (ACP marketplace) | | You want agent-to-agent commerce | Use `x402janus-acp` | ## Setup ```bash SKILL_DIR="$PWD/skills/x402janus-acp" cd "$SKILL_DIR" && npm install ``` **Required environment variables:** | Variable | Description | |----------|-------------| | `ACP_API_KEY` | Virtuals ACP API key for your agent (buyer key) | Get your ACP key at: https://app.virtuals.io/acp **Optional:** | Variable | Default | Description | |----------|---------|-------------| | `ACP_BASE_URL` | `https://claw-api.virtuals.io` | ACP API base URL | | `ACP_AGENT_WALLET` | — | Your agent's wallet address | ## Commands ### List Available Offerings See what scan tiers x402janus offers on the ACP marketplace. ```bash ACP_API_KEY=$KEY npx tsx scripts/list-offerings.ts --json ``` ### Scan a Wallet via ACP Creates a job on the ACP marketplace, waits for x402janus to complete it, and returns results. ```bash # Basic scan ACP_API_KEY=$KEY npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 # With JSON output ACP_API_KEY=$KEY npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --json # Specific offering tier ACP_API_KEY=$KEY npx tsx scripts/scan-wallet-acp.ts 0x... --offering janus_scan_deep # Custom timeout ACP_API_KEY=$KEY npx tsx scripts/scan-wallet-acp.ts 0x... --timeout 180 # Check status of existing job ACP_API_KEY=$KEY npx tsx scripts/scan-wallet-acp.ts --job-id 42 ``` ## Available Offerings Prices below are in $VIRTUAL. | Offering | Price | SLA | Description | |----------|-------|-----|-------------| | `janus_scan_quick` | 0.01 VIRTUAL | 5 min | Pre-transaction security check | | `janus_scan_standard` | 0.05 VIRTUAL | 5 min | Standard scan with approval analysis | | `janus_scan_deep` | 0.25 VIRTUAL | 5 min | Deep forensic scan | | `janus_approvals` | 0.01 VIRTUAL | 5 min | List active approvals with risk | | `janus_revoke` | 0.05 VIRTUAL | 5 min | Build revoke transaction | | `janus_revoke_batch` | 0.10 VIRTUAL | 5 min | Batch revoke all dangerous approvals | ## How It Works 1. Your agent calls the scan script with a wallet address 2. Script creates a job on the ACP marketplace targeting x402janus 3. x402janus picks up the job, runs the scan, delivers results 4. Script polls for completion and returns the result 5. Payment settles in $VIRTUAL through the ACP protocol

path: README.md
content: # x402janus-acp

**Hire x402janus via the Virtuals ACP marketplace** — wallet security scanning for AI agents, paid through the ACP job system instead of direct x402 micropayments.

---

## What This Does

This skill lets any OpenClaw agent commission a wallet security scan from x402janus without needing:
- A funded on-chain wallet with USDC on Base
- Direct x402 payment infrastructure
- The `x402janus` skill (which requires `PRIVATE_KEY`)

Instead, you authenticate with an ACP API key, create a job on the Virtuals marketplace, and x402janus delivers the scan result as the job deliverable.

---

## Install

```bash
cd skills/x402janus-acp
npm install
```

Create `.env`:
```bash
ACP_API_KEY=your_virtuals_api_key_here

# Optional
ACP_AGENT_WALLET=0xYourAgentWallet
ACP_BASE_URL=https://claw-api.virtuals.io
JANUS_OFFERING_NAME=wallet-scan-basic
```

Get your API key at [claw-api.virtuals.io](https://claw-api.virtuals.io).

---

## Usage

### List scan tiers and prices

```bash
npx tsx scripts/list-offerings.ts

# JSON output
npx tsx scripts/list-offerings.ts --json
```

Sample output:
```
  x402janus — ACP Marketplace Offerings
  ======================================

  Agent:    x402janus
  Wallet:   0x...
  Offering: wallet-scan-basic
  Price:    0.01 USDC
  Requires: walletAddress

  Agent:    x402janus
  Wallet:   0x...
  Offering: wallet-scan-deep
  Price:    0.05 USDC
  Requires: walletAddress
```

---

### Scan a wallet

```bash
# Full scan (creates job, polls to completion)
npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

# With explicit timeout (seconds)
npx tsx scripts/scan-wallet-acp.ts 0xd8dA6... --timeout 180

# JSON output only (machine-readable, no progress messages)
npx tsx scripts/scan-wallet-acp.ts 0xd8dA6... --json

# Choose a specific scan tier
npx tsx scripts/scan-wallet-acp.ts 0xd8dA6... --offering wallet-scan-deep

# Poll an existing job by ID (resume if interrupted)
npx tsx scripts/scan-wallet-acp.ts --job-id 42
```

---

### Use as a module in your agent

```typescript
import { scanWalletAcp } from "./skills/x402janus-acp/scripts/scan-wallet-acp.ts";
import { listOfferings } from "./skills/x402janus-acp/scripts/list-offerings.ts";

// List available tiers
const offerings = await listOfferings({ json: false });

// Run a scan
const result = await scanWalletAcp({
  walletAddress: [REDACTED_SECRET],
  timeoutSeconds: 120,
  pollIntervalSeconds: 5,
});

if (result.phase === "completed" && result.data) {
  console.log("Scan result:", result.data);
} else {
  console.error("Scan failed:", result.phase);
}
```

---

## Environment Variables

| Variable             | Required | Description                                                    |
|----------------------|----------|----------------------------------------------------------------|
| `ACP_API_KEY`        | **Yes**  | Virtuals ACP API key (`LITE_AGENT_API_KEY`)                    |
| `ACP_AGENT_WALLET`   | No       | Your agent's wallet address on Base                            |
| `ACP_BASE_URL`       | No       | ACP base URL (default: `https://claw-api.virtuals.io`)         |
| `JANUS_OFFERING_NAME`| No       | Default scan tier (auto-selects first available if unset)      |

---

## x402 vs ACP: Which to Use?

| Factor               | x402 (direct)                         | ACP marketplace (this skill)           |
|----------------------|----------------------------------------|----------------------------------------|
| **Skill**            | `x402janus`                            | `x402janus-acp`                        |
| **Auth**             | `PRIVATE_KEY` + USDC on Base           | `ACP_API_KEY` only                     |
| **Latency**          | ~1–3 seconds                           | 10–120 seconds (job queue + polling)   |
| **Payment**          | USDC on Base, per call                 | USDC via Virtuals ACP settlement       |
| **Setup complexity** | Medium (needs funded wallet)           | Low (API key only)                     |
| **Best for**         | Agents with Base wallets               | Agents without on-chain setup          |
| **Resumable jobs**   | No (synchronous)                       | Yes (`--job-id` flag)                  |
| **Batch scanning**   | No native batching                     | Create multiple jobs in parallel       |

**Use x402 when:** your agent already has a Base wallet with USDC and needs fast, synchronous scans.

**Use ACP when:** you want simpler setup (no wallet management), need resumable jobs, or are operating in an ACP-first agent economy.

---

## How It Works

```
Your Agent
    │
    │  1. POST /acp/agents?query=x402janus  → find x402janus + offerings
    │
    │  2. POST /acp/jobs  → create job with walletAddress as requirement
    │
    │  3. GET /acp/jobs/:id  → poll every 5s until phase=completed
    │
    ▼
ACP Marketplace
    │
    │  4. x402janus agent receives job, runs forensic scan
    │  5. x402janus submits deliverable (risk report JSON)
    │
    ▼
Your Agent
    │
    │  6. Deliverable extracted, parsed, returned to caller
    ▼
```

---

## Error Handling

| Situation                        | Behavior                                                      |
|----------------------------------|---------------------------------------------------------------|
| `ACP_API_KEY` not set            | Exit 1 with clear message                                     |
| Invalid wallet address           | Exit 1 with validation error                                  |
| x402janus not on marketplace     | Exit 1 with setup hint                                        |
| Requested offering not found     | Exit 1 listing available offerings                            |
| Job creation fails               | Exit 1 with API error message                                 |
| Job times out                    | Exit 1 with job ID to resume via `--job-id`                   |
| Job rejected                     | Returns `phase: "rejected"` with memo history                 |

---

## Files

```
x402janus-acp/
├── SKILL.md                    — OpenClaw skill descriptor
├── README.md                   — This file
├── package.json                — npm dependencies
├── tsconfig.json               — TypeScript config
└── scripts/
    ├── list-offerings.ts       — List x402janus tiers + prices
    └── scan-wallet-acp.ts      — Create job, poll, return results
```

---

## License

MIT — x402janus-acp is part of the x402janus skill ecosystem.


path: package-lock.json
content: {
  "name": "x402janus-acp",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "x402janus-acp",
      "version": "1.0.0",
      "devDependencies": {
        "@types/node": "^22.0.0",
        "tsx": "^4.19.2",
        "typescript": "^5.7.2"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@types/node": {
      "version": "22.19.13",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/esbuild": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.27.3",
        "@esbuild/android-arm": "0.27.3",
        "@esbuild/android-arm64": "0.27.3",
        "@esbuild/android-x64": "0.27.3",
        "@esbuild/darwin-arm64": "0.27.3",
        "@esbuild/darwin-x64": "0.27.3",
        "@esbuild/freebsd-arm64": "0.27.3",
        "@esbuild/freebsd-x64": "0.27.3",
        "@esbuild/linux-arm": "0.27.3",
        "@esbuild/linux-arm64": "0.27.3",
        "@esbuild/linux-ia32": "0.27.3",
        "@esbuild/linux-loong64": "0.27.3",
        "@esbuild/linux-mips64el": "0.27.3",
        "@esbuild/linux-ppc64": "0.27.3",
        "@esbuild/linux-riscv64": "0.27.3",
        "@esbuild/linux-s390x": "0.27.3",
        "@esbuild/linux-x64": "0.27.3",
        "@esbuild/netbsd-arm64": "0.27.3",
        "@esbuild/netbsd-x64": "0.27.3",
        "@esbuild/openbsd-arm64": "0.27.3",
        "@esbuild/openbsd-x64": "0.27.3",
        "@esbuild/openharmony-arm64": "0.27.3",
        "@esbuild/sunos-x64": "0.27.3",
        "@esbuild/win32-arm64": "0.27.3",
        "@esbuild/win32-ia32": "0.27.3",
        "@esbuild/win32-x64": "0.27.3"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/get-tsconfig": {
      "version": "4.13.6",
      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "resolve-pkg-maps": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
      }
    },
    "node_modules/resolve-pkg-maps": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
      }
    },
    "node_modules/tsx": {
      "version": "4.21.0",
      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "~0.27.0",
        "get-tsconfig": "^4.7.5"
      },
      "bin": {
        "tsx": "dist/cli.mjs"
      },
      "engines": {
        "node": ">=18.0.0"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "x402janus-acp",
  "version": "2.0.0",
  "description": "ACP buyer skill — hire x402janus for wallet security scans via the Virtuals ACP marketplace",
  "type": "module",
  "scripts": {
    "list": "npx tsx scripts/list-offerings.ts",
    "scan": "npx tsx scripts/scan-wallet-acp.ts"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


path: scripts/list-offerings.ts
content: #!/usr/bin/env npx tsx
/**
 * list-offerings.ts
 *
 * Lists all x402janus scan tiers and prices available on the ACP marketplace.
 * Fetches the x402janus agent profile from the ACP API and displays its job offerings.
 *
 * Usage:
 *   npx tsx scripts/list-offerings.ts [--json]
 *
 * Environment:
 *   ACP_API_KEY    — Virtuals ACP API key (buyer key, required)
 *   ACP_BASE_URL   — ACP base URL (default: https://claw-api.virtuals.io)
 */

import { parseArgs } from "util";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface JobOffering {
  id?: number | string;
  name: string;
  type?: string;
  price?: number;
  priceType?: string;
  priceV2?: { type: string; value: number };
  slaMinutes?: number;
  deliverable?: string;
  description?: string;
  requirement?: Record<string, string> | string;
  requiredFunds?: boolean;
}

interface AgentProfile {
  id: number | string;
  name: string;
  description?: string;
  walletAddress?: string;
  profilePic?: string | null;
  jobOfferings?: JobOffering[];
  enabledChains?: Array<{ id: number; name: string }>;
}

// ---------------------------------------------------------------------------
// Fetch
// ---------------------------------------------------------------------------

async function fetchAgents(
  [REDACTED_SECRET],
  baseUrl: string,
  query: string
): Promise<AgentProfile[]> {
  const resp = await fetch(`${baseUrl}/acp/agents?query=${encodeURIComponent(query)}`, {
    headers: {
      "x-api-key": apiKey,
      "Content-Type": "application/json",
    },
  });

  if (!resp.ok) {
    const body = await resp.text();
    throw new Error(`ACP API error: ${resp.status} ${resp.statusText} — ${body.slice(0, 200)}`);
  }

  const json = await resp.json() as { data: AgentProfile[] };
  return json.data ?? [];
}

function resolveJanusAgent(agents: AgentProfile[]): AgentProfile {
  const janus = agents.find((agent) => agent.name.toLowerCase() === "x402janus");
  if (!janus) {
    const candidates = agents.map((agent) => agent.name).join(", ");
    throw new Error(
      `Exact agent "x402janus" not found in ACP search results. Candidates: ${candidates}`
    );
  }
  return janus;
}

// ---------------------------------------------------------------------------
// Display
// ---------------------------------------------------------------------------

function formatPrice(offering: JobOffering): string {
  if (offering.priceV2) {
    return `${offering.priceV2.value} ${offering.priceV2.type}`;
  }
  const price = offering.price ?? 0;
  const priceType = offering.priceType ?? "VIRTUAL";
  return `${price} ${priceType}`;
}

function printOfferings(profile: AgentProfile, json: boolean): void {
  const offerings = profile.jobOfferings ?? [];

  if (json) {
    console.log(JSON.stringify({
      agent: {
        id: profile.id,
        name: profile.name,
        description: profile.description ?? "",
        wallet: profile.walletAddress ?? "",
        chains: (profile.enabledChains ?? []).map((c) => c.name),
      },
      offerings: offerings.map(o => ({
        name: o.name,
        price: o.priceV2?.value ?? o.price ?? 0,
        currency: (o.priceV2?.type ?? o.priceType ?? "VIRTUAL"),
        slaMinutes: o.slaMinutes ?? null,
        description: o.description ?? "",
        requirement: o.requirement ?? null,
      })),
    }, null, 2));
    return;
  }

  console.log(`\n🔲  x402janus ACP Offerings\n`);
  console.log(`Agent: ${profile.name} (ID: ${profile.id})`);
  if (profile.enabledChains && profile.enabledChains.length > 0) {
    console.log(`Chains: ${profile.enabledChains.map(c => c.name).join(", ")}`);
  }
  if (profile.walletAddress) {
    console.log(`Wallet: ${profile.walletAddress}`);
  }
  console.log(`\n${"─".repeat(60)}`);

  if (offerings.length === 0) {
    console.log("  No offerings registered.");
    return;
  }

  for (const o of offerings) {
    const price = formatPrice(o);
    console.log(`\n  📦 ${o.name}`);
    console.log(`     Price:       ${price}`);
    if (o.slaMinutes !== undefined) {
      console.log(`     SLA:         ${o.slaMinutes} minutes`);
    }
    if (o.description) {
      console.log(`     Description: ${o.description}`);
    }
    if (o.requirement) {
      const requirement =
        typeof o.requirement === "string" ? o.requirement : JSON.stringify(o.requirement);
      console.log(`     Requires:    ${requirement}`);
    }
  }

  console.log(`\n${"─".repeat(60)}\n`);
  console.log(`  ${offerings.length} offering(s) available.\n`);
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
  const { values } = parseArgs({
    args: process.argv.slice(2),
    options: {
      json: { type: "boolean", default: false },
      help: { type: "boolean", short: "h", default: false },
    },
    allowPositionals: false,
  });

  if (values.help) {
    console.log(`
x402janus — list ACP marketplace offerings

Usage: npx tsx scripts/list-offerings.ts [--json]

Options:
  --json       Output as JSON
  --help, -h   Show this help

Environment:
  ACP_API_KEY    Virtuals ACP API key (buyer key, required)
  ACP_BASE_URL   ACP base URL (default: https://claw-api.virtuals.io)
`);
    process.exit(0);
  }

  const [REDACTED_SECRET];
  if (!apiKey) {
    console.error("Error: ACP_API_KEY environment variable is not set.");
    console.error("Get your key from the Virtuals ACP dashboard.");
    process.exit(1);
  }

  const baseUrl = (process.env.ACP_BASE_URL ?? "https://claw-api.virtuals.io").replace(/\/$/, "");

  try {
    const agents = await fetchAgents(apiKey, baseUrl, "x402janus");
    if (agents.length === 0) {
      console.error(`No agents found matching "x402janus" on ACP marketplace.`);
      process.exit(1);
    }
    const profile = resolveJanusAgent(agents);
    printOfferings(profile, values.json ?? false);
  } catch (err) {
    console.error("Error:", err instanceof Error ? err.message : String(err));
    process.exit(1);
  }
}

main();


path: scripts/scan-wallet-acp.ts
content: #!/usr/bin/env tsx
/**
 * scan-wallet-acp.ts
 *
 * Hires x402janus via the Virtuals ACP marketplace to scan a wallet for
 * risky token approvals, drainer patterns, and on-chain threats. Uses the
 * ACP job system: create job → submit wallet → poll for deliverable.
 *
 * Usage:
 *   npx tsx scripts/scan-wallet-acp.ts <walletAddress> [options]
 *   npx tsx scripts/scan-wallet-acp.ts --job-id <id>   (poll existing job)
 *
 * Options:
 *   --timeout <seconds>       Max time to wait for job completion (default: 120)
 *   --poll-interval <seconds> How often to check job status (default: 5)
 *   --offering <name>         Specific offering name to use
 *   --json                    Output raw JSON result
 *   --job-id <id>             Skip creation, poll an existing job
 *   -h, --help                Show this help
 *
 * Environment:
 *   ACP_API_KEY          — Virtuals ACP API key (required)
 *   ACP_AGENT_WALLET     — Your agent's wallet address on Base (optional)
 *   ACP_BASE_URL         — ACP base URL (default: https://claw-api.virtuals.io)
 *   JANUS_OFFERING_NAME  — Default offering name to use (optional)
 */

import axios, { AxiosInstance } from "axios";
import dotenv from "dotenv";

dotenv.config();

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** ACP job phases as returned by the marketplace API. */
type JobPhase =
  | "pending"
  | "negotiation"
  | "transaction"
  | "completed"
  | "rejected"
  | "expired"
  | string;

/** A single memo/history entry from the job. */
interface JobMemo {
  nextPhase: string;
  content: string;
  createdAt: string;
  status: string;
}

/** Full ACP job state from GET /acp/jobs/:id */
interface AcpJob {
  id: number | string;
  phase: JobPhase;
  providerName?: string;
  providerAddress?: string;
  clientName?: string;
  clientAddress?: string;
  deliverable?: string | null;
  memos?: JobMemo[];
}

/** ACP agent offering used when discovering x402janus. */
interface JobOffering {
  name: string;
  price: number;
  priceType: string;
  requirement: string;
}

interface AcpAgent {
  id: string;
  name: string;
  walletAddress: string;
  description: string;
  jobOfferings: JobOffering[];
}

/** Parsed scan result extracted from the job deliverable. */
export interface ScanResult {
  jobId: number | string;
  walletAddress: string;
  phase: JobPhase;
  deliverable: string | null;
  /** Parsed JSON deliverable (if the provider returned structured data). */
  data?: unknown;
  memos: JobMemo[];
}

/** Configuration for a scan operation. */
interface ScanOptions {
  /** Wallet address to scan. */
  walletAddress: string;
  /** Override API key (default: ACP_API_KEY). */
  apiKey?: string;
  /** Override base URL (default: ACP_BASE_URL or production). */
  baseUrl?: string;
  /** Specific ACP offering name to use. */
  offeringName?: string;
  /** Caller's agent wallet address (optional, required by some offerings). */
  agentWallet?: string;
  /** Max seconds to wait for job completion (default: 120). */
  timeoutSeconds?: number;
  /** Seconds between status polls (default: 5). */
  pollIntervalSeconds?: number;
  /** Output raw JSON to stdout. */
  json?: boolean;
}

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const DEFAULT_BASE_URL = "https://claw-api.virtuals.io";
const DEFAULT_TIMEOUT_SECONDS = 120;
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
const TERMINAL_PHASES: JobPhase[] = ["completed", "rejected", "expired"];
const JANUS_SEARCH_QUERY = "x402janus";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Builds an Axios instance pre-configured for the ACP API.
 */
function buildClient([REDACTED_SECRET], baseUrl: string): AxiosInstance {
  return axios.create({
    baseURL: baseUrl,
    headers: { "x-api-key": apiKey },
    timeout: 15_000,
  });
}

/**
 * Validates an Ethereum address (basic hex format check).
 *
 * @param address - The address string to validate.
 * @returns true if it looks like a valid EVM address.
 */
function isValidAddress(address: string): boolean {
  return /^0x[0-9a-fA-F]{40}$/.test(address);
}

/**
 * Sleeps for the given number of milliseconds.
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Fetches agents from ACP matching the given query.
 *
 * @param client - Configured Axios instance.
 * @param query  - Search term.
 * @throws If the HTTP request fails.
 */
async function fetchAgents(
  client: AxiosInstance,
  query: string
): Promise<AcpAgent[]> {
  const res = await client.get<{ data: AcpAgent[] }>(
    `/acp/agents?query=${encodeURIComponent(query)}`
  );
  return res.data.data ?? [];
}

/**
 * Finds the x402janus agent and the target offering on the ACP marketplace.
 *
 * @param client       - Configured Axios instance.
 * @param offeringName - Preferred offering name. Uses first available if null.
 * @returns { agent, offering } — the matched agent and its offering.
 * @throws If no x402janus agent or matching offering is found.
 */
async function resolveJanusOffering(
  client: AxiosInstance,
  offeringName: string | null
): Promise<{ agent: AcpAgent; offering: JobOffering }> {
  const agents = await fetchAgents(client, JANUS_SEARCH_QUERY);

  if (agents.length === 0) {
    throw new Error(
      `No agents found matching "${JANUS_SEARCH_QUERY}" on ACP marketplace. ` +
      "x402janus may not be registered yet. Check https://claw-api.virtuals.io"
    );
  }

  // Require an exact x402janus agent match.
  // Do NOT fall back to arbitrary search results (spoofing risk).
  const janusAgent = agents.find((a) => a.name.toLowerCase() === "x402janus");
  if (!janusAgent) {
    const candidates = agents.map((a) => a.name).join(", ");
    throw new Error(
      `Exact agent "x402janus" not found in ACP search results. Candidates: ${candidates}`
    );
  }

  const offerings = janusAgent.jobOfferings ?? [];
  if (offerings.length === 0) {
    throw new Error(
      `x402janus agent (${janusAgent.name}) has no job offerings registered.`
    );
  }

  let offering: JobOffering | undefined;
  if (offeringName) {
    offering = offerings.find(
      (o) => o.name.toLowerCase() === offeringName.toLowerCase()
    );
    if (!offering) {
      const names = offerings.map((o) => `"${o.name}"`).join(", ");
      throw new Error(
        `Offering "${offeringName}" not found. Available: ${names}`
      );
    }
  } else {
    // Default: use the first available offering
    offering = offerings[0];
  }

  return { agent: janusAgent, offering };
}

/**
 * Creates an ACP job, targeting x402janus for a wallet scan.
 *
 * @param client           - Configured Axios instance.
 * @param providerWallet   - x402janus agent wallet address.
 * @param offeringName     - The offering name to purchase.
 * @param walletToScan     - The wallet address to submit for scanning.
 * @param agentWallet      - Caller's agent wallet (optional).
 * @returns The created job's numeric ID.
 * @throws If job creation fails.
 */
async function createScanJob(
  client: AxiosInstance,
  providerWallet: string,
  offeringName: string,
  walletToScan: string,
  agentWallet?: string
): Promise<number> {
  const serviceRequirements: Record<string, string> = {
    walletAddress: walletToScan,
  };

  if (agentWallet) {
    serviceRequirements.clientWalletAddress = agentWallet;
  }

  const res = await client.post<{ data: { jobId: number } }>("/acp/jobs", {
    providerWalletAddress: providerWallet,
    jobOfferingName: offeringName,
    serviceRequirements,
  });

  const jobId = res.data?.data?.jobId;
  if (!jobId) {
    throw new Error(
      `Unexpected response from job creation: ${JSON.stringify(res.data)}`
    );
  }

  return jobId;
}

/**
 * Fetches the current state of an ACP job by ID.
 *
 * @param client - Configured Axios instance.
 * @param jobId  - Numeric ACP job ID.
 * @returns Raw ACP job data.
 * @throws If the job is not found or the request fails.
 */
async function fetchJobStatus(
  client: AxiosInstance,
  jobId: number | string
): Promise<AcpJob> {
  const res = await client.get<{ data: AcpJob }>(`/acp/jobs/${jobId}`);
  const data = res.data?.data;
  if (!data) {
    throw new Error(`Job ${jobId} not found or empty response.`);
  }
  return data;
}

/**
 * Polls an ACP job until it reaches a terminal phase or times out.
 *
 * @param client          - Configured Axios instance.
 * @param jobId           - Job to poll.
 * @param timeoutMs       - Max milliseconds to wait.
 * @param pollIntervalMs  - Milliseconds between polls.
 * @param verbose         - Print phase updates to stderr.
 * @returns Final ACP job state.
 * @throws If the job times out before completing.
 */
async function pollJobToCompletion(
  client: AxiosInstance,
  jobId: number | string,
  timeoutMs: number,
  pollIntervalMs: number,
  verbose = true
): Promise<AcpJob> {
  const deadline = Date.now() + timeoutMs;
  let lastPhase = "";

  while (Date.now() < deadline) {
    const job = await fetchJobStatus(client, jobId);

    if (verbose && job.phase !== lastPhase) {
      process.stderr.write(`  [job ${jobId}] phase: ${job.phase}\n`);
      lastPhase = job.phase;
    }

    if (TERMINAL_PHASES.includes(job.phase)) {
      return job;
    }

    const remaining = deadline - Date.now();
    if (remaining <= 0) break;

    await sleep(Math.min(pollIntervalMs, remaining));
  }

  throw new Error(
    `Job ${jobId} did not complete within ${timeoutMs / 1000}s. ` +
    "Use --job-id to resume polling later."
  );
}

/**
 * Attempts to parse the job deliverable as JSON.
 *
 * @param deliverable - Raw deliverable string from the job.
 * @returns Parsed object, or undefined if not valid JSON.
 */
function parseDeliverable(deliverable: string | null | undefined): unknown {
  if (!deliverable) return undefined;
  try {
    return JSON.parse(deliverable);
  } catch {
    return undefined;
  }
}

// ---------------------------------------------------------------------------
// Main exported function
// ---------------------------------------------------------------------------

/**
 * Scans a wallet via the x402janus ACP marketplace offering.
 *
 * Flow:
 *   1. Resolve x402janus agent + target offering on the marketplace.
 *   2. Create an ACP job with the wallet address as the service requirement.
 *   3. Poll the job until it reaches a terminal phase (completed/rejected/expired).
 *   4. Return structured scan results from the deliverable.
 *
 * @param options - Scan configuration.
 * @returns Structured scan result with risk data.
 *
 * @example
 * const result = await scanWalletAcp({
 *   walletAddress: "0xYourWalletAddress",
 *   timeoutSeconds: 90,
 * });
 * console.log(result.data); // parsed risk report
 */
export async function scanWalletAcp(options: ScanOptions): Promise<ScanResult> {
  const {
    walletAddress,
    timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
    pollIntervalSeconds = DEFAULT_POLL_INTERVAL_SECONDS,
    json = false,
  } = options;

  // Validate inputs
  if (!isValidAddress(walletAddress)) {
    throw new Error(
      `Invalid wallet address: "${walletAddress}". Must be a 0x-prefixed 40-char hex address.`
    );
  }

  const [REDACTED_SECRET] ?? process.env.ACP_API_KEY;
  if (!apiKey) {
    throw new Error(
      "ACP_API_KEY environment variable is not set. " +
      "Get your key at https://claw-api.virtuals.io"
    );
  }

  const baseUrl = options.baseUrl ?? process.env.ACP_BASE_URL ?? DEFAULT_BASE_URL;
  const agentWallet = options.agentWallet ?? process.env.ACP_AGENT_WALLET;
  const offeringName =
    options.offeringName ?? process.env.JANUS_OFFERING_NAME ?? null;

  const client = buildClient(apiKey, baseUrl);

  if (!json) {
    process.stderr.write(`\n  Resolving x402janus on ACP marketplace…\n`);
  }

  // Step 1: Find x402janus + the target offering
  const { agent, offering } = await resolveJanusOffering(client, offeringName);

  if (!json) {
    process.stderr.write(
      `  Found: ${agent.name} — offering "${offering.name}" @ ${offering.price} ${offering.priceType}\n`
    );
    process.stderr.write(`  Creating ACP job for wallet: ${walletAddress}\n`);
  }

  // Step 2: Create the job
  const jobId = await createScanJob(
    client,
    agent.walletAddress,
    offering.name,
    walletAddress,
    agentWallet
  );

  if (!json) {
    process.stderr.write(
      `  Job created: #${jobId} — polling for up to ${timeoutSeconds}s…\n\n`
    );
  }

  // Step 3: Poll to completion
  const finalJob = await pollJobToCompletion(
    client,
    jobId,
    timeoutSeconds * 1000,
    pollIntervalSeconds * 1000,
    !json
  );

  const result: ScanResult = {
    jobId: finalJob.id,
    walletAddress,
    phase: finalJob.phase,
    deliverable: finalJob.deliverable ?? null,
    data: parseDeliverable(finalJob.deliverable),
    memos: finalJob.memos ?? [],
  };

  return result;
}

// ---------------------------------------------------------------------------
// CLI entrypoint
// ---------------------------------------------------------------------------

function printHelp(): void {
  console.log(`
scan-wallet-acp — Hire x402janus via ACP marketplace for wallet scans

Usage:
  npx tsx scripts/scan-wallet-acp.ts <walletAddress> [options]
  npx tsx scripts/scan-wallet-acp.ts --job-id <id> [options]

Options:
  --timeout <seconds>       Max seconds to wait for completion (default: ${DEFAULT_TIMEOUT_SECONDS})
  --poll-interval <seconds> Seconds between status checks (default: ${DEFAULT_POLL_INTERVAL_SECONDS})
  --offering <name>         Specific x402janus offering name to use
  --json                    Output raw JSON result only (no progress messages)
  --job-id <id>             Skip creation; poll an existing job by ID
  -h, --help                Show this help

Environment variables:
  ACP_API_KEY          — Virtuals ACP API key (required)
  ACP_AGENT_WALLET     — Your agent's wallet address (optional)
  ACP_BASE_URL         — ACP base URL (default: https://claw-api.virtuals.io)
  JANUS_OFFERING_NAME  — Default offering name (optional)

Examples:
  npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
  npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --json
  npx tsx scripts/scan-wallet-acp.ts 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --timeout 180
  npx tsx scripts/scan-wallet-acp.ts --job-id 42
`);
}

function parseArgs(argv: string[]): {
  walletAddress?: string;
  jobId?: string;
  timeoutSeconds: number;
  pollIntervalSeconds: number;
  offeringName?: string;
  json: boolean;
} {
  const args = argv.slice(2);
  let walletAddress: string | undefined;
  let jobId: string | undefined;
  let timeoutSeconds = DEFAULT_TIMEOUT_SECONDS;
  let pollIntervalSeconds = DEFAULT_POLL_INTERVAL_SECONDS;
  let offeringName: string | undefined;
  let json = false;

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === "-h" || arg === "--help") {
      printHelp();
      process.exit(0);
    } else if (arg === "--json") {
      json = true;
    } else if (arg === "--timeout" && args[i + 1]) {
      timeoutSeconds = parseInt(args[++i], 10);
      if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
        console.error("Error: --timeout must be a positive integer.");
        process.exit(1);
      }
    } else if (arg === "--poll-interval" && args[i + 1]) {
      pollIntervalSeconds = parseInt(args[++i], 10);
      if (isNaN(pollIntervalSeconds) || pollIntervalSeconds <= 0) {
        console.error("Error: --poll-interval must be a positive integer.");
        process.exit(1);
      }
    } else if (arg === "--offering" && args[i + 1]) {
      offeringName = args[++i];
    } else if (arg === "--job-id" && args[i + 1]) {
      jobId = args[++i];
    } else if (!arg.startsWith("--") && !walletAddress) {
      walletAddress = arg;
    }
  }

  return {
    walletAddress,
    jobId,
    timeoutSeconds,
    pollIntervalSeconds,
    offeringName,
    json,
  };
}

// Run when called directly
const isMain =
  process.argv[1]?.endsWith("scan-wallet-acp.ts") ||
  process.argv[1]?.endsWith("scan-wallet-acp.js");

if (isMain) {
  const parsed = parseArgs(process.argv);

  if (!parsed.walletAddress && !parsed.jobId) {
    console.error("Error: provide a wallet address or --job-id.");
    printHelp();
    process.exit(1);
  }

  const [REDACTED_SECRET];
  if (!apiKey) {
    console.error("Error: ACP_API_KEY environment variable is not set.");
    console.error("Get your key at: https://claw-api.virtuals.io");
    process.exit(1);
  }

  const baseUrl = process.env.ACP_BASE_URL ?? DEFAULT_BASE_URL;
  const client = buildClient(apiKey, baseUrl);

  const run = async (): Promise<void> => {
    // Mode: poll existing job
    if (parsed.jobId && !parsed.walletAddress) {
      if (!parsed.json) {
        process.stderr.write(`\n  Polling existing job #${parsed.jobId}…\n\n`);
      }
      const job = await pollJobToCompletion(
        client,
        parsed.jobId,
        parsed.timeoutSeconds * 1000,
        parsed.pollIntervalSeconds * 1000,
        !parsed.json
      );

      const result: ScanResult = {
        jobId: job.id,
        walletAddress: "(from job)",
        phase: job.phase,
        deliverable: job.deliverable ?? null,
        data: parseDeliverable(job.deliverable),
        memos: job.memos ?? [],
      };

      if (parsed.json) {
        console.log(JSON.stringify(result, null, 2));
        return;
      }

      printResultHuman(result);
      return;
    }

    // Mode: full scan
    const result = await scanWalletAcp({
      walletAddress: parsed.walletAddress!,
      offeringName: parsed.offeringName,
      timeoutSeconds: parsed.timeoutSeconds,
      pollIntervalSeconds: parsed.pollIntervalSeconds,
      json: parsed.json,
    });

    if (parsed.json) {
      console.log(JSON.stringify(result, null, 2));
      return;
    }

    printResultHuman(result);
  };

  run().catch((err) => {
    const msg = err instanceof Error ? err.message : String(err);
    console.error(`\n  Error: ${msg}\n`);
    process.exit(1);
  });
}

/**
 * Prints a scan result in human-readable format.
 *
 * @param result - Completed scan result.
 */
function printResultHuman(result: ScanResult): void {
  console.log("\n  x402janus ACP Scan Result");
  console.log("  ─────────────────────────");
  console.log(`  Job ID:  ${result.jobId}`);
  console.log(`  Wallet:  ${result.walletAddress}`);
  console.log(`  Status:  ${result.phase}`);
  console.log("");

  if (result.phase === "completed" && result.deliverable) {
    console.log("  Deliverable:");
    if (result.data && typeof result.data === "object") {
      // Pretty-print structured JSON data
      const lines = JSON.stringify(result.data, null, 2).split("\n");
      for (const line of lines) {
        console.log(`    ${line}`);
      }
    } else {
      console.log(`    ${result.deliverable}`);
    }
  } else if (result.phase === "rejected") {
    console.log("  Job was rejected by x402janus.");
    if (result.memos.length > 0) {
      console.log("  Last memo:", result.memos[result.memos.length - 1]?.content ?? "");
    }
  } else if (result.phase === "expired") {
    console.log("  Job expired without completion.");
  }

  if (result.memos.length > 0) {
    console.log("\n  History:");
    for (const m of result.memos) {
      console.log(`    [${m.nextPhase}] ${m.content} (${m.createdAt})`);
    }
  }

  console.log("");
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": ".",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["scripts/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)