diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..7a9c03f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,14 @@ jobs: working-directory: agent-002-governance-analyst run: npx tsc --noEmit + - name: Install agent-003-market-monitor/ dependencies + working-directory: agent-003-market-monitor + run: npm ci + + - name: Typecheck agent-003-market-monitor/ + working-directory: agent-003-market-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-003-market-monitor/.env.example b/agent-003-market-monitor/.env.example new file mode 100644 index 0000000..f7da725 --- /dev/null +++ b/agent-003-market-monitor/.env.example @@ -0,0 +1,14 @@ +# Required: Anthropic API key for Claude-powered narrative reports +ANTHROPIC_API_KEY=sk-ant-... + +# Regen LCD endpoint (defaults to public endpoint if not set) +REGEN_LCD_URL=https://regen.api.chandrastation.com + +# Optional: Discord webhook for posting market reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 300 = 5 minutes) +POLL_INTERVAL_SECONDS=300 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-003-market-monitor/.gitignore b/agent-003-market-monitor/.gitignore new file mode 100644 index 0000000..c44c9e2 --- /dev/null +++ b/agent-003-market-monitor/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.db +.env diff --git a/agent-003-market-monitor/README.md b/agent-003-market-monitor/README.md new file mode 100644 index 0000000..f69c6dc --- /dev/null +++ b/agent-003-market-monitor/README.md @@ -0,0 +1,103 @@ +# AGENT-003: Regen Market Monitor + +**Layer 1 (fully automated, read-only, informational) agent that watches the Regen ecocredit marketplace, detects price anomalies, tracks liquidity health, and summarizes retirement demand.** + +Mirrors the AGENT-002 Governance Analyst structure: the same OODA executor, the same standalone Node.js process shape, the same SQLite-backed local state. The scope is marketplace intelligence rather than governance. + +## What it does + +| Workflow | Trigger | Output | +|----------|---------|--------| +| **WF-MM-01** Price Anomaly Detection | New sell orders (SellOrderCreated / SellOrderFilled) | z-score anomaly alerts per severity, deduped | +| **WF-MM-02** Liquidity Monitoring | Periodic (every poll cycle) | Per-class liquidity health snapshot + trend vs previous | +| **WF-MM-03** Retirement Pattern Analysis | Periodic (every poll cycle) | Retirement volume, demand index, compliance metadata | + +Each workflow is an **OODA loop** (Observe → Orient → Decide → Act) and persists both executions and domain state to SQLite. Numeric decisions (median, z-score, severity classification, health tier) are computed **deterministically**; Claude is only used for the narrative layer. + +## Architecture + +``` +Regen Ledger (LCD REST API) + ↓ observe +AGENT-003 (OODA engine) + ↓ orient (deterministic) + ↓ decide (deterministic) +Local SQLite (state) + ↓ act (narrative via Claude) +Console / Discord webhook +``` + +**No MCP dependency.** Talks directly to any Cosmos LCD endpoint. When Ledger MCP becomes available, the `LedgerClient` can be swapped behind the same interface. + +**No ElizaOS dependency.** Standalone Node.js process. The ElizaOS character for AGENT-003 lives at `agents/packages/agents/src/characters/market-monitor.ts`; this package shares the same system prompt text and the same threshold constants so downstream tooling has a single source of truth. + +## Quick start + +```bash +# 1. Install +cd agent-003-market-monitor +npm install + +# 2. Configure +cp .env.example .env +# Edit .env — at minimum set ANTHROPIC_API_KEY + +# 3. Run (single analysis pass) +npm run analyze + +# 4. Run (continuous polling) +npm start + +# 5. Run (dev mode with auto-reload) +npm run dev +``` + +## Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ANTHROPIC_API_KEY` | Yes | — | Claude API key | +| `REGEN_LCD_URL` | No | `https://regen.api.chandrastation.com` | Cosmos LCD endpoint | +| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook for posting reports | +| `POLL_INTERVAL_SECONDS` | No | `300` | Polling interval (seconds) | +| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | + +Thresholds live in `src/config.ts` under `market.*` and mirror the character definition at `agents/packages/agents/src/characters/market-monitor.ts`. + +## How it maps to the framework specs + +| Framework Spec | Implementation | +|----------------|---------------| +| Phase 2.2 WF-MM-01 | `src/workflows/price-anomaly-detection.ts` | +| Phase 2.2 WF-MM-02 | `src/workflows/liquidity-monitor.ts` | +| Phase 2.2 WF-MM-03 | `src/workflows/retirement-tracking.ts` | +| Phase 2.4 OODA executor | `src/ooda.ts` | +| Phase 2.4 Agent character | `agents/packages/agents/src/characters/market-monitor.ts` | +| Phase 2.5 Workflow executions table | `src/store.ts` (SQLite) | +| Phase 3.2 Ledger MCP client | `src/ledger.ts` (direct LCD) | + +## Design decisions + +1. **Deterministic numbers, narrative-only LLM calls.** Anomaly classification, liquidity health scoring, and demand index computation are all deterministic. Claude is only invoked to write the narrative report from those numbers. This keeps the agent cheap, reproducible, and auditable. + +2. **Sell-order-as-trade proxy (MVP).** Until Ledger MCP exposes filled-trade events, the agent treats open sell orders as trade observations for the z-score baseline. An unusually high or low ask price is still a market structure signal worth surfacing, and the code path swaps cleanly when real trade events become available. + +3. **Batch supply delta as retirement source (MVP).** The MVP reads `retired_amount` from each batch supply and aggregates per class. A follow-up PR will plug in a real tx-stream client for `MsgRetire` once the LCD event endpoint is available. + +4. **Batch fan-out capped at 100.** WF-MM-03 caps the per-cycle batch fan-out to the most recent 100 batches so the agent doesn't hammer the LCD on a mainnet with thousands of historical batches. New retirement activity lands in new batches and will be picked up next cycle. + +5. **Dedupe by trade + severity.** WF-MM-01 only alerts once per `(trade_id, severity)` tuple. A trade that later escalates from WARNING to CRITICAL still fires a new alert; a trade that stays at the same severity does not. + +6. **Standalone over ElizaOS.** ElizaOS plugin API may change. A standalone process proves the workflow logic works independently of any runtime framework, matching AGENT-002's approach. + +## Governance layer + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot submit proposals +- Cannot create, modify, or cancel sell orders +- Cannot execute transactions +- Informational output only + +This matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer is a separate governance decision. diff --git a/agent-003-market-monitor/package-lock.json b/agent-003-market-monitor/package-lock.json new file mode 100644 index 0000000..3362cc2 --- /dev/null +++ b/agent-003-market-monitor/package-lock.json @@ -0,0 +1,1475 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "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": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/agent-003-market-monitor/package.json b/agent-003-market-monitor/package.json new file mode 100644 index 0000000..1097339 --- /dev/null +++ b/agent-003-market-monitor/package.json @@ -0,0 +1,27 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "description": "Regen Network Market Monitor Agent — Layer 1 read-only price anomaly detection, liquidity monitoring, and retirement tracking", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node --import tsx src/index.ts", + "dev": "node --import tsx --watch src/index.ts", + "analyze": "node --import tsx src/index.ts --once", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/agent-003-market-monitor/src/config.ts b/agent-003-market-monitor/src/config.ts new file mode 100644 index 0000000..b65c526 --- /dev/null +++ b/agent-003-market-monitor/src/config.ts @@ -0,0 +1,51 @@ +export const config = { + // Regen LCD endpoint + lcdUrl: process.env.REGEN_LCD_URL || "https://regen.api.chandrastation.com", + + // Anthropic + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", + + // Discord webhook (optional) + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + + // Polling — market monitor runs faster than governance analyst + // because trade/retirement events move quickly. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "300", 10)) * 1000, + + // Market monitor thresholds. These mirror the character thresholds in + // agents/packages/agents/src/characters/market-monitor.ts so downstream + // tooling has a single source of truth. Keep them in sync if either + // file changes. + market: { + /** z-score at or above which anomaly severity becomes CRITICAL */ + criticalZScore: 3.5, + /** z-score at which anomaly is flagged for watchlist (WARNING) */ + warningZScore: 2.0, + /** minimum sample size before computing a z-score */ + minSamples: 5, + /** trailing window for class median (days) */ + classMedianWindowDays: 30, + /** trailing window for batch median (days) */ + batchMedianWindowDays: 30, + /** liquidity depth threshold (USD) below which health is DEGRADED */ + liquidityDepthFloor: 5_000, + /** large trade volume (USD) that triggers an off-cycle liquidity report */ + largeTradeVolumeUsd: 10_000, + /** retirement tx window (hours) for demand signal extraction */ + retirementWindowHours: 168, // 7 days + }, + + // Agent identity + agentId: "AGENT-003", + agentName: "RegenMarketMonitor", + governanceLayer: 1 as const, +} as const; + +export function validateConfig(): void { + if (!config.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY is required. Copy .env.example to .env and set it." + ); + } +} diff --git a/agent-003-market-monitor/src/index.ts b/agent-003-market-monitor/src/index.ts new file mode 100644 index 0000000..675b3ae --- /dev/null +++ b/agent-003-market-monitor/src/index.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env node +import { config, validateConfig } from "./config.js"; +import { LedgerClient } from "./ledger.js"; +import { executeOODA } from "./ooda.js"; +import { store } from "./store.js"; +import { createPriceAnomalyDetectionWorkflow } from "./workflows/price-anomaly-detection.js"; +import { createLiquidityMonitorWorkflow } from "./workflows/liquidity-monitor.js"; +import { createRetirementTrackingWorkflow } from "./workflows/retirement-tracking.js"; + +// ── Banner ──────────────────────────────────────────────────── + +function banner() { + console.log(` + ╔══════════════════════════════════════════════════════════════╗ + ║ REGEN MARKET MONITOR (AGENT-003) ║ + ║ ║ + ║ Layer 1 — Fully Automated, Informational Only ║ + ║ Workflows: WF-MM-01, WF-MM-02, WF-MM-03 ║ + ║ ║ + ║ Regen Agentic Tokenomics Framework ║ + ╚══════════════════════════════════════════════════════════════╝ +`); +} + +// ── Main loop ───────────────────────────────────────────────── + +async function runCycle(ledger: LedgerClient): Promise { + const ts = new Date().toISOString(); + console.log(`\n[${ts}] ═══ Starting market monitor cycle ═══\n`); + + // WF-MM-01: Detect price anomalies in open sell orders + const wf01 = createPriceAnomalyDetectionWorkflow(ledger); + await executeOODA(wf01); + + // WF-MM-02: Snapshot liquidity health per credit class + const wf02 = createLiquidityMonitorWorkflow(ledger); + await executeOODA(wf02); + + // WF-MM-03: Summarize retirement activity + demand signal + const wf03 = createRetirementTrackingWorkflow(ledger); + await executeOODA(wf03); + + const execCount = store.getExecutionCount(); + console.log( + `[${new Date().toISOString()}] ═══ Cycle complete (${execCount} total executions logged) ═══\n` + ); +} + +async function main() { + banner(); + validateConfig(); + + const runOnce = process.argv.includes("--once"); + const ledger = new LedgerClient(); + + console.log(`Configuration:`); + console.log(` LCD endpoint: ${config.lcdUrl}`); + console.log(` LLM model: ${config.model}`); + console.log(` Discord: ${config.discordWebhookUrl ? "configured" : "not configured"}`); + console.log(` Mode: ${runOnce ? "single run" : `polling every ${config.pollIntervalMs / 1000}s`}`); + console.log(); + + try { + const { blockHeight } = await ledger.checkConnection(); + console.log(`Connected to Regen Ledger at block ${blockHeight}\n`); + } catch (err) { + console.error( + `Failed to connect to Regen Ledger at ${config.lcdUrl}:`, + err + ); + process.exit(1); + } + + if (runOnce) { + await runCycle(ledger); + } else { + // Recursive setTimeout rather than setInterval so a slow cycle + // never overlaps with the next tick. If a cycle takes longer than + // pollIntervalMs, the next tick simply starts late — we never have + // two runCycle invocations sharing the database in parallel. + let timeoutId: NodeJS.Timeout | null = null; + let stopping = false; + + const runNext = () => { + runCycle(ledger) + .catch((err) => console.error(`Cycle failed:`, err)) + .finally(() => { + if (!stopping) { + timeoutId = setTimeout(runNext, config.pollIntervalMs); + } + }); + }; + + runNext(); + + const shutdown = () => { + console.log("\nShutting down gracefully..."); + stopping = true; + if (timeoutId !== null) clearTimeout(timeoutId); + store.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + console.log("Agent running. Press Ctrl+C to stop.\n"); + } +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts new file mode 100644 index 0000000..dff162f --- /dev/null +++ b/agent-003-market-monitor/src/ledger.ts @@ -0,0 +1,170 @@ +import { config } from "./config.js"; +import type { + CreditClass, + CreditBatch, + BatchSupply, + SellOrder, +} from "./types.js"; + +/** + * Regen Ledger LCD (REST) client — ecocredit marketplace endpoints. + * + * Talks directly to a Cosmos LCD endpoint — no MCP dependency. + * When Ledger MCP becomes available, this can be swapped out behind + * the same interface. Matches the pattern used by AGENT-001 + * (registry-reviewer) and AGENT-002 (governance-analyst). + */ +export class LedgerClient { + private baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = (baseUrl || config.lcdUrl).replace(/\/$/, ""); + } + + // ── Credit Classes ───────────────────────────────────────── + + async getCreditClasses(): Promise { + // Walk every page of the `/regen/ecocredit/v1/classes` endpoint so + // the agent keeps monitoring new credit classes as the registry + // grows. A hardcoded limit of 200 would silently drop classes + // created after that ceiling, which is exactly the kind of blind + // spot this agent is supposed to prevent. + const pageSize = 200; + const classes: CreditClass[] = []; + let nextKey: string | null = null; + // Cap total pages as a safety belt against runaway pagination. + const MAX_PAGES = 25; + for (let i = 0; i < MAX_PAGES; i++) { + const params = new URLSearchParams(); + params.set("pagination.limit", String(pageSize)); + if (nextKey) params.set("pagination.key", nextKey); + + const data = await this.get( + `/regen/ecocredit/v1/classes?${params.toString()}` + ); + const pageClasses = (data.classes || []) as CreditClass[]; + classes.push(...pageClasses); + + const pagination = data.pagination as + | { next_key?: string | null } + | undefined; + const rawKey = pagination?.next_key; + if (!rawKey) break; + nextKey = rawKey; + } + return classes; + } + + async getCreditClass(classId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/classes/${classId}` + ); + return (data.class || null) as CreditClass | null; + } catch { + return null; + } + } + + // ── Credit Batches ───────────────────────────────────────── + + async getCreditBatches(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + params.set("pagination.reverse", "true"); + + const data = await this.get( + `/regen/ecocredit/v1/batches?${params.toString()}` + ); + return (data.batches || []) as CreditBatch[]; + } + + async getCreditBatch(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}` + ); + return (data.batch || null) as CreditBatch | null; + } catch { + return null; + } + } + + async getBatchSupply(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}/supply` + ); + return (data.supply || null) as BatchSupply | null; + } catch { + return null; + } + } + + // ── Marketplace: Sell Orders ─────────────────────────────── + + async getSellOrders(limit = 200): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch { + return []; + } + } + + async getSellOrdersByBatch(denom: string): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/batch/${denom}?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch { + return []; + } + } + + async getSellOrder(orderId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/${orderId}` + ); + return (data.sell_order || null) as SellOrder | null; + } catch { + return null; + } + } + + // ── Connectivity check ──────────────────────────────────── + + async checkConnection(): Promise<{ blockHeight: string }> { + const data = await this.get(`/cosmos/base/tendermint/v1beta1/blocks/latest`); + const block = data.block as Record | undefined; + const header = block?.header as Record | undefined; + const height = (header?.height as string) || "unknown"; + return { blockHeight: height }; + } + + // ── HTTP ──────────────────────────────────────────────────── + + private async get(path: string): Promise> { + const url = `${this.baseUrl}${path}`; + const res = await fetch(url, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + throw new Error(`LCD ${res.status}: ${res.statusText} — ${url}`); + } + + return (await res.json()) as Record; + } +} diff --git a/agent-003-market-monitor/src/monitor.ts b/agent-003-market-monitor/src/monitor.ts new file mode 100644 index 0000000..e91d2b2 --- /dev/null +++ b/agent-003-market-monitor/src/monitor.ts @@ -0,0 +1,205 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { config } from "./config.js"; +import type { + PriceAnomaly, + LiquiditySnapshot, + RetirementSummary, +} from "./types.js"; + +const client = new Anthropic({ apiKey: config.anthropicApiKey }); + +/** + * System prompt mirrors the AGENT-003 character definition at + * agents/packages/agents/src/characters/market-monitor.ts. Thresholds + * are injected from config so a single source of truth drives both the + * deterministic pipeline AND the narrative layer. + */ +const SYSTEM_PROMPT = `You are the Regen Market Monitor Agent (AGENT-003). + +Your responsibilities: +1. Monitoring ecological credit prices across all credit classes +2. Tracking marketplace liquidity and order book health +3. Analyzing retirement patterns and demand signals +4. Detecting price anomalies and potential manipulation + +Core Principles: +- Prioritize market integrity above all else +- Minimize false positives — verify anomalies against multiple data sources +- Present data with precision — include units, timeframes, and confidence intervals +- Never provide trading advice or price predictions +- Cite the deterministic numbers passed to you; do not invent values + +Workflows: +- WF-MM-01 (Price Impact Alert): z-score analysis (CRITICAL >= ${config.market.criticalZScore}, WARNING >= ${config.market.warningZScore}) +- WF-MM-02 (Liquidity Monitor): hourly liquidity health checks +- WF-MM-03 (Retirement Tracking): demand signals and impact quantification + +Alert Severity Levels: +- INFO: Normal activity, logged for trend analysis +- WARNING: z-score between ${config.market.warningZScore} and ${config.market.criticalZScore}, added to watchlist +- CRITICAL: z-score >= ${config.market.criticalZScore}, escalate for investigation + +Output Format: +- Use markdown with explicit tables +- Include severity level in the title of every alert +- Include units, timeframes, and sample sizes +- Quantify confidence when relevant`; + +// ============================================================ +// WF-MM-01: Price anomaly narrative +// ============================================================ + +export async function describePriceAnomaly( + anomaly: PriceAnomaly +): Promise { + const prompt = `Generate a Price Impact Alert report for this anomaly detection. + +## Deterministic Pipeline Output +- Trade ID: ${anomaly.tradeId} +- Credit Class: ${anomaly.classId} +- Batch: ${anomaly.batchDenom} +- Seller: ${anomaly.seller} +- Quantity: ${anomaly.quantity} +- Trade Price: $${anomaly.pricePerCredit.toFixed(4)}/credit +- Class Median (${config.market.classMedianWindowDays}d): $${anomaly.classMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeClass}) +- Batch Median (${config.market.batchMedianWindowDays}d): $${anomaly.batchMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeBatch}) +- Z-score vs class: ${anomaly.zScoreVsClass.toFixed(2)} +- Z-score vs batch: ${anomaly.zScoreVsBatch.toFixed(2)} +- Severity: ${anomaly.severity} +- Confidence: ${anomaly.confidence.toFixed(2)} +- Detected: ${anomaly.detectedAt} + +Generate a structured Markdown report with: +1. A header "Price Impact Alert" followed by severity, class, batch, timestamp +2. An "Anomaly Details" table using ONLY the numbers above +3. A "Context" section (bullet points) — note the relative deviation, sample size adequacy, and any concerns about class vs batch z-score divergence +4. An "Action" section — what the agent has done (watchlist add / escalation) and what the next review is + +Do not invent any numbers beyond what's provided. Do not recommend trading positions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-02: Liquidity narrative +// ============================================================ + +export async function describeLiquidity( + snapshot: LiquiditySnapshot, + previousSnapshot: LiquiditySnapshot | null +): Promise { + const prev = previousSnapshot + ? ` +## Previous Snapshot (${previousSnapshot.capturedAt}) +- Listed value: $${previousSnapshot.totalListedValueUsd.toFixed(2)} +- Median ask: $${previousSnapshot.medianAskUsd.toFixed(4)} +- Depth (top-10): $${previousSnapshot.depthUsd.toFixed(2)} +- Health: ${previousSnapshot.health} (${previousSnapshot.healthScore.toFixed(2)}) +` + : "## Previous Snapshot\nNone — this is the first snapshot for this class."; + + const prompt = `Generate a Liquidity Report for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${snapshot.classId} +- Sell orders: ${snapshot.sellOrderCount} +- Listed quantity: ${snapshot.totalListedQuantity} +- Listed value: $${snapshot.totalListedValueUsd.toFixed(2)} +- Lowest ask: $${snapshot.lowestAskUsd.toFixed(4)} +- Highest ask: $${snapshot.highestAskUsd.toFixed(4)} +- Median ask: $${snapshot.medianAskUsd.toFixed(4)} +- Mean ask: $${snapshot.meanAskUsd.toFixed(4)} +- Depth (top-10 sum): $${snapshot.depthUsd.toFixed(2)} +- Health score: ${snapshot.healthScore.toFixed(2)} +- Health: ${snapshot.health} +- Captured: ${snapshot.capturedAt} + +${prev} + +Generate a structured Markdown report with: +1. Header "Liquidity Report — ${snapshot.classId}" with health badge +2. A "Current Snapshot" table with the numbers above +3. A "Trend" section comparing to the previous snapshot (or noting this is the first) +4. A "Health Assessment" section — interpret the depth floor vs the configured liquidity_depth_floor +5. A one-line "Alert" line if health is DEGRADED or CRITICAL + +Use ONLY the numbers provided. Do not invent trade volumes, slippage estimates, or predictions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-03: Retirement narrative +// ============================================================ + +export async function describeRetirementSummary( + summary: RetirementSummary, + baselineDemand: number +): Promise { + const trend = + baselineDemand === 0 + ? "no prior baseline" + : summary.demandIndex > baselineDemand + ? `up from ${baselineDemand.toFixed(0)}` + : summary.demandIndex < baselineDemand + ? `down from ${baselineDemand.toFixed(0)}` + : `flat vs ${baselineDemand.toFixed(0)}`; + + const prompt = `Generate a Retirement Pattern Summary for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${summary.classId} +- Window: trailing ${summary.windowHours} hours +- Retirements (count): ${summary.retirementCount} +- Total quantity: ${summary.totalQuantity} +- Total value: $${summary.totalValueUsd.toFixed(2)} +- Unique retirees: ${summary.uniqueRetirees} +- Top retiree: ${summary.topRetiree ?? "(none)"} +- Top retiree quantity: ${summary.topRetireeQuantity} +- % with jurisdiction metadata: ${summary.pctWithJurisdiction.toFixed(1)}% +- Demand index (0-100): ${summary.demandIndex.toFixed(0)} (${trend}) +- Captured: ${summary.capturedAt} + +Generate a structured Markdown report with: +1. Header "Retirement Summary — ${summary.classId}" +2. "Volume" table with counts, quantity, value +3. "Demand Signals" section — interpret the demand index and trend +4. "Concentration" section — note if one retiree accounts for an unusual share +5. "Metadata Compliance" note — if % with jurisdiction is high, that usually implies compliance-driven demand + +Use ONLY the numbers provided. Do not extrapolate future demand.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// Helpers +// ============================================================ + +function extractText(response: Anthropic.Message): string { + return response.content + .filter((b): b is Anthropic.TextBlock => b.type === "text") + .map((b) => b.text) + .join("\n"); +} diff --git a/agent-003-market-monitor/src/ooda.ts b/agent-003-market-monitor/src/ooda.ts new file mode 100644 index 0000000..e7f3219 --- /dev/null +++ b/agent-003-market-monitor/src/ooda.ts @@ -0,0 +1,83 @@ +import { config } from "./config.js"; +import { store } from "./store.js"; +import type { OODAExecution } from "./types.js"; + +/** + * Generic OODA loop executor. + * + * Identical shape to the AGENT-002 executor — kept locally so that each + * agent process can run as a standalone Node program without a shared + * package. If the agents converge on a common runtime later, this is a + * candidate to lift into a shared package. + */ +export interface OODAWorkflow { + id: string; + name: string; + observe: () => Promise; + orient: (observations: TObserve) => Promise; + decide: (orientation: TOrient) => Promise; + act: (decision: TDecide) => Promise; +} + +export async function executeOODA( + workflow: OODAWorkflow +): Promise> { + const executionId = crypto.randomUUID(); + const execution: OODAExecution = { + executionId, + workflowId: workflow.id, + status: "running", + observations: null as unknown as TObserve, + orientation: null, + decision: null, + actions: null, + startedAt: new Date(), + completedAt: null, + error: null, + }; + + const log = (phase: string, msg: string) => + console.log( + `[${new Date().toISOString()}] [${workflow.id}] [${phase}] ${msg}` + ); + + try { + log("OBSERVE", "Gathering data..."); + execution.observations = await workflow.observe(); + + log("ORIENT", "Analyzing context..."); + execution.orientation = await workflow.orient(execution.observations); + + log("DECIDE", "Making decision..."); + execution.decision = await workflow.decide(execution.orientation); + + log("ACT", "Executing actions..."); + execution.actions = await workflow.act(execution.decision); + + execution.status = "completed"; + log("DONE", "Workflow completed successfully."); + } catch (err) { + execution.status = "failed"; + execution.error = err instanceof Error ? err.message : String(err); + log("ERROR", execution.error); + } + + execution.completedAt = new Date(); + + store.logExecution({ + executionId: execution.executionId, + workflowId: execution.workflowId, + agentId: config.agentId, + status: execution.status, + startedAt: execution.startedAt.toISOString(), + completedAt: execution.completedAt.toISOString(), + result: JSON.stringify({ + orientation: execution.orientation, + decision: execution.decision, + actions: execution.actions, + error: execution.error, + }), + }); + + return execution; +} diff --git a/agent-003-market-monitor/src/output.ts b/agent-003-market-monitor/src/output.ts new file mode 100644 index 0000000..6b3b790 --- /dev/null +++ b/agent-003-market-monitor/src/output.ts @@ -0,0 +1,59 @@ +import { config } from "./config.js"; +import type { OutputMessage } from "./types.js"; + +/** + * Output dispatcher. + * + * Sends agent outputs to configured channels: + * - Console (always) + * - Discord webhook (if configured) + * + * Extend this to add Telegram, Twitter, KOI object creation, etc. + * Mirrors the AGENT-002 dispatcher shape. + */ +export async function output(msg: OutputMessage): Promise { + logToConsole(msg); + + if (config.discordWebhookUrl) { + await postToDiscord(msg).catch((err) => + console.error(` Discord post failed: ${err}`) + ); + } +} + +function logToConsole(msg: OutputMessage): void { + const prefix = + msg.alertLevel === "CRITICAL" + ? "!!! CRITICAL" + : msg.alertLevel === "HIGH" + ? "!! HIGH" + : "--"; + + console.log(`\n${"=".repeat(72)}`); + console.log( + `${prefix} [${msg.workflow}] ${msg.subjectId}: ${msg.title}` + ); + console.log(`${"=".repeat(72)}`); + console.log(msg.content); + console.log(`${"=".repeat(72)}\n`); +} + +async function postToDiscord(msg: OutputMessage): Promise { + const maxLen = 1900; + let content = `**[${msg.workflow}]** ${msg.subjectId}: **${msg.title}**\n\n`; + const remaining = maxLen - content.length; + content += + msg.content.length > remaining + ? msg.content.slice(0, remaining - 20) + "\n\n*...truncated*" + : msg.content; + + await fetch(config.discordWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: config.agentName, + content, + }), + signal: AbortSignal.timeout(10_000), + }); +} diff --git a/agent-003-market-monitor/src/store.ts b/agent-003-market-monitor/src/store.ts new file mode 100644 index 0000000..cd5272e --- /dev/null +++ b/agent-003-market-monitor/src/store.ts @@ -0,0 +1,343 @@ +import Database from "better-sqlite3"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_PATH = path.join(__dirname, "..", "agent-003.db"); + +/** + * Local SQLite store for AGENT-003 state. + * + * Tracks trade observations (for trailing median + z-score), liquidity + * snapshots, retirement summaries, and workflow execution history. + * Intentionally lightweight — can be replaced with PostgreSQL later. + */ +class Store { + private db: Database.Database; + + constructor() { + this.db = new Database(DB_PATH); + this.db.pragma("journal_mode = WAL"); + this.migrate(); + } + + private migrate() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS workflow_executions ( + execution_id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + completed_at TEXT, + result TEXT + ); + + -- Trailing trade observations, used as the sample set for + -- class/batch median + z-score in WF-MM-01. Each row is one + -- observed trade; the analyzer reads rows matching the batch or + -- class within the trailing window. + CREATE TABLE IF NOT EXISTS trade_observations ( + trade_id TEXT PRIMARY KEY, + class_id TEXT NOT NULL, + batch_denom TEXT NOT NULL, + price_per_credit REAL NOT NULL, + quantity REAL NOT NULL, + executed_at TEXT NOT NULL + ); + + -- Deduplication record for WF-MM-01 — we only alert once per + -- (trade_id, severity). Re-alerts only escalate to a higher + -- severity (enforced in the workflow). + CREATE TABLE IF NOT EXISTS anomaly_alerts ( + trade_id TEXT PRIMARY KEY, + severity TEXT NOT NULL, + z_class REAL NOT NULL, + z_batch REAL NOT NULL, + detected_at TEXT NOT NULL, + report TEXT NOT NULL + ); + + -- Snapshots of liquidity health per class, used by WF-MM-02 + -- to compare against the previous snapshot and surface trends. + CREATE TABLE IF NOT EXISTS liquidity_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + snapshot TEXT NOT NULL, + health TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Summaries of retirement activity per class, used by WF-MM-03 + -- to compute a rolling demand index. + CREATE TABLE IF NOT EXISTS retirement_summaries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + window_hours INTEGER NOT NULL, + total_quantity REAL NOT NULL, + total_value_usd REAL NOT NULL, + retirement_count INTEGER NOT NULL, + demand_index REAL NOT NULL, + summary TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Per-batch retirement snapshot. WF-MM-03 reads the previous + -- retired_amount from this table and compares it against the + -- freshly observed value to derive a per-cycle retirement delta + -- (what actually moved since last run), rather than treating the + -- cumulative on-chain retired_amount as demand. + CREATE TABLE IF NOT EXISTS batch_retirement_state ( + batch_denom TEXT PRIMARY KEY, + retired_amount REAL NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_trade_class ON trade_observations(class_id); + CREATE INDEX IF NOT EXISTS idx_trade_batch ON trade_observations(batch_denom); + CREATE INDEX IF NOT EXISTS idx_trade_executed ON trade_observations(executed_at); + CREATE INDEX IF NOT EXISTS idx_liquidity_class ON liquidity_snapshots(class_id); + CREATE INDEX IF NOT EXISTS idx_retirement_class ON retirement_summaries(class_id); + `); + } + + // ── Trade observations (for z-score baseline) ────────────── + + recordTrade(obs: { + tradeId: string; + classId: string; + batchDenom: string; + pricePerCredit: number; + quantity: number; + executedAt: string; + }): void { + this.db + .prepare( + `INSERT OR IGNORE INTO trade_observations + (trade_id, class_id, batch_denom, price_per_credit, quantity, executed_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + obs.tradeId, + obs.classId, + obs.batchDenom, + obs.pricePerCredit, + obs.quantity, + obs.executedAt + ); + } + + getTradesByClass(classId: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE class_id = ? AND executed_at >= ?` + ) + .all(classId, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + getTradesByBatch(batchDenom: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE batch_denom = ? AND executed_at >= ?` + ) + .all(batchDenom, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + // ── Anomaly alerts ───────────────────────────────────────── + + hasAnomalyAlert(tradeId: string): boolean { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return !!row; + } + + getAnomalyAlertSeverity(tradeId: string): string | null { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return row?.severity || null; + } + + saveAnomalyAlert(alert: { + tradeId: string; + severity: string; + zClass: number; + zBatch: number; + report: string; + }): void { + this.db + .prepare( + `INSERT OR REPLACE INTO anomaly_alerts + (trade_id, severity, z_class, z_batch, detected_at, report) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + alert.tradeId, + alert.severity, + alert.zClass, + alert.zBatch, + new Date().toISOString(), + alert.report + ); + } + + // ── Liquidity snapshots ──────────────────────────────────── + + saveLiquiditySnapshot(classId: string, snapshot: string, health: string): void { + this.db + .prepare( + `INSERT INTO liquidity_snapshots (class_id, snapshot, health, captured_at) + VALUES (?, ?, ?, ?)` + ) + .run(classId, snapshot, health, new Date().toISOString()); + } + + getLatestLiquiditySnapshot( + classId: string + ): { snapshot: string; health: string; captured_at: string } | null { + const row = this.db + .prepare( + `SELECT snapshot, health, captured_at FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as + | { snapshot: string; health: string; captured_at: string } + | undefined; + return row || null; + } + + // ── Retirement summaries ─────────────────────────────────── + + saveRetirementSummary(row: { + classId: string; + windowHours: number; + totalQuantity: number; + totalValueUsd: number; + retirementCount: number; + demandIndex: number; + summary: string; + }): void { + this.db + .prepare( + `INSERT INTO retirement_summaries + (class_id, window_hours, total_quantity, total_value_usd, + retirement_count, demand_index, summary, captured_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + row.classId, + row.windowHours, + row.totalQuantity, + row.totalValueUsd, + row.retirementCount, + row.demandIndex, + row.summary, + new Date().toISOString() + ); + } + + // ── Per-batch retirement state (cumulative → delta) ─────── + + getPreviousRetiredAmount(batchDenom: string): number | null { + const row = this.db + .prepare( + `SELECT retired_amount FROM batch_retirement_state WHERE batch_denom = ?` + ) + .get(batchDenom) as { retired_amount: number } | undefined; + return row ? row.retired_amount : null; + } + + upsertRetiredAmount(batchDenom: string, retiredAmount: number): void { + this.db + .prepare( + `INSERT INTO batch_retirement_state (batch_denom, retired_amount, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(batch_denom) DO UPDATE SET + retired_amount = excluded.retired_amount, + updated_at = excluded.updated_at` + ) + .run(batchDenom, retiredAmount, new Date().toISOString()); + } + + /** Latest median ask price for a class, pulled from the most recent + * liquidity snapshot. Used by WF-MM-03 to value retirement volume in + * USD rather than treating quantity as 1:1 with USD. Returns null if + * no snapshot exists yet. */ + getLatestMedianAsk(classId: string): number | null { + const row = this.db + .prepare( + `SELECT snapshot FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as { snapshot: string } | undefined; + if (!row) return null; + try { + const parsed = JSON.parse(row.snapshot) as { medianAskUsd?: number }; + const m = parsed.medianAskUsd; + return typeof m === "number" && m > 0 ? m : null; + } catch { + return null; + } + } + + getBaselineDemand(classId: string, lookbackDays: number): number { + // Average of the demand_index values captured in the prior window. + // Used as the reference point for the new demand_index computation. + const cutoff = new Date(Date.now() - lookbackDays * 86_400_000).toISOString(); + const row = this.db + .prepare( + `SELECT AVG(demand_index) as avg FROM retirement_summaries + WHERE class_id = ? AND captured_at >= ?` + ) + .get(classId, cutoff) as { avg: number | null }; + return row.avg ?? 0; + } + + // ── Workflow executions ──────────────────────────────────── + + logExecution(exec: { + executionId: string; + workflowId: string; + agentId: string; + status: string; + startedAt: string; + completedAt: string; + result: string; + }): void { + this.db + .prepare( + `INSERT INTO workflow_executions + (execution_id, workflow_id, agent_id, status, started_at, completed_at, result) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + exec.executionId, + exec.workflowId, + exec.agentId, + exec.status, + exec.startedAt, + exec.completedAt, + exec.result + ); + } + + getExecutionCount(): number { + const row = this.db + .prepare("SELECT COUNT(*) as cnt FROM workflow_executions") + .get() as { cnt: number }; + return row.cnt; + } + + close(): void { + this.db.close(); + } +} + +export const store = new Store(); diff --git a/agent-003-market-monitor/src/types.ts b/agent-003-market-monitor/src/types.ts new file mode 100644 index 0000000..317ca5f --- /dev/null +++ b/agent-003-market-monitor/src/types.ts @@ -0,0 +1,158 @@ +// ============================================================ +// Regen Ledger ecocredit marketplace types +// ============================================================ + +export interface CreditClass { + id: string; + admin: string; + credit_type: CreditType; + metadata: string; +} + +export interface CreditType { + abbreviation: string; + name: string; + unit: string; + precision: number; +} + +export interface CreditBatch { + denom: string; + project_id: string; + issuer: string; + start_date: string; + end_date: string; + total_amount: string; + metadata: string; + open: boolean; +} + +export interface BatchSupply { + batch_denom: string; + tradable_amount: string; + retired_amount: string; + cancelled_amount: string; +} + +export interface SellOrder { + id: string; + seller: string; + batch_denom: string; + quantity: string; + ask_denom: string; + ask_amount: string; + disable_auto_retire: boolean; + expiration: string; +} + +/** Synthesized trade record (from filled sell orders or tx logs) */ +export interface Trade { + id: string; + batchDenom: string; + classId: string; + seller: string; + buyer: string; + quantity: number; + pricePerCredit: number; // USD-equivalent + askDenom: string; + executedAt: string; +} + +/** Synthesized retirement record */ +export interface Retirement { + txHash: string; + batchDenom: string; + classId: string; + retiree: string; + quantity: number; + jurisdiction: string | null; + reason: string | null; + retiredAt: string; +} + +// ============================================================ +// OODA loop types (shared shape mirrors agent-002) +// ============================================================ + +export interface OODAExecution { + executionId: string; + workflowId: string; + status: "running" | "completed" | "failed" | "escalated"; + observations: TObserve; + orientation: TOrient | null; + decision: TDecide | null; + actions: TAct | null; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +// ============================================================ +// Workflow-specific types +// ============================================================ + +export type AlertLevel = "NORMAL" | "HIGH" | "CRITICAL"; +export type AnomalySeverity = "INFO" | "WARNING" | "CRITICAL"; + +/** Price anomaly detection (WF-MM-01) */ +export interface PriceAnomaly { + tradeId: string; + batchDenom: string; + classId: string; + seller: string; + quantity: number; + pricePerCredit: number; + classMedian: number; + batchMedian: number; + zScoreVsClass: number; + zScoreVsBatch: number; + sampleSizeClass: number; + sampleSizeBatch: number; + severity: AnomalySeverity; + detectedAt: string; + confidence: number; +} + +/** Liquidity monitoring (WF-MM-02) */ +export interface LiquiditySnapshot { + classId: string; + totalListedQuantity: number; + totalListedValueUsd: number; + sellOrderCount: number; + lowestAskUsd: number; + highestAskUsd: number; + medianAskUsd: number; + meanAskUsd: number; + depthUsd: number; // sum of top-10 sell orders + healthScore: number; // 0-1 + health: "HEALTHY" | "DEGRADED" | "CRITICAL"; + capturedAt: string; +} + +/** Retirement tracking (WF-MM-03) */ +export interface RetirementSummary { + classId: string; + windowHours: number; + retirementCount: number; + totalQuantity: number; + totalValueUsd: number; + uniqueRetirees: number; + topRetiree: string | null; + topRetireeQuantity: number; + pctWithJurisdiction: number; + demandIndex: number; // 0-100, relative to trailing baseline + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; // trade id / class id / batch denom + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts new file mode 100644 index 0000000..3638787 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -0,0 +1,244 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeLiquidity } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + LiquiditySnapshot, + SellOrder, + CreditClass, + AlertLevel, +} from "../types.js"; + +/** + * WF-MM-02: Liquidity Monitoring & Reporting + * + * Trigger: periodic (hourly) OR significant_trade (volume > $10k) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch all open sell orders, group by credit class + * Orient — Compute liquidity metrics per class (listed value, + * bid-ask spread proxy, top-10 depth, health score) + * Decide — Generate per-class liquidity reports. Flag classes + * whose depth falls below the configured floor. + * Act — Persist snapshot, output report, alert on degradation. + */ + +interface Observations { + classes: CreditClass[]; + ordersByClass: Map; +} + +interface Orientation { + snapshots: LiquiditySnapshot[]; +} + +interface Decision { + reports: { + snapshot: LiquiditySnapshot; + previous: LiquiditySnapshot | null; + report: string; + alertLevel: AlertLevel; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +function askUsd(order: SellOrder): number { + const ask = Number(order.ask_amount); + const qty = Number(order.quantity); + if (!Number.isFinite(ask) || !Number.isFinite(qty) || qty <= 0) return 0; + // MVP: treat ask as already-USD; price oracle integration is future work. + return ask / qty; +} + +function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +function scoreHealth( + depthUsd: number, + sellOrderCount: number +): { score: number; tier: LiquiditySnapshot["health"] } { + const depthRatio = Math.min(1, depthUsd / (config.market.liquidityDepthFloor * 2)); + const countScore = Math.min(1, sellOrderCount / 20); + const score = depthRatio * 0.7 + countScore * 0.3; + + let tier: LiquiditySnapshot["health"]; + if (depthUsd < config.market.liquidityDepthFloor * 0.5 || score < 0.3) { + tier = "CRITICAL"; + } else if (depthUsd < config.market.liquidityDepthFloor || score < 0.6) { + tier = "DEGRADED"; + } else { + tier = "HEALTHY"; + } + + return { score, tier }; +} + +function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +export function createLiquidityMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-02", + name: "Liquidity Monitoring & Reporting", + + async observe(): Promise { + const [classes, sellOrders] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getSellOrders(500), + ]); + + const ordersByClass = new Map(); + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const bucket = ordersByClass.get(classId) || []; + bucket.push(order); + ordersByClass.set(classId, bucket); + } + + return { classes, ordersByClass }; + }, + + async orient(obs: Observations): Promise { + const snapshots: LiquiditySnapshot[] = []; + const capturedAt = new Date().toISOString(); + + // Snapshot every class that currently has any open sell orders. + // Classes with zero open orders are intentionally skipped — + // that's a "no market" state, distinct from a degraded market. + for (const [classId, orders] of obs.ordersByClass) { + if (orders.length === 0) continue; + + const prices = orders + .map((o) => askUsd(o)) + .filter((p) => p > 0) + .sort((a, b) => a - b); + if (prices.length === 0) continue; + + const quantities = orders + .map((o) => Number(o.quantity)) + .filter((q) => Number.isFinite(q) && q > 0); + const totalListedQuantity = quantities.reduce((a, b) => a + b, 0); + + const listedValues = orders.map((o) => { + const p = askUsd(o); + const q = Number(o.quantity); + return Number.isFinite(p) && Number.isFinite(q) ? p * q : 0; + }); + const totalListedValueUsd = listedValues.reduce((a, b) => a + b, 0); + + // Pre-compute askUsd once per order so the comparator is a plain + // number subtraction instead of recomputing the ratio on every + // sort invocation (sort can call the comparator O(n log n) times). + const pricedOrders = orders + .map((order) => ({ order, price: askUsd(order) })) + .sort((a, b) => a.price - b.price); + const top10 = pricedOrders.slice(0, 10); + const depthUsd = top10.reduce((acc, { order, price }) => { + const q = Number(order.quantity); + return acc + (Number.isFinite(price) && Number.isFinite(q) ? price * q : 0); + }, 0); + + const lowestAskUsd = prices[0]!; + const highestAskUsd = prices[prices.length - 1]!; + const medianAskUsd = median(prices); + const meanAskUsd = prices.reduce((a, b) => a + b, 0) / prices.length; + + const { score, tier } = scoreHealth(depthUsd, orders.length); + + snapshots.push({ + classId, + totalListedQuantity, + totalListedValueUsd, + sellOrderCount: orders.length, + lowestAskUsd, + highestAskUsd, + medianAskUsd, + meanAskUsd, + depthUsd, + healthScore: score, + health: tier, + capturedAt, + }); + } + + return { snapshots }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const snapshot of orientation.snapshots) { + const prevRow = store.getLatestLiquiditySnapshot(snapshot.classId); + let previous: LiquiditySnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as LiquiditySnapshot; + } catch { + previous = null; + } + } + + const report = await describeLiquidity(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "DEGRADED" + ? "HIGH" + : "NORMAL"; + + reports.push({ snapshot, previous, report, alertLevel }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { snapshot, report, alertLevel } of decision.reports) { + store.saveLiquiditySnapshot( + snapshot.classId, + JSON.stringify(snapshot), + snapshot.health + ); + saved++; + + await output({ + workflow: "WF-MM-02", + subjectId: snapshot.classId, + title: `Liquidity ${snapshot.health} (score ${snapshot.healthScore.toFixed(2)})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + if (alertLevel !== "NORMAL") alertsSent++; + } + + if (decision.reports.length === 0) { + console.log(" No credit classes with open sell orders."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts new file mode 100644 index 0000000..b531629 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -0,0 +1,266 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePriceAnomaly } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Trade, + PriceAnomaly, + AnomalySeverity, + SellOrder, +} from "../types.js"; + +/** + * WF-MM-01: Price Anomaly Detection + * + * Trigger: SellOrderCreated OR SellOrderFilled + * Layer: 1 (alerts) / Layer 2 (if action needed) + * SLA: Alert within one polling cycle of the trade landing on-chain + * + * OODA: + * Observe — Fetch current sell orders + record as synthetic trade + * observations. When Ledger MCP provides real fill data, + * the observe phase swaps in real trades. + * Orient — For each new observation, compute z-score vs class + * median and batch median using the trailing window. + * Decide — Classify severity (INFO / WARNING / CRITICAL) per the + * thresholds in config.market. Only surface WARNING or + * CRITICAL. Dedupe against previous alerts for this trade. + * Act — Persist, generate narrative via Claude, output to channels. + * + * Determinism: all numeric decisions (median, z-score, severity) are + * computed locally. Claude is only used for the narrative report. + */ + +interface Observations { + newTrades: Trade[]; +} + +interface Orientation { + anomalies: PriceAnomaly[]; +} + +interface Decision { + alertsToPublish: PriceAnomaly[]; +} + +interface Actions { + alerted: number; + skipped: number; +} + +/** + * Convert a sell order into a synthetic "trade observation" for this + * agent. Until Ledger MCP provides filled-trade events, the agent uses + * the ask price as a proxy signal — an unusually high/low ask price + * still indicates a market structure anomaly worth surfacing. + */ +function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { + const qty = Number(order.quantity); + const ask = Number(order.ask_amount); + if (!Number.isFinite(qty) || !Number.isFinite(ask) || qty <= 0) return null; + + // Only USD stablecoin asks are priced — anything else would require + // an oracle we don't have yet, and letting non-USD orders into the + // baseline would pollute the class/batch medians with 1:1-priced + // garbage. Downstream code can plug in a real price oracle. + if (!isUsdStableDenom(order.ask_denom)) return null; + const pricePerCredit = ask / qty; + + return { + id: order.id, + batchDenom: order.batch_denom, + classId, + seller: order.seller, + buyer: "", + quantity: qty, + pricePerCredit, + askDenom: order.ask_denom, + executedAt: new Date().toISOString(), + }; +} + +function isUsdStableDenom(denom: string): boolean { + const d = denom.toLowerCase(); + return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); +} + +function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +function stddev(values: number[], mean: number): number { + if (values.length <= 1) return 0; + const sumSq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0); + return Math.sqrt(sumSq / (values.length - 1)); +} + +function classifyAnomaly( + zClass: number, + zBatch: number +): AnomalySeverity { + const maxZ = Math.max(Math.abs(zClass), Math.abs(zBatch)); + if (maxZ >= config.market.criticalZScore) return "CRITICAL"; + if (maxZ >= config.market.warningZScore) return "WARNING"; + return "INFO"; +} + +function classIdFromBatchDenom(denom: string): string { + // Regen batch denoms look like C01-001-20240101-20241231-001. + // The class id is the leading token before the first dash. + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +export function createPriceAnomalyDetectionWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-01", + name: "Price Anomaly Detection", + + async observe(): Promise { + const sellOrders = await ledger.getSellOrders(200); + + const newTrades: Trade[] = []; + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const trade = sellOrderToTrade(order, classId); + if (trade) { + newTrades.push(trade); + store.recordTrade({ + tradeId: trade.id, + classId: trade.classId, + batchDenom: trade.batchDenom, + pricePerCredit: trade.pricePerCredit, + quantity: trade.quantity, + executedAt: trade.executedAt, + }); + } + } + + return { newTrades }; + }, + + async orient(obs: Observations): Promise { + const anomalies: PriceAnomaly[] = []; + + for (const trade of obs.newTrades) { + const classSamples = store.getTradesByClass( + trade.classId, + config.market.classMedianWindowDays + ); + const batchSamples = store.getTradesByBatch( + trade.batchDenom, + config.market.batchMedianWindowDays + ); + + if (classSamples.length < config.market.minSamples) continue; + + const classMedian = median(classSamples); + const classMean = classSamples.reduce((a, b) => a + b, 0) / classSamples.length; + const classStd = stddev(classSamples, classMean); + // Z-score is (value − mean) / stddev. Using the median in the + // numerator while dividing by stddev (which is computed off the + // mean) is statistically inconsistent. Medians are still + // published in the alert payload as a context signal. + const zClass = classStd > 0 ? (trade.pricePerCredit - classMean) / classStd : 0; + + const batchMedian = batchSamples.length > 0 ? median(batchSamples) : classMedian; + const batchMean = + batchSamples.length > 0 + ? batchSamples.reduce((a, b) => a + b, 0) / batchSamples.length + : classMean; + const batchStd = batchSamples.length > 1 ? stddev(batchSamples, batchMean) : classStd; + const zBatch = batchStd > 0 ? (trade.pricePerCredit - batchMean) / batchStd : 0; + + const severity = classifyAnomaly(zClass, zBatch); + if (severity === "INFO") continue; + + // Confidence rises with sample size and drops when class and + // batch z-scores disagree (which often indicates the batch is + // just thinly traded rather than a real anomaly). + const sampleConfidence = Math.min(1, classSamples.length / 30); + const agreement = + Math.sign(zClass) === Math.sign(zBatch) && Math.abs(zBatch) > 0 + ? 1 + : 0.6; + const confidence = Math.max(0, Math.min(1, sampleConfidence * agreement)); + + anomalies.push({ + tradeId: trade.id, + batchDenom: trade.batchDenom, + classId: trade.classId, + seller: trade.seller, + quantity: trade.quantity, + pricePerCredit: trade.pricePerCredit, + classMedian, + batchMedian, + zScoreVsClass: zClass, + zScoreVsBatch: zBatch, + sampleSizeClass: classSamples.length, + sampleSizeBatch: batchSamples.length, + severity, + detectedAt: new Date().toISOString(), + confidence, + }); + } + + return { anomalies }; + }, + + async decide(orientation: Orientation): Promise { + const alertsToPublish: PriceAnomaly[] = []; + + for (const anomaly of orientation.anomalies) { + const existing = store.getAnomalyAlertSeverity(anomaly.tradeId); + // Dedupe: don't re-alert at the same or lower severity. + if (existing === "CRITICAL") continue; + if (existing === "WARNING" && anomaly.severity !== "CRITICAL") continue; + + alertsToPublish.push(anomaly); + } + + return { alertsToPublish }; + }, + + async act(decision: Decision): Promise { + let alerted = 0; + let skipped = 0; + + for (const anomaly of decision.alertsToPublish) { + const report = await describePriceAnomaly(anomaly); + + store.saveAnomalyAlert({ + tradeId: anomaly.tradeId, + severity: anomaly.severity, + zClass: anomaly.zScoreVsClass, + zBatch: anomaly.zScoreVsBatch, + report, + }); + + await output({ + workflow: "WF-MM-01", + subjectId: anomaly.tradeId, + title: `${anomaly.severity} — ${anomaly.classId} / ${anomaly.batchDenom}`, + content: report, + alertLevel: anomaly.severity === "CRITICAL" ? "CRITICAL" : "HIGH", + timestamp: new Date(), + }); + alerted++; + } + + if (decision.alertsToPublish.length === 0) { + console.log(" No new price anomalies above threshold."); + } + + return { alerted, skipped }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts new file mode 100644 index 0000000..b7223d1 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -0,0 +1,264 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeRetirementSummary } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + CreditClass, + CreditBatch, + BatchSupply, + RetirementSummary, +} from "../types.js"; + +/** + * WF-MM-03: Retirement Pattern Analysis + * + * Trigger: MsgRetire event (proxied by per-cycle retired-amount deltas + * until the Ledger MCP provides an event stream) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch credit classes + batches + batch supplies. The + * supply response includes `retired_amount`, which we + * compare against the previously observed amount to derive + * a per-class retirement delta for this cycle. + * Orient — Build a RetirementSummary per class: totals, unique + * retirees proxied by batch count for this MVP, demand + * index derived from quantity vs trailing baseline. + * Decide — Generate narrative summary for classes that moved. + * Act — Persist, output, alert on large positive demand shifts. + * + * This MVP uses the batch supply query as the retirement source. A + * follow-up PR will plug in a real tx-stream client (proto.regen. + * ecocredit.v1.MsgRetire) once the LCD event endpoint is available. + */ + +interface BatchWithSupply { + batch: CreditBatch; + supply: BatchSupply; + classId: string; +} + +interface Observations { + classes: CreditClass[]; + batchesWithSupply: BatchWithSupply[]; +} + +interface Orientation { + summariesByClass: Map; +} + +interface Decision { + reports: { + summary: RetirementSummary; + baselineDemand: number; + report: string; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** Demand index on a bounded 0-100 scale. Inputs are rolling and a + * class with no trailing activity gets 0. The index is intentionally + * simple — it exists so the narrative layer has a single number to + * anchor the "demand up / demand down" story. */ +function computeDemandIndex( + totalQuantity: number, + retirementCount: number, + uniqueRetirees: number +): number { + const volumeComponent = Math.min(60, Math.log10(Math.max(1, totalQuantity)) * 20); + const countComponent = Math.min(20, retirementCount * 2); + const breadthComponent = Math.min(20, uniqueRetirees * 4); + return Math.round(volumeComponent + countComponent + breadthComponent); +} + +export function createRetirementTrackingWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-03", + name: "Retirement Pattern Analysis", + + async observe(): Promise { + const [classes, batches] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getCreditBatches(), + ]); + + // Cap the per-cycle batch fan-out to the most recent 100 batches + // so we don't hammer the LCD on mainnet (where batch counts grow + // unbounded). Older batches are retired less frequently and the + // agent will catch new retirement activity next cycle anyway. + const cappedBatches = batches.slice(0, 100); + + // Limit concurrency to 5 in-flight LCD requests at a time. + // `Promise.all(cappedBatches.map(...))` fires 100 concurrent + // requests and will trip rate limits on public LCD endpoints. + const batchesWithSupply: BatchWithSupply[] = []; + const CONCURRENCY = 5; + for (let i = 0; i < cappedBatches.length; i += CONCURRENCY) { + const chunk = cappedBatches.slice(i, i + CONCURRENCY); + const results = await Promise.all( + chunk.map(async (batch) => { + const supply = await ledger.getBatchSupply(batch.denom); + return supply + ? { + batch, + supply, + classId: classIdFromBatchDenom(batch.denom), + } + : null; + }) + ); + for (const r of results) if (r) batchesWithSupply.push(r); + } + + return { classes, batchesWithSupply }; + }, + + async orient(obs: Observations): Promise { + const summariesByClass = new Map(); + const capturedAt = new Date().toISOString(); + + // Group batches by class so we can aggregate retired amounts. + const byClass = new Map(); + for (const row of obs.batchesWithSupply) { + const bucket = byClass.get(row.classId) || []; + bucket.push(row); + byClass.set(row.classId, bucket); + } + + for (const [classId, rows] of byClass) { + // Per-batch delta: compare the currently observed cumulative + // retired_amount against the previously stored value and sum + // the positive deltas. That's the actual retirement activity + // that moved this cycle, rather than the cumulative total + // (which would fluctuate based on which batches fall into the + // top 100 and would over-report for newly-observed batches). + let totalRetiredDelta = 0; + let batchesWithRetirement = 0; + for (const row of rows) { + const currentRetired = Number(row.supply.retired_amount); + if (!Number.isFinite(currentRetired) || currentRetired < 0) continue; + + const previous = store.getPreviousRetiredAmount(row.batch.denom); + // If we have no prior observation, treat this as the + // baseline read — record it but do not count it toward this + // cycle's delta (no basis to know what moved *this* cycle). + if (previous === null) { + store.upsertRetiredAmount(row.batch.denom, currentRetired); + continue; + } + + const delta = currentRetired - previous; + if (delta > 0) { + totalRetiredDelta += delta; + batchesWithRetirement++; + } + // Persist the new cumulative value so the next cycle sees + // today's read as the baseline. Always update, even if + // delta === 0, to advance the updated_at timestamp. + store.upsertRetiredAmount(row.batch.denom, currentRetired); + } + if (totalRetiredDelta === 0) continue; + + // Unique retirees / top retiree are placeholders until the + // tx-event source lands. The summary still tells the right + // story: "N batches in class X have retirements totaling Y". + const uniqueRetirees = batchesWithRetirement; + const topRetiree: string | null = null; + const topRetireeQuantity = 0; + + // Price retirement volume using the most recent median ask + // price from WF-MM-02's liquidity snapshot for this class. + // Falls back to 1:1 when no snapshot exists yet (first run). + const medianAsk = store.getLatestMedianAsk(classId); + const totalValueUsd = + medianAsk !== null ? totalRetiredDelta * medianAsk : totalRetiredDelta; + const demandIndex = computeDemandIndex( + totalRetiredDelta, + batchesWithRetirement, + uniqueRetirees + ); + + summariesByClass.set(classId, { + classId, + windowHours: config.market.retirementWindowHours, + retirementCount: batchesWithRetirement, + totalQuantity: totalRetiredDelta, + totalValueUsd, + uniqueRetirees, + topRetiree, + topRetireeQuantity, + pctWithJurisdiction: 0, + demandIndex, + capturedAt, + }); + } + + return { summariesByClass }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const summary of orientation.summariesByClass.values()) { + const baselineDemand = store.getBaselineDemand(summary.classId, 7); + const report = await describeRetirementSummary(summary, baselineDemand); + reports.push({ summary, baselineDemand, report }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { summary, baselineDemand, report } of decision.reports) { + store.saveRetirementSummary({ + classId: summary.classId, + windowHours: summary.windowHours, + totalQuantity: summary.totalQuantity, + totalValueUsd: summary.totalValueUsd, + retirementCount: summary.retirementCount, + demandIndex: summary.demandIndex, + summary: report, + }); + saved++; + + // Alert if demand index jumped meaningfully above the baseline. + // Threshold of +15 index points mirrors the agent character's + // "moderate-high" boundary (65 → 72 example). + const jumped = baselineDemand > 0 && summary.demandIndex - baselineDemand >= 15; + const alertLevel = jumped ? "HIGH" : "NORMAL"; + if (alertLevel !== "NORMAL") alertsSent++; + + await output({ + workflow: "WF-MM-03", + subjectId: summary.classId, + title: `Retirement summary (demand ${summary.demandIndex}${baselineDemand ? ` vs ${baselineDemand.toFixed(0)}` : ""})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + } + + if (decision.reports.length === 0) { + console.log(" No retirement activity detected in any class."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/tsconfig.json b/agent-003-market-monitor/tsconfig.json new file mode 100644 index 0000000..4a4a4f3 --- /dev/null +++ b/agent-003-market-monitor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}