diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/RIGHTS.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/RIGHTS.md new file mode 100644 index 00000000..393218a5 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/RIGHTS.md @@ -0,0 +1,14 @@ +# Submission rights declaration + +Project: `Agent Acceptance Gate` +Submission slug: `runesleo-agent-acceptance-gate` +Submitter: `Leo / runesleo` +Date: `2026-09-17` + +I own, or have sufficient authorization for, the source code, service, data, branding, and other materials submitted in this pull request. I authorize X-Agent to retain, review, archive, and evaluate this submission under the official X-Agent MCP Hackathon 2026 rules. + +Authorization basis: owner Leo explicitly approved public repository visibility, Cloudflare Worker deployment of commit `15c007b4a785a1263df989bb72c6e10e4b60ddf4` to `https://api.leolabs.me`, and official PR submission to `xagentAI/xagt-plugin` on 2026-09-17 ("部署呗 不搞白不搞"). + +Third-party components and their licenses: Node.js built-ins; Cloudflare Wrangler 4.133.0 as development/deployment tooling (pinned in `source/package-lock.json`); Cloudflare Workers as the hosting runtime. Review upstream license notices as applicable. + +Exceptions or restrictions: The source repository currently has no standalone `LICENSE` file. The existing paid x402 surfaces on the same Worker remain separate from this reviewer capability and are not part of the hackathon claim beyond noting they are unchanged. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/SUBMISSION.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/SUBMISSION.md new file mode 100644 index 00000000..4b2bbe47 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/SUBMISSION.md @@ -0,0 +1,54 @@ +# Agent Acceptance Gate + +## Capability + +- **One-line description:** Deterministically audit an AI-agent delivery before acceptance and return a verdict, evidence gaps, risks, and the next safe gate. +- **Who it helps:** Agent marketplaces, buyers, evaluators, and orchestration systems that need a machine-readable acceptance gate. +- **Capability boundary:** Read-only evaluation of supplied evidence. It does not inspect a private repository, execute validation commands, sign, settle, pay, trade, mutate files, or change an account. +- **Track:** General Challenge (Open Innovation) + +## Live API + +- **API base URL:** https://api.leolabs.me +- **Capability URL:** https://api.leolabs.me/xagent/agent-delivery-acceptance-audit +- **Health-check URL:** https://api.leolabs.me/health +- **Deployment proof URL:** https://api.leolabs.me/.well-known/xagent-verification.json +- **Authentication:** None during the bounded reviewer window. The route is disabled unless `XAGENT_REVIEW_ENABLED=true` is explicitly deployed. +- **Rate limits / known limits:** Cloudflare Worker platform limits apply; JSON request bodies are capped at 1 MiB. The audit is deterministic and makes no outbound call on this route. +- **API contract:** `source/openapi.yaml` + +## Source and reproducibility + +- **Source repository:** https://github.com/runesleo/agent-acceptance-gate +- **Review commit:** `15c007b4a785a1263df989bb72c6e10e4b60ddf4` +- **Public branch:** `codex/xagent-mcp-hackathon-prep-20260917` +- **Source submitted in this PR:** `source/` +- **Setup:** `npm ci` +- **Run tests:** `npm test && npm run worker:check` +- **Run locally:** `npx wrangler dev --var XAGENT_GIT_COMMIT:15c007b4a785a1263df989bb72c6e10e4b60ddf4 --var XAGENT_PROJECT_SLUG:runesleo-agent-acceptance-gate --var XAGENT_REVIEW_ENABLED:true` +- **Deploy:** `npm run deploy:worker -- --var XAGENT_GIT_COMMIT:15c007b4a785a1263df989bb72c6e10e4b60ddf4 --var XAGENT_PROJECT_SLUG:runesleo-agent-acceptance-gate --var XAGENT_REVIEW_ENABLED:true` +- **Version binding:** The Worker returns `15c007b4a785a1263df989bb72c6e10e4b60ddf4` from `/health` and from the same-origin verification document. Missing or malformed deployment identity fails closed with HTTP 503. + +Unrelated `research/demo-video-cn/` media is excluded. Worker implementation, tests, API contract, config example, and dependency lock are included. + +## Verification + +Repeatable commands and live response fixtures are in `verification/README.md`. + +- **Offline health/proof contract:** Covered by `npm run test:xagent`. +- **Capability call:** `POST /xagent/agent-delivery-acceptance-audit` with `verification/request.json`. +- **Expected error behavior:** Missing `delivery_summary` returns HTTP 400; a disabled review route returns HTTP 404; missing deployment identity returns HTTP 503. + +## Security and data handling + +- **Data collected:** Request fields describing a task and delivery evidence. No wallet seed, signature, payment credential, or customer record is required. +- **Purpose and retention:** The reviewer route evaluates the body in memory and does not persist it. +- **Third parties / outbound network calls:** Cloudflare Workers hosts the API. This reviewer route makes no outbound call. Existing paid routes have separate x402/OKX behavior and are not changed by this review route. +- **Secrets:** No secrets are committed. Deployment values in `config/xagent-review.env.example` are non-secret bindings. +- **Known risks / restrictions:** The result is a deterministic evidence-quality judgment, not a security certification. It relies on caller-supplied evidence and intentionally preserves human approval gates. + +## Support + +- **Team / builder:** Leo / `runesleo` +- **Contact:** GitHub `@runesleo` via the source repository +- **License / rights:** See `RIGHTS.md`. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/.gitignore b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/.gitignore new file mode 100644 index 00000000..a9add931 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/.gitignore @@ -0,0 +1,18 @@ +node_modules/ +.DS_Store +.env +.env.* +generated-audits/ +coverage/ +dist/ +*.log + +.secrets.env +.dev.vars +.wrangler/ + +# demo intermediates +research/demo-video-cn/audio/ +research/demo-video-cn/part-*.mp4 +research/demo-video-cn/slides/ +research/demo-video-cn/concat.txt diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/AGENTS.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/AGENTS.md new file mode 100644 index 00000000..f0f5fbaf --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/AGENTS.md @@ -0,0 +1,41 @@ +# Agent Instructions + +Default communication: Chinese. + +## Scope + +This repo is a local-only prototype for `Agent Acceptance Gate`. + +It should stay agent-first and human-readable: + +- agents call the API/tool; +- humans review the result and authorize next gates. + +## Allowed local work + +- Edit source, docs, schemas, discovery metadata, demo files, and tests. +- Run `npm test`. +- Run `npm run serve` locally. +- Add more sample inputs/outputs from public-safe or local worker writebacks. + +## Hard gates + +Do not do these without explicit Leo approval: + +- push to any remote; +- create public GitHub repo or change repo visibility; +- deploy a public endpoint; +- submit OKX.AI ASP listing; +- connect OKX Agentic Wallet, API credentials, wallet address, x402, or payment middleware; +- publish to leolabs / X as official launch; +- claim legal, investment, smart-contract security, or guaranteed correctness. + +## Product boundary + +This is not an observability platform, full code review tool, security auditor, wallet tool, or general AI evaluation framework. + +Primary positioning: + +```text +Can this agent delivery be accepted? +``` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/CLAUDE_INDEPENDENT_RESEARCH_PROMPT.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/CLAUDE_INDEPENDENT_RESEARCH_PROMPT.md new file mode 100644 index 00000000..677e48af --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/CLAUDE_INDEPENDENT_RESEARCH_PROMPT.md @@ -0,0 +1,93 @@ +# Claude Independent Research Prompt + +Purpose: let Claude independently re-evaluate the OKX.AI / Agent commerce opportunity without being anchored by the current Codex-built product. + +Use this tomorrow in a fresh Claude thread. + +## Phase 1: Blind market research + +Do **not** read `/Users/zhangxu/Projects/agent-acceptance-gate` yet. + +Start from first principles and current market evidence. + +Research: + +1. What will the OKX.AI / agent-commerce / A2A / A2MCP market likely become? +2. In a world where agents hire agents, call tools, spend budgets, deliver work, release escrow, and dispute outcomes, which services are naturally high-frequency? +3. Which services have the largest market capacity or highest willingness to pay? +4. Which services are likely to be platform-owned vs third-party ASP opportunities? +5. What should a one-person company build first if the target is meaningful revenue, not just a hackathon demo? + +Required sources: + +- OKX.AI official docs: ASP, A2MCP, A2A, Evaluator, ASP registration. +- OKX / X Layer hackathon announcement and Star_OKX $1M OPC tweet. +- Agentic payments: x402, AP2, UCP, OKX Payment SDK. +- MCP ecosystem: tool directories, MCP adoption, MCP security/market research. +- Agent marketplaces: RentAHuman or similar task/escrow systems. +- Agent eval / observability / QA companies: LangSmith, Braintrust, HoneyHive, Arize. + +Output format: + +```text +1. Market structure map +2. High-frequency service demand map +3. Highest-revenue opportunity ranking +4. Risks / platform-owned areas +5. Recommended wedge for Leo +6. 7-day execution plan +7. Kill criteria +``` + +Important: + +- Do not assume `Agent Acceptance Gate` is correct. +- Do not optimize for what Codex already built. +- If the best answer is unrelated to acceptance/verification, say so. +- Separate "strategically interesting" from "currently demanded." + +## Phase 2: Compare against Codex repo + +Only after Phase 1 is complete, read: + +```text +/Users/zhangxu/Projects/agent-acceptance-gate +``` + +Then compare: + +1. Where does the existing repo match your independent market thesis? +2. Where is it overfit to Codex's assumptions? +3. What should be kept? +4. What should be renamed, pivoted, or killed? +5. What is the next concrete step if Leo wants to pursue the strongest opportunity? + +Output format: + +```text +Claude independent conclusion: + +Keep: +Change: +Kill: +New direction: +Next 24h: +Next 7d: +Hard gates: +``` + +## Hard gates + +Do not do any of these without explicit Leo approval: + +- OKX.AI account / Agentic Wallet login; +- API key / credential setup; +- wallet address / funding / signing / transaction / staking; +- payment middleware / x402 / OKX Payment SDK integration; +- ASP listing submission; +- GitHub remote creation or public repo; +- push / deploy / production endpoint; +- leolabs public publish. + +Read-only web research and local notes are allowed. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/DEPLOYMENT.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/DEPLOYMENT.md new file mode 100644 index 00000000..a28f7952 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/DEPLOYMENT.md @@ -0,0 +1,72 @@ +# Deployment + +Status: worker_api_live_xagent_patch_local_only + +## Public surfaces currently live + +- Static demo: https://agent-acceptance-gate.pages.dev/ +- Cloudflare Worker API: https://api.leolabs.me +- Current production health: https://api.leolabs.me/health +- Existing paid service catalogue: https://api.leolabs.me/api/okx-ai-services + +The existing Worker API is live. The X-Agent commit-bound health response, +same-origin verification document, and reviewer POST route prepared on +`codex/xagent-mcp-hackathon-prep-20260917` are **not yet deployed**. + +## X-Agent deployment identity + +A review deployment must set all three values. Start from +`config/xagent-review.env.example` and bind the exact public source commit: + +```text +XAGENT_GIT_COMMIT= +XAGENT_PROJECT_SLUG=runesleo-agent-acceptance-gate +XAGENT_REVIEW_ENABLED=true +``` + +`XAGENT_GIT_COMMIT` must match the public source commit submitted for review. +With a missing or malformed commit, `/health` and the verification endpoint +fail closed with HTTP 503. When `XAGENT_REVIEW_ENABLED` is unset or false, the +reviewer POST route returns HTTP 404 and the existing x402 route remains intact. + +## Local verification + +```bash +npm test +npm run worker:check +npm run test:xagent +npm run test:xagent-submission +``` + +## Deployment command + +After the source commit is public and Leo explicitly approves deployment, +inject the non-secret Worker variables explicitly through Wrangler: + +```bash +npm run deploy:worker -- \ + --var XAGENT_GIT_COMMIT: \ + --var XAGENT_PROJECT_SLUG:runesleo-agent-acceptance-gate \ + --var XAGENT_REVIEW_ENABLED:true +``` + +For a local Worker check, use the same `--var` arguments with `npx wrangler dev`. +Shell environment variables alone are not treated as Worker bindings. + +After deployment, verify: + +```bash +curl --fail --silent --show-error https://api.leolabs.me/health +curl --fail --silent --show-error https://api.leolabs.me/.well-known/xagent-verification.json +``` + +The two responses must expose the same exact 40-character commit as the public +review commit. A reviewer capability call is documented in the submission +verification packet. + +## Rollback + +Rollback means redeploying the previously verified Worker source and restoring +its prior environment configuration. Do not deploy, alter Worker variables, +push source, or open the official submission PR without explicit Leo approval. +The static Pages demo is a separate surface and is not changed by this patch. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/PROJECT_SSOT.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/PROJECT_SSOT.md new file mode 100644 index 00000000..afe3f5f9 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/PROJECT_SSOT.md @@ -0,0 +1,73 @@ +# PROJECT SSOT + +Project: Agent Acceptance Gate +Chinese name: Agent 验收门禁 +Status: local_repo_private_static_demo_public +Owner thread: product_distribution / cmd5 +Created: 2026-07-02 + +## Purpose + +Agent-first, human-readable acceptance gate for AI agent deliveries. + +The service is designed for an agent marketplace flow: + +```text +Seller Agent finishes work +Buyer Agent / Seller Agent / Evaluator Agent calls audit_agent_delivery +Service returns pass / needs_review / fail +Human reviews result and authorizes next gate +``` + +## Current state + +Runnable prototype with public static demo: + +- deterministic audit engine; +- CLI; +- local HTTP API; +- buyer-facing demo; +- OpenAPI draft; +- MCP-style tool manifest; +- agent discovery metadata; +- launch/billing drafts; +- public static demo at https://agent-acceptance-gate.pages.dev/. + +## Not launched + +The project is not: + +- submitted to OKX.AI; +- connected to wallet/payment middleware; +- connected to API keys or credentials; +- pushed to GitHub; +- published on leolabs. + +Only `demo/index.html` is publicly deployed as a static Cloudflare Pages demo. + +## Hard gates + +Require explicit Leo approval before: + +- creating a public GitHub repo or changing visibility; +- adding remote origin / pushing; +- deploying public endpoint; +- OKX.AI Agentic Wallet login or ASP listing; +- adding wallet address / payment middleware / x402 / OKX Payment SDK; +- publishing leolabs or X announcement as an official launch; +- claiming security, legal, investment, or smart-contract audit coverage. + +## Validation + +Current local validation: + +```bash +npm test +``` + +Expected: + +```text +PASS 5/5 sample audit cases +PASS http smoke +``` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/README.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/README.md new file mode 100644 index 00000000..9258c9da --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/README.md @@ -0,0 +1,173 @@ +# OKX.AI ASP Package: Agent Deliverable Auditor + +Created: 2026-07-02 +Owner thread: product_distribution / cmd5 +Status: live_worker_api_x402; X-Agent patch local_only_not_deployed + +## Bottom line + +`Agent Acceptance Gate` is a live OKX.AI/A2MCP service and an X-Agent MCP Hackathon reuse candidate. + +It checks whether an AI agent transaction can continue before task acceptance, paid service calls, delivery acceptance, payment release, or dispute review. + +The public Worker is live at `https://api.leolabs.me`. The X-Agent version-binding and reviewer route in the isolated preparation branch are not yet deployed; public deployment, push, and submission remain explicit owner gates. + +The service does not execute wallet actions, agent tasks, repo mutations, or deployments. It reads a task prompt, writeback, artifact list, changed files, validation output, declared hard gates, and next gate. It returns a structured verdict. + +## Why this exists + +OKX.AI creates a marketplace where users can hire Agents, ASPs can sell services, and Evaluators can arbitrate disputes. That marketplace needs a boring but valuable layer: + +```text +Can this agent transaction continue? +``` + +The first sellable wedge is delivery acceptance. The larger product is an agent transaction gate. + +## Package contents + +- `marketplace-listing-draft.md` - listing copy for OKX.AI ASP review. +- `service-spec.md` - proposed MCP/API contract and scoring rubric. +- `pricing.md` - 3 candidate per-call tiers. +- `sample-inputs/` - 5 historical delivery-style inputs. +- `sample-outputs/` - 5 expected audit outputs. +- `src/auditor.mjs` - local deterministic audit engine. +- `src/http-server.mjs` - local HTTP API and demo server. +- `bin/audit-agent-deliverable.mjs` - local CLI wrapper. +- `bin/serve-demo.mjs` - local demo/API server launcher. +- `test/run-samples.mjs` - sample regression test. +- `test/http-smoke.mjs` - HTTP endpoint smoke test. +- `prototype.md` - local prototype usage notes. +- `demo/index.html` - buyer-facing local demo. +- `multi-model-validation-20260702.md` - independent validation and launch decision. +- `billing-event-protocol-v0.md` - future paid-call semantics. +- `buyer-facing-launch-packet-cn.md` - public-safe launch copy draft. +- `discovery/agent-service.json` - agent/service discovery metadata. +- `discovery/mcp-tool-manifest.json` - MCP-style tool manifest. +- `openapi.yaml` - HTTP API contract. +- `config/xagent-review.env.example` - non-secret deployment identity example for the bounded X-Agent review surface. +- `agent-market-ecosystem-analysis-cn.md` - market/ecosystem analysis in Chinese. +- `go-no-go.md` - launch decision and hard gates. + +## Local prototype + +This package now includes a runnable local prototype. + +Run all sample audits: + +```bash +npm run audit:samples +``` + +Run one input: + +```bash +npm run audit -- sample-inputs/01-pmquant-rename.json --pretty +``` + +Run regression tests: + +```bash +npm test +``` + +Run local demo/API server: + +```bash +npm run serve +``` + +Local service endpoints: + +```text +GET /health +GET /api/sample-audits +GET /.well-known/agent-service.json +GET /mcp-tool-manifest.json +GET /openapi.yaml +POST /audit-agent-deliverable +``` + +Current validation: + +```text +PASS 01-pmquant-rename.json: needs_review score=84 +PASS 02-t310-governance-update.json: needs_review score=80 +PASS 03-alkanes-red-stop.json: pass score=96 +PASS 04-dashboard-curation-readout.json: needs_review score=84 +PASS 05-claude-science-readout.json: pass score=93 +All sample audit cases passed +PASS http smoke on http://127.0.0.1: +``` + +The prototype is rule-based. It validates the product shape before adding any LLM, MCP server, OKX account, wallet, payment, endpoint, or listing flow. + +## Buyer-facing demo + +Public static demo: + +```text +https://agent-acceptance-gate.pages.dev/ +``` + +Open the local demo: + +```text +/Users/zhangxu/Projects/agent-acceptance-gate/demo/index.html +``` + +The demo reframes JSON audit output as a buyer acceptance report: + +- Accept / Needs Review / Reject verdict; +- score and dimension bars; +- reasons not to accept yet; +- missing evidence; +- next gate; +- seller questions; +- machine flags. + +Multi-model validation result: + +```text +Product direction: Yellow +Local artifact/demo: Green +Direct public / OKX.AI production launch: Red +``` + +Reason: the pain is real, but the current product must be experienced at the delivery acceptance / payment / dispute node. Standalone CLI/JSON is too abstract for buyers. + +## Recommended product shape + +Start as A2MCP: + +- fixed schema; +- pay-per-call; +- no negotiation; +- instant structured output; +- buyer / ASP / evaluator usable. + +Do not start as A2A custom service. A2A would require negotiation, manual delivery QA, dispute handling, and broad service scope before demand is proven. + +Do not start as Evaluator. Evaluator registration requires OKB stake and introduces slashing / timeout risk. + +## Local validation goal + +Before any OKX account, wallet, endpoint, or listing action, the local package should answer: + +1. Can the service be explained in one sentence? +2. Can the output be structured enough to price per call? +3. Does it catch missing validation, hard-gate risk, dirty state, deferred release checks, and unclear next gates? +4. Would a buyer use it before accepting an Agent task? + +## Hard gates before real launch + +Do not proceed without explicit Leo approval for: + +- Agentic Wallet login / account action; +- OKX API credential or Onchain OS production configuration; +- receiving wallet address; +- funding, signing, transaction, staking, or payment setup; +- deployment of new Worker code or X-Agent review routes; +- ASP listing submission; +- OKX.AI terms / price / payment terms; +- push / deploy / public publish of any related website asset. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-09-listing-bigpack/RECEIPT.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-09-listing-bigpack/RECEIPT.md new file mode 100644 index 00000000..6555d082 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-09-listing-bigpack/RECEIPT.md @@ -0,0 +1,42 @@ +# OKX #3977 Big Pack Receipt · 2026-07-09 + +Leo 授权:「按你的建议来」→ 头像 B(signal-only)+ 一次 create 全部 6 个 + activate。 + +## Result + +- **Approval**: Listing under review(`approvalDisplayStatus=2`) +- **AI 质检备注**: `AI 质检建议通过` +- **Agent**: #3977 Leo Labs · status still `not listed`(等人工/平台过审) +- **Services**: **10**(旧 4 换号 + 新 6) + +## Avatar + +- Local: `~/Projects/content/brand/okx-avatar-1024-signal-only-candidate.jpg` +- CDN: `https://static.okx.com/cdn/web3/wallet/marketplace/headimages/agent/avatar/cd470af0-d299-4605-8e58-ca0498f9e9d8.jpg` + +## Tx + +- update txHash: `0x3d8c766895cd6a944e601ad3e62899542701ef5f9c873a6818cf66f3ea1f7542` + +## Service IDs (post-update) + +| id | Name | Fee | +|----|------|-----| +| 30207 | World Cup Smart Money Radar | 0.1 | +| 30208 | Polymarket Smart Money Radar | 0.05 | +| 30209 | Agent Delivery Audit Gate | 0.2 | +| 30210 | Event Price Divergence Radar | 0.1 | +| 30211 | Crypto Market Regime Radar | 0.1 | +| 30212 | World Cup Upset Alert | 0.1 | +| 30213 | Token DD Verdict | 0.05 | +| 30214 | PM Trade Preflight | 0.1 | +| 30215 | PM Event Readout | 0.1 | +| 30216 | Content Verify Claims | 0.1 | + +## Note + +`activate` 响应里 `activate.success=false` 仍带旧拒审文案,但同响应 `submitApproval[0].success=true`(approvalStatus=2)。最终 `agent get` 以 **Listing under review** 为准。 + +## Logs + +同目录 `01-upload.json` … `08-service-list-final.json` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/RECEIPT.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/RECEIPT.md new file mode 100644 index 00000000..4eaf5508 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/RECEIPT.md @@ -0,0 +1,38 @@ +# T0521 · x402 free-trial fix deploy + resubmit · 2026-07-11 + +## Status + +**Listing under review** (`approvalDisplayStatus=2`) · remark: `AI 质检建议通过` + +## What changed + +| Step | Evidence | +|---|---| +| Code | `agent-acceptance-gate@543abe8` — unpaid POST always 402 unless `X402_FREE_TRIAL=true` | +| Deploy | Worker Version ID `2aa22173-50ee-4e35-94f5-55af1d50e699` → `api.leolabs.me` | +| Live catalog | `billing.free_trial` = disabled | +| Unpaid POST | HTTP **402** | +| x402-check | **10/10** listed endpoints `valid=true` with `--body '{}'` | +| Activate | `submitApproval.approvalStatus=2 success=true` → under review | + +## Commands used + +```bash +npm run deploy:worker +onchainos agent x402-check --endpoint --body '{}' +onchainos agent activate --agent-id 3977 --preferred-language zh-CN +``` + +## Discipline + +Under review freeze: **no fragmented service update/activate** until terminal status. + +## Next + +1. Wait for listing terminal (email watcher now covers Junk/All Mail) +2. On approve: screenshot + English contest post + Google form (Leo publish gate) +3. Do **not** set `X402_FREE_TRIAL=true` until after listing passes (optional marketing later) + +## Note on CLI + +`onchainos agent x402-check` **without** `--body` still hits GET sample → false 200 failure. Always use `--body '{}'`. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/activate.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/activate.json new file mode 100644 index 00000000..45674d4d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/activate.json @@ -0,0 +1,16 @@ +{ + "ok": true, + "data": { + "activate": { + "approvalStatus": 5, + "rejectReason": "当前 Agent 未通过 x402 标准校验,请按照下述步骤修改后再提交:\n\n1. 使用 OKX Payment SDK 在您的服务端完成 x402 接入,参考接入指南:https://web3.okx.com/zh-hans/onchainos/dev-docs/okxai/howtomcp\n2. 完成所需的集成步骤,确保未支付请求返回标准 402 挑战。\n3. 重新验证服务可用性后再次提交。", + "success": false + }, + "submitApproval": [ + { + "approvalStatus": 2, + "success": true + } + ] + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/agent-get-after-activate.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/agent-get-after-activate.json new file mode 100644 index 00000000..8737261a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/agent-get-after-activate.json @@ -0,0 +1,68 @@ +{ + "ok": true, + "data": [ + { + "agentId": "3977", + "agentWalletAddress": "0x1e1a2f7ac1bc6df29a1878c3f26b17dccdc16e15", + "approvalDisplayStatus": 2, + "approvalLabel": "Listing under review", + "approvalRemark": "AI 质检建议通过", + "card": [ + { + "label": "Agent ID", + "value": "#3977" + }, + { + "label": "Name", + "value": "Leo Labs" + }, + { + "label": "Role", + "value": "ASP" + }, + { + "label": "Status", + "value": "not listed" + }, + { + "label": "Approval status", + "value": "Listing under review" + }, + { + "label": "Address", + "value": "0x1e1a…6e15" + }, + { + "label": "Description", + "value": "Data-only gates for agents and solo builders: prediction-market signals, delivery audits, token DD, and trade preflight. JSON in, structured verdict out — not chatbots or trade tips.\n给 Agent 和独立开发者的数据闸门:预测市场信号、交付验收、代币尽调、下单前检查。JSON 进、结构化结论出;不是聊天机器人,也不是喊单。" + }, + { + "label": "Profile photo", + "value": "https://static.okx.com/cdn/web3/wallet/marketplace/headimages/agent/avatar/cd470af0-d299-4605-8e58-ca0498f9e9d8.jpg" + } + ], + "categoryCode": [ + "SOFTWARE_SERVICES" + ], + "chainIndex": 196, + "communicationAddress": "0x5ce65c41e57807A2e21a60a65CcbDC1aA36a13e6", + "createdAt": 1783202055209, + "keyUuid": "890caff7-743b-435e-889f-1d6227170977", + "lastOnlineTime": null, + "name": "Leo Labs", + "onlineStatus": 1, + "ownerAddress": "0x1e1a2f7ac1bc6df29a1878c3f26b17dccdc16e15", + "profileDescription": "Data-only gates for agents and solo builders: prediction-market signals, delivery audits, token DD, and trade preflight. JSON in, structured verdict out — not chatbots or trade tips.\n给 Agent 和独立开发者的数据闸门:预测市场信号、交付验收、代币尽调、下单前检查。JSON 进、结构化结论出;不是聊天机器人,也不是喊单。", + "profilePicture": "https://static.okx.com/cdn/web3/wallet/marketplace/headimages/agent/avatar/cd470af0-d299-4605-8e58-ca0498f9e9d8.jpg", + "role": 2, + "roleLabel": "ASP", + "securityRate": null, + "serviceList": [], + "soldCount": 0, + "status": 2, + "statusLabel": "not listed", + "tagCodes": [], + "updatedAt": 1783739293586 + } + ] +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/deployed_at_utc.txt b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/deployed_at_utc.txt new file mode 100644 index 00000000..cafeabac --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/deployed_at_utc.txt @@ -0,0 +1 @@ +2026-07-11T03:08:36Z diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/git_sha.txt b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/git_sha.txt new file mode 100644 index 00000000..92ce8887 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/git_sha.txt @@ -0,0 +1 @@ +543abe8 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/worker_version_id.txt b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/worker_version_id.txt new file mode 100644 index 00000000..9436e837 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/worker_version_id.txt @@ -0,0 +1 @@ +2aa22173-50ee-4e35-94f5-55af1d50e699 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/x402-check-all.txt b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/x402-check-all.txt new file mode 100644 index 00000000..5164dab8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/_inventory/2026-07-11-x402-resubmit/x402-check-all.txt @@ -0,0 +1,11 @@ +id=30207 valid=True amount=100000 https://api.leolabs.me/world-cup-smart-money-radar +id=30208 valid=True amount=50000 https://api.leolabs.me/polymarket-smart-money-radar +id=30209 valid=True amount=200000 https://api.leolabs.me/agent-delivery-acceptance-audit +id=30210 valid=True amount=100000 https://api.leolabs.me/event-price-divergence-radar +id=30211 valid=True amount=100000 https://api.leolabs.me/crypto-market-regime-radar +id=30212 valid=True amount=100000 https://api.leolabs.me/world-cup-upset-alert +id=30213 valid=True amount=50000 https://api.leolabs.me/token-dd-verdict +id=30214 valid=True amount=100000 https://api.leolabs.me/pm-trade-preflight +id=30215 valid=True amount=100000 https://api.leolabs.me/pm-event-readout +id=30216 valid=True amount=100000 https://api.leolabs.me/content-verify-claims +SUMMARY 10/10 valid; bad=0 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/agent-market-ecosystem-analysis-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/agent-market-ecosystem-analysis-cn.md new file mode 100644 index 00000000..bd9bcad7 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/agent-market-ecosystem-analysis-cn.md @@ -0,0 +1,259 @@ +# Agent 市场生态分析:Agent 验收门禁 + +Created: 2026-07-02 +Status: local_research_synthesis + +## 核心结论 + +这个产品应该是 `Agent-first, human-readable`。 + +也就是说: + +```text +Agent 调用服务; +人类看结论; +市场按有效审计结果计费; +争议时复用证据。 +``` + +它不是普通用户每天主动打开的 App,而是 Agent 市场里的验收基础设施。 + +## 1. 现在市场/生态里已经有什么 + +### A. Agent 工具 / MCP 市场 + +现状: + +- MCP 已成为 Agent 连接外部工具的重要标准; +- 公开 MCP 工具数量已经非常大; +- 工具发现、工具路由、工具安全正在变成独立问题。 + +含义: + +单纯“再做一个 MCP 工具”不够。必须明确告诉 Agent: + +```text +什么场景必须调用我。 +``` + +本产品的触发词应该是: + +- `before_accept_delivery` +- `before_release_payment` +- `before_submit_delivery` +- `before_dispute_vote` +- `before_public_release_or_deploy` + +### B. Agent observability / evaluation + +代表: + +- LangSmith +- Braintrust +- HoneyHive +- Arize + +它们主要卖: + +- trace; +- evaluation; +- prompt / dataset / regression; +- production monitoring; +- human annotation。 + +缺口: + +它们偏“开发者运营 Agent 系统”,不直接解决市场交易里的问题: + +```text +这次 Agent 交付到底能不能收? +``` + +所以我们不要和它们正面竞争 observability,而是切 `acceptance gate`。 + +### C. Agent commerce / task marketplace + +代表方向: + +- OKX.AI A2MCP / A2A; +- x402 / HTTP 402 pay-per-call; +- AI hires humans / AI hires services 类 marketplace。 + +OKX.AI 的关键点: + +- A2MCP:标准 API/MCP,按次收费,无需协商; +- A2A:Agent 协商价格、scope、delivery,资金 escrow,用户确认后释放。 + +这正好给本产品两个入口: + +1. A2MCP:作为可调用验收 API; +2. A2A:作为交付/争议流程里的验收门禁。 + +## 2. 别人都在做什么 + +别人主要在做四层: + +### 工具连接层 + +让 Agent 能调用 Slack、Figma、GitHub、数据库、支付、搜索、浏览器。 + +问题: + +工具越来越多,Agent 不知道何时该调用哪个,且调用后结果难验收。 + +### 观测评测层 + +记录 Agent trace,做 eval,找失败原因。 + +问题: + +它们通常面向开发团队,不是面向买家付款/交付验收。 + +### 支付协议层 + +用 x402 / Payment SDK / escrow 让 Agent 能按次买 API、买服务、雇人。 + +问题: + +支付可以自动化,但“该不该付款”仍需要验收逻辑。 + +### 任务市场层 + +让 Agent 领任务、发布任务、雇人或雇其他 Agent。 + +问题: + +市场越开放,越需要 acceptance / dispute / evidence layer。 + +## 3. 大众需要的东西是什么 + +如果说“大众”是普通个人用户,他们不需要“agent audit”。 + +他们需要的是: + +- 别被骗; +- 少返工; +- 不要为半成品付款; +- 不要把没验证的东西上线; +- 出问题时有人能说清楚责任。 + +如果说“大众”是 Agent 市场里的大量 Agent / ASP / buyer agent,它们需要的是: + +- 自动找工具; +- 自动验收; +- 自动补证据; +- 自动生成交付报告; +- 自动判断是否能放款; +- 自动进入争议流程。 + +所以产品文案必须从: + +```text +Audit agent deliverables +``` + +改成: + +```text +Can this agent delivery be accepted? +``` + +## 4. 有没有 Agent 可以去领任务 + +有这个方向。 + +OKX.AI 的 A2A 模式就是 Agent 与 Agent 协商任务、价格、scope、delivery,资金进 escrow,用户确认后释放。 + +更广义上,AI hires humans / Agent hires services 的市场也已经出现。 + +这说明未来市场结构可能是: + +```text +User / Buyer Agent 发布任务 +↓ +Seller Agent / ASP 接任务 +↓ +Seller Agent 调用工具完成任务 +↓ +Acceptance Gate 审计交付 +↓ +Buyer Agent / Human 决定 accept / reject / dispute +↓ +Payment / escrow release +``` + +我们的服务应该卡在: + +```text +交付 -> 放款 / 接受 / 争议 +``` + +之间。 + +## 产品定位建议 + +不要定位: + +```text +Agent Deliverable Auditor +``` + +应该定位: + +```text +Agent Acceptance Gate +Agent 验收门禁 +``` + +## Agent 自动发现策略 + +需要机器可读 metadata: + +- service name; +- tool name; +- call_when; +- do_not_call_for; +- input schema; +- output schema; +- billable event; +- pricing mode; +- risk boundaries; +- tags。 + +本地已补: + +- `discovery/agent-service.json` +- `discovery/mcp-tool-manifest.json` +- `openapi.yaml` + +未来如果发布到市场,应把这些 metadata 提交到: + +- OKX.AI ASP listing; +- MCP registry / directory; +- GitHub README; +- leolabs product brief; +- ChatGPT / Claude app-style manifest(如果走该渠道)。 + +## 最小可抢先发布形态 + +不要直接做正式收费。 + +推荐先发: + +```text +Agent Acceptance Gate +Local demo + API draft + public-safe writeup +``` + +发布目标不是赚钱,而是抢占概念: + +```text +Agent marketplace 缺的不是更多 Agent,而是交付验收层。 +``` + +## 下一步 + +1. 用 10 个真实 worker writeback 跑一轮。 +2. 把 `call_when` 和 `machine_flags` 调到足够精准。 +3. 决定公开渠道:X / leolabs / GitHub / OKX.AI。 +4. 真正上 OKX.AI 前,必须补 endpoint deploy、payment middleware、receiving wallet、privacy/terms/rate limit。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/billing-event-protocol-v0.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/billing-event-protocol-v0.md new file mode 100644 index 00000000..079fa7e8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/billing-event-protocol-v0.md @@ -0,0 +1,83 @@ +# Billing Event Protocol v0 + +Status: local_only_design + +## Purpose + +Multi-model validation found that per-call pricing only works if the charge event is explicit. + +This protocol defines the minimum billing semantics for a future OKX.AI / A2MCP launch. It is not active and does not connect to payments. + +## Current launch decision + +Do not charge users yet. + +Use this only as a design constraint for the future payment integration. + +## Billable event + +Recommended billable event: + +```text +valid_audit_generated +``` + +Do not bill on raw request receipt. Bill only when: + +1. request JSON is valid; +2. required fields are present; +3. audit verdict is generated; +4. response is returned or durably stored for retry. + +## Non-billable events + +No charge for: + +- invalid JSON; +- missing required fields; +- server error before audit generation; +- duplicate retry with the same idempotency key; +- health checks; +- sample/demo calls. + +## Required request fields before paid launch + +Future paid endpoint should require: + +```json +{ + "idempotency_key": "buyer-task-id-or-client-generated-key", + "mode": "quick | full | evaluator", + "task": {}, + "delivery": {}, + "context": {} +} +``` + +## Pricing mapping + +Start narrow: + +- `quick`: simple acceptance check; +- `full`: complete buyer-ready report; +- `evaluator`: not launched until dispute demand exists. + +Do not launch dynamic pricing until usage data exists. + +## Refund / retry rule + +If the service returns a syntactically valid audit, the call is billable. + +If the user claims the audit quality is bad, handle through support or credits, not automatic refund. Automatic refund requires a separate quality SLA that does not exist yet. + +## Hard gates before activation + +- OKX.AI account / Agentic Wallet; +- receiving wallet address; +- payment middleware or reverse proxy; +- endpoint deploy; +- idempotency storage; +- public terms; +- privacy statement; +- abuse/rate-limit policy. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/audit-agent-deliverable.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/audit-agent-deliverable.mjs new file mode 100755 index 00000000..b652f0b6 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/audit-agent-deliverable.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { auditDelivery } from '../src/auditor.mjs'; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const args = process.argv.slice(2); + +if (args.includes('--help') || args.length === 0) { + printHelp(); + process.exit(args.length === 0 ? 1 : 0); +} + +const pretty = args.includes('--pretty'); +const useSamples = args.includes('--samples'); +const outIndex = args.indexOf('--out'); +const outDir = outIndex >= 0 ? path.resolve(process.cwd(), args[outIndex + 1] ?? '') : null; +const files = useSamples + ? fs.readdirSync(path.join(rootDir, 'sample-inputs')) + .filter((file) => file.endsWith('.json')) + .sort() + .map((file) => path.join(rootDir, 'sample-inputs', file)) + : args.filter((arg) => !arg.startsWith('--') && arg !== (outDir ?? '')).map((file) => path.resolve(process.cwd(), file)); + +if (outIndex >= 0 && !args[outIndex + 1]) { + throw new Error('--out requires a directory path'); +} + +if (!files.length) { + throw new Error('No input files found'); +} + +if (outDir) fs.mkdirSync(outDir, { recursive: true }); + +const outputs = []; +for (const file of files) { + const input = JSON.parse(fs.readFileSync(file, 'utf8')); + const audit = auditDelivery(input); + outputs.push({ input_file: file, audit }); + + if (outDir) { + const base = path.basename(file, '.json'); + fs.writeFileSync(path.join(outDir, `${base}-audit.json`), `${JSON.stringify(audit, null, 2)}\n`); + } +} + +if (!outDir) { + const payload = useSamples || files.length > 1 ? outputs : outputs[0].audit; + process.stdout.write(`${JSON.stringify(payload, null, pretty ? 2 : 0)}\n`); +} else { + for (const output of outputs) { + process.stdout.write(`wrote ${path.basename(output.input_file, '.json')}-audit.json\n`); + } +} + +function printHelp() { + process.stdout.write(`Agent Deliverable Auditor prototype + +Usage: + npm run audit -- sample-inputs/01-pmquant-rename.json --pretty + npm run audit:samples + node ./bin/audit-agent-deliverable.mjs --samples --out generated-audits + +Options: + --samples Audit every JSON file in sample-inputs/ + --pretty Pretty-print JSON output + --out DIR Write one audit JSON file per input + --help Show this help +`); +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/serve-demo.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/serve-demo.mjs new file mode 100755 index 00000000..26509dd6 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/bin/serve-demo.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import { createServer } from '../src/http-server.mjs'; + +const portArg = process.argv.find((arg) => arg.startsWith('--port=')); +const port = Number(portArg?.split('=')[1] ?? process.env.PORT ?? 8787); +const host = process.env.HOST ?? '127.0.0.1'; + +const server = createServer(); + +server.listen(port, host, () => { + console.log(`Agent Acceptance Gate demo`); + console.log(`Demo: http://${host}:${port}/`); + console.log(`Health: http://${host}:${port}/health`); + console.log(`API: POST http://${host}:${port}/audit-agent-deliverable`); +}); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/buyer-facing-launch-packet-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/buyer-facing-launch-packet-cn.md new file mode 100644 index 00000000..10ea804a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/buyer-facing-launch-packet-cn.md @@ -0,0 +1,105 @@ +# Buyer-facing Launch Packet CN + +Status: public_safe_draft_not_published + +## Product name + +Agent 交付验收器 + +English subtitle: + +Agent Acceptance Gate + +## One-liner + +在接受 Agent 交付、放款或发布前,先跑一次验收门禁:它会告诉你现在能不能收、缺什么证据、有没有 hard gate 风险。 + +## Positioning + +不是 agent observability。 + +不是代码安全审计。 + +不是投资、交易、钱包工具。 + +这是一个面向买家/ASP/Evaluator 的交付验收层。 + +## Buyer pain + +Agent 说“做完了”不等于你该接受。 + +真实风险通常藏在这些地方: + +- build 没跑; +- screenshot / visual smoke 没做; +- repo 还是 dirty; +- deploy/push/public publish 还没被 owner 批准; +- seller 没写 rollback; +- 任务失败其实是 guard 正确触发; +- writeback 看起来完整,但 next gate 不清楚。 + +## Demo framing + +五个样例: + +1. 本地完成但不能发布; +2. 内容做了但治理规则还没放行; +3. 任务失败日志但安全停止正确; +4. 大分支验收包可用但不是 release packet; +5. 只读研究交付可接受。 + +## Suggested X post + +```text +做了一个小工具原型:Agent 交付验收器。 + +Agent 说“我做完了”,买家真正要判断的是: + +- 能不能直接收? +- 缺什么证据? +- 有没有 push / deploy / wallet / credential 这种 hard gate? +- 下一步该让 seller 补什么? + +现在先做了本地 demo:把 agent writeback 转成 Accept / Needs Review / Reject 的验收卡片。 + +我越来越觉得,Agent marketplace 缺的不是更多 agent,而是交付验收层。 +``` + +## Suggested OKX.AI listing direction + +Do not submit yet. + +Future listing should use: + +```text +Agent Acceptance Gate +Audits AI agent deliveries before acceptance, payment release, or dispute review. +``` + +Avoid claiming: + +- full security audit; +- legal compliance; +- investment review; +- guaranteed correctness; +- automatic dispute resolution. + +## Launch readiness + +Ready now: + +- local demo; +- local HTTP API; +- sample audits; +- buyer-facing copy; +- billing event protocol draft. + +Not ready: + +- public endpoint; +- payment middleware; +- receiving wallet; +- OKX.AI ASP listing; +- privacy / terms; +- rate limit / abuse handling; +- real buyer demand proof. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/config/xagent-review.env.example b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/config/xagent-review.env.example new file mode 100644 index 00000000..7acfa973 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/config/xagent-review.env.example @@ -0,0 +1,7 @@ +# Public deployment identity; these values are not secrets. +# Replace the sample commit with the exact public Git commit being deployed. +XAGENT_GIT_COMMIT=0123456789abcdef0123456789abcdef01234567 +XAGENT_PROJECT_SLUG=runesleo-agent-acceptance-gate + +# Keep false outside the bounded reviewer window. +XAGENT_REVIEW_ENABLED=false diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/demo/index.html b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/demo/index.html new file mode 100644 index 00000000..0c07285a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/demo/index.html @@ -0,0 +1,672 @@ + + + + + + Agent 验收门禁 Demo + + + +
+ + +
+
+
+

+

+
+
+
+ +
+
+
+
验收分
+
+
+

买家结论

+

+
+
+ +
+

下一步

+

+
+ +
+
+

不能直接接受的原因

+
    +
    +
    +

    需要补的证据

    +
      +
      +
      + +
      +
      +

      支持接受的证据

      +
        +
        +
        +

        要问 Seller 的问题

        +
          +
          +
          + +
          +

          评分维度

          +
          +
          + +
          +

          机器 flags

          +
          +
          +
          +
          + + + + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/agent-service.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/agent-service.json new file mode 100644 index 00000000..1e14153d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/agent-service.json @@ -0,0 +1,125 @@ +{ + "schema_version": "0.1", + "service_id": "agent-acceptance-gate", + "service_name": "Agent Acceptance Gate", + "display_name_zh": "Agent 验收门禁", + "status": "local_only_not_published", + "audience": ["agent", "asp", "buyer_agent", "evaluator_agent"], + "primary_user": "agent", + "human_role": "review_result_and_authorize_next_gate", + "one_liner": "Check whether an AI agent transaction can continue before task acceptance, paid service calls, delivery acceptance, payment release, or dispute review.", + "call_when": [ + "before_hire_agent", + "before_accept_task", + "before_assign_budget", + "before_paid_tool_call", + "before_buy_service", + "before_agent_spends_budget", + "before_accept_delivery", + "before_release_payment", + "before_submit_delivery_to_buyer", + "before_dispute_vote", + "after_buyer_rejects_delivery", + "after_seller_claims_completion", + "before_public_release_or_deploy" + ], + "do_not_call_for": [ + "legal_advice", + "investment_advice", + "wallet_signing", + "smart_contract_security_audit", + "full_code_review", + "public_deploy" + ], + "capabilities": [ + "agent_transaction_gate", + "pre_hire_risk_check", + "budget_spend_guard", + "delivery_acceptance_audit", + "hard_gate_detection", + "missing_evidence_detection", + "release_readiness_triage", + "dispute_fact_packet" + ], + "tools": [ + { + "name": "world_cup_smart_money_radar", + "description": "Return World Cup prediction-market smart-money movements: profitable wallet labels, position changes, side, notional size, confidence, and caveats. Data and analytics only; no trading, custody, or investment advice.", + "endpoint": "POST /world-cup-smart-money-radar", + "billable_event": "valid_world_cup_smart_money_report_generated", + "launch_priority": "first_okx_ai_listing_candidate" + }, + { + "name": "polymarket_smart_money_radar", + "description": "Return all-market Polymarket smart-money movements for profitable wallet cohorts. Data and analytics only; no trading, custody, or investment advice.", + "endpoint": "POST /polymarket-smart-money-radar", + "billable_event": "valid_polymarket_smart_money_report_generated", + "launch_priority": "batch_listing_candidate" + }, + { + "name": "event_probability_crypto_divergence", + "description": "Compare prediction-market event probability changes with crypto spot and funding moves to surface divergence signals for research agents.", + "endpoint": "POST /event-probability-crypto-divergence", + "billable_event": "valid_event_crypto_divergence_report_generated", + "launch_priority": "batch_listing_candidate" + }, + { + "name": "crypto_market_pulse_report", + "description": "Return a compact crypto market pulse with flows, anomalies, and watch items for agent research workflows.", + "endpoint": "POST /crypto-market-pulse-report", + "billable_event": "valid_crypto_market_pulse_report_generated", + "launch_priority": "batch_listing_candidate" + }, + { + "name": "audit_agent_delivery", + "description": "Given a task, agent writeback, artifact paths, changed files, validation output, hard gates, and next gate, return pass/needs_review/fail with missing evidence, risks, buyer summary, evaluator notes, and machine flags.", + "endpoint": "POST /audit-agent-deliverable", + "billable_event": "valid_audit_generated" + }, + { + "name": "assess_agent_transaction", + "description": "Future tool shape: assess whether an agent transaction should continue before accepting a task, spending budget, buying a service, releasing payment, or entering dispute.", + "endpoint": "planned", + "billable_event": "valid_transaction_assessment_generated" + } + ], + "pricing_intent": { + "mode": "future_paid_call_not_active", + "billable_event": "valid_audit_generated", + "free_events": ["invalid_json", "missing_required_fields", "server_error", "duplicate_retry", "health_check", "sample_demo_call"] + }, + "discovery_tags": [ + "agent marketplace", + "agent transaction gate", + "World Cup smart money", + "prediction market analytics", + "Polymarket analytics", + "budget guard", + "pre-hire gate", + "delivery acceptance", + "agent QA", + "escrow safety", + "A2MCP", + "MCP tool", + "OKX.AI ASP candidate", + "evaluator support", + "release gate", + "hard gate" + ], + "local_endpoints": { + "health": "GET /health", + "sample_audits": "GET /api/sample-audits", + "audit": "POST /audit-agent-deliverable", + "openapi": "GET /openapi.yaml", + "mcp_manifest": "GET /mcp-tool-manifest.json" + }, + "hard_gates_before_public_launch": [ + "public_endpoint_deploy", + "wallet_or_receiving_address", + "payment_middleware", + "OKX_Agentic_Wallet_or_account", + "ASP_listing_submission", + "privacy_terms", + "rate_limit_and_abuse_policy" + ] +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/asp-avatar.png b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/asp-avatar.png new file mode 100644 index 00000000..3766a137 Binary files /dev/null and b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/asp-avatar.png differ diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/mcp-tool-manifest.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/mcp-tool-manifest.json new file mode 100644 index 00000000..cbdbe942 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/discovery/mcp-tool-manifest.json @@ -0,0 +1,169 @@ +{ + "schema_version": "0.1", + "server": { + "name": "agent-acceptance-gate", + "description": "MCP/API service for checking whether AI agent transactions can continue before task acceptance, paid service calls, delivery acceptance, payment release, or dispute review.", + "status": "local_only_not_published" + }, + "tools": [ + { + "name": "world_cup_smart_money_radar", + "title": "World Cup Smart Money Radar", + "description": "Use this when an agent needs World Cup prediction-market smart-money movement signals. It returns wallet labels, position changes, market side, notional size, confidence, rationale, and caveats. Data and analytics only; no custody, no trading, no investment advice.", + "input_schema": { + "type": "object", + "properties": { + "market": { + "type": "string", + "description": "Optional market hint, market id, or all." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 10 + } + } + }, + "output_schema": { + "type": "object", + "required": ["schema_version", "service_id", "mode", "summary", "signals", "caveats", "next_gate"], + "properties": { + "schema_version": { "type": "string" }, + "service_id": { "type": "string", "const": "world_cup_smart_money_radar" }, + "mode": { "type": "string" }, + "summary": { "type": "string" }, + "signals": { "type": "array", "items": { "type": "object" } }, + "caveats": { "type": "array", "items": { "type": "string" } }, + "next_gate": { "type": "string" } + } + }, + "routing_hints": [ + "call for World Cup prediction-market smart-money movement", + "call when an agent needs a compact Polymarket-derived signal report", + "do not call for trade execution, custody, or investment advice" + ] + }, + { + "name": "polymarket_smart_money_radar", + "title": "Polymarket Smart Money Radar", + "description": "Use this when an agent needs all-market Polymarket smart-money movement signals. Data and analytics only; no custody, no trading, no investment advice.", + "input_schema": { + "type": "object", + "properties": { + "query": { "type": "string" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 10 } + } + } + }, + { + "name": "event_probability_crypto_divergence", + "title": "Event Probability Crypto Divergence", + "description": "Use this when an agent needs event probability versus crypto spot/funding divergence signals. Data and analytics only.", + "input_schema": { + "type": "object", + "properties": { + "event": { "type": "string" }, + "asset": { "type": "string" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 10 } + } + } + }, + { + "name": "crypto_market_pulse_report", + "title": "Crypto Market Pulse Report", + "description": "Use this when an agent needs a compact crypto market pulse report with flows, anomalies, and watch items.", + "input_schema": { + "type": "object", + "properties": { + "query": { "type": "string" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 10 } + } + } + }, + { + "name": "audit_agent_delivery", + "title": "Audit Agent Delivery", + "description": "Use this before accepting an AI agent delivery, releasing escrow/payment, submitting a seller delivery, or voting on a dispute. It checks artifacts, changed files, validation, rollback, hard gates, and next gate, then returns pass/needs_review/fail with buyer-ready reasoning.", + "input_schema": { + "type": "object", + "required": ["schema_version", "mode", "task", "delivery", "context"], + "properties": { + "schema_version": { "type": "string", "const": "0.1" }, + "mode": { "type": "string", "enum": ["quick", "full", "evaluator"] }, + "task": { + "type": "object", + "required": ["buyer_goal", "surface"], + "properties": { + "task_id": { "type": "string" }, + "buyer_goal": { "type": "string" }, + "surface": { "type": "string", "enum": ["repo", "website", "research", "data", "ops", "other"] }, + "allowed_actions": { "type": "array", "items": { "type": "string" } }, + "forbidden_actions": { "type": "array", "items": { "type": "string" } }, + "acceptance_criteria": { "type": "array", "items": { "type": "string" } } + } + }, + "delivery": { + "type": "object", + "required": ["writeback_text", "next_gate"], + "properties": { + "writeback_text": { "type": "string" }, + "artifact_paths": { "type": "array", "items": { "type": "string" } }, + "changed_files": { "type": "array", "items": { "type": "string" } }, + "validation": { "type": "array", "items": { "type": "string" } }, + "validation_output": { "type": "string" }, + "rollback_plan": { "type": "string" }, + "hard_gates_declared": { "type": "array", "items": { "type": "string" } }, + "next_gate": { "type": "string" } + } + }, + "context": { + "type": "object", + "properties": { + "repo_state": { "type": "string", "enum": ["clean", "dirty", "unknown", "not_applicable"] }, + "public_publish_requested": { "type": "boolean" }, + "payments_or_wallets_in_scope": { "type": "boolean" }, + "credentials_in_scope": { "type": "boolean" }, + "notes": { "type": "string" } + } + } + } + }, + "output_schema": { + "type": "object", + "required": ["schema_version", "verdict", "score", "dimension_scores", "missing", "risks", "next_gate", "buyer_summary", "machine_flags"], + "properties": { + "schema_version": { "type": "string" }, + "verdict": { "type": "string", "enum": ["pass", "needs_review", "fail"] }, + "score": { "type": "number" }, + "dimension_scores": { "type": "object" }, + "missing": { "type": "array", "items": { "type": "string" } }, + "risks": { "type": "array", "items": { "type": "string" } }, + "positive_evidence": { "type": "array", "items": { "type": "string" } }, + "questions_for_seller": { "type": "array", "items": { "type": "string" } }, + "next_gate": { "type": "string" }, + "buyer_summary": { "type": "string" }, + "evaluator_notes": { "type": "string" }, + "machine_flags": { "type": "array", "items": { "type": "string" } } + } + }, + "routing_hints": [ + "call when user asks if an agent delivery is safe to accept", + "call before payment release or escrow release", + "call before public deployment if the delivery includes release claims", + "call when dispute evidence needs structure" + ] + }, + { + "name": "assess_agent_transaction", + "title": "Assess Agent Transaction", + "status": "planned", + "description": "Future tool: use before an agent accepts a task, spends budget, buys a service, releases payment, or enters dispute. Returns whether the transaction can continue, what evidence is missing, and which hard gates apply.", + "routing_hints": [ + "call before an agent spends budget", + "call before a buyer agent hires a seller agent", + "call before a seller agent accepts unclear scope", + "call before dispute escalation" + ] + } + ] +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/go-no-go.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/go-no-go.md new file mode 100644 index 00000000..b9f3681b --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/go-no-go.md @@ -0,0 +1,67 @@ +# Go / No-Go + +Status: local_decision_packet + +## Current decision + +Go for local prototype and demo. + +No-go for OKX.AI production launch today. + +## Why local prototype is worth doing + +- The buyer problem is real in any agent marketplace: accepting an Agent delivery requires evidence review. +- The service can be expressed as one sentence. +- The output can be structured, scored, and priced per call. +- Leo already has historical writebacks that expose the exact failure modes: deferred validation, hard gates, dirty state, writer locks, rollback, and unclear release gates. +- A2MCP is a better fit than A2A because the service is repeatable. + +## Why production launch is not approved yet + +- No public endpoint exists. +- Demand inside OKX.AI is unproven. +- Marketplace listing requires account / Agentic Wallet / endpoint / pricing decisions. +- Live monetization needs wallet/payment setup and likely OKX production configuration. +- The local package is a spec and sample set, not an executable service yet. + +## Go criteria for next step + +Proceed to a local executable prototype only if: + +1. The five sample outputs feel useful enough that Leo would use one before accepting a worker delivery. +2. The schema does not need free-form negotiation to make sense. +3. The service catches at least three meaningful risks across the sample set. +4. The product remains narrow: delivery acceptance audit, not general consulting. + +## No-go / kill criteria + +Stop or park if: + +- the output feels like generic prose instead of a decision aid; +- buyers would still need the same amount of manual review after using it; +- the service cannot distinguish `task failed safely` from `agent failed the task`; +- OKX.AI demand appears too thin after marketplace observation; +- production launch would require paid resources or wallet actions before demand proof. + +## Next recommended build + +Build a local CLI or MCP stub that: + +```text +input: sample-inputs/*.json +output: sample-outputs/*.json-shaped verdict +``` + +First implementation can be deterministic plus LLM-assisted later. The immediate goal is not model quality; it is product usefulness and repeatable output shape. + +## Hard gates still closed + +- OKX account / Agentic Wallet login; +- API key / credential setup; +- receiving wallet address; +- wallet funding / signing / transaction / staking; +- public endpoint deploy; +- ASP listing submission; +- website publication; +- repo commit / push / deploy. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/hackathon-submission-packet-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/hackathon-submission-packet-cn.md new file mode 100644 index 00000000..6d3eb963 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/hackathon-submission-packet-cn.md @@ -0,0 +1,170 @@ +# OKX AI Genesis Hackathon Submission Packet + +Status: draft_not_submitted +Project: Agent Acceptance Gate + +## One-liner + +Agent Acceptance Gate is a transaction gate for agent commerce: before an agent accepts a task, spends budget, submits delivery, releases payment, or votes on a dispute, it checks whether the transaction can continue safely. + +## Problem + +Agent marketplaces create a new failure mode: + +```text +Agents can transact faster than humans can verify. +``` + +This causes: + +- unclear task scope; +- unnecessary paid tool calls; +- incomplete deliveries; +- unsafe release/deploy actions; +- payment disputes; +- evaluator overload. + +## Solution + +Agent Acceptance Gate provides a machine-callable check: + +```text +Can this agent transaction continue? +``` + +Current prototype focuses on delivery acceptance: + +- pass / needs_review / fail; +- missing evidence; +- hard-gate risks; +- buyer summary; +- evaluator notes; +- next gate. + +Expanded roadmap covers: + +- pre-hire gate; +- pre-call budget gate; +- delivery acceptance gate; +- dispute/evaluator gate. + +## Why OKX.AI + +OKX.AI is building A2A / A2MCP agent commerce. + +The more agents transact, the more the market needs: + +- acceptance standards; +- evidence packets; +- escrow release checks; +- dispute-ready facts; +- seller reputation signals. + +Agent Acceptance Gate is designed as infrastructure for that market. + +## Demo + +Static demo: + +```text +https://agent-acceptance-gate.pages.dev/ +``` + +Local API draft: + +```text +POST /audit-agent-deliverable +GET /.well-known/agent-service.json +GET /mcp-tool-manifest.json +GET /openapi.yaml +``` + +## Current artifacts + +- Public static demo. +- Local HTTP prototype. +- Deterministic audit engine. +- OpenAPI 3.1 draft. +- MCP-style tool manifest. +- Agent discovery metadata. +- 5 sample audits. +- Revenue route map. +- Billing event protocol. + +## What is not live yet + +- No OKX.AI listing. +- No public API endpoint. +- No wallet. +- No payment middleware. +- No receiving address. +- No production auth/rate limit. + +## Hackathon positioning + +Do not pitch as: + +```text +another auditor +``` + +Pitch as: + +```text +the acceptance and transaction gate for agent commerce +``` + +## Evaluation criteria fit + +### Solves real problems + +Agent commerce needs trust checks before money and external state move. + +### Generates real usage + +Potential call points: + +- every task posting; +- every paid service call; +- every delivery; +- every payment release; +- every dispute. + +### Uses OKX.AI design + +Designed for: + +- ASPs; +- buyer agents; +- seller agents; +- evaluator agents; +- A2MCP call surface; +- future payment/escrow flow. + +## Immediate submission gap + +Need before submission: + +1. public API endpoint or hosted mock endpoint; +2. short demo video / screenshots; +3. 10 real usage examples; +4. pricing mode; +5. privacy / terms boundary; +6. OKX.AI ASP registration path, if submitting formally. + +## Proposed category + +```text +Agent Commerce Infrastructure +``` + +Tags: + +- agent marketplace; +- A2MCP; +- escrow safety; +- delivery acceptance; +- budget guard; +- dispute support; +- evaluator tooling. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/market-demand-research-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/market-demand-research-cn.md new file mode 100644 index 00000000..48127a61 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/market-demand-research-cn.md @@ -0,0 +1,722 @@ +# Agent 市场需求调研:先看生态,再选产品 + +Created: 2026-07-02 +Status: market_research_v1 + +## 结论先行 + +`Agent Acceptance Gate` 不是当前最确定的大需求。 + +它是一个 **有战略价值、但依赖 Agent 市场交易量起来的基础设施切口**。 + +如果目标是 OKX.AI 上 $1M ARR OPC,应该先研究整个 Agent 市场的高频交易节点,而不是先押一个产品。 + +当前最可能赚钱的方向不是“验收工具”本身,而是: + +```text +Agent commerce operations layer +``` + +也就是围绕 Agent 接任务、花预算、调用服务、交付、结算、争议、信誉这些动作,提供高频、可计费、可自动调用的基础服务。 + +## 1. Agent 市场未来会是什么样 + +未来 Agent 市场大概率不是一个单点工具市场,而是多层市场: + +```text +User / Human +↓ +Buyer Agent +↓ +Task / Intent Market +↓ +Seller Agent / ASP +↓ +Tool / MCP / API Market +↓ +Payment / Escrow / Settlement +↓ +Evidence / Verification / Reputation +``` + +### Layer A: Intent / Task layer + +用户不再逐个找服务,而是把目标交给 Agent。 + +需求: + +- 把人类意图转成可执行任务; +- 定义预算; +- 定义验收标准; +- 选择 seller agent / ASP。 + +高频点: + +- task clarification; +- scope decomposition; +- vendor/service matching; +- budget allocation。 + +### Layer B: Service / Tool layer + +Agent 为完成任务调用工具: + +- MCP server; +- paid API; +- data service; +- research service; +- code/build/deploy service; +- human service。 + +需求: + +- 发现工具; +- 判断工具是否可信; +- 控制预算; +- 记录调用结果; +- 防止恶意工具 / 过度调用。 + +### Layer C: Transaction / Payment layer + +Agent 会自己发起小额、高频交易。 + +需求: + +- pay-per-call; +- subscription; +- escrow; +- refund; +- settlement; +- rate limit; +- payment authorization。 + +这层是大市场,因为所有服务都要过钱。 + +### Layer D: Verification / Clearing layer + +交易之后要判断是否完成。 + +需求: + +- evidence envelope; +- acceptance check; +- dispute packet; +- reputation update; +- settlement instruction。 + +这层不是最高频,但一旦平台有真实交易,会变成基础设施。 + +## 2. 当前生态具体情况 + +### OKX.AI + +OKX.AI 文档已经定义三类角色: + +- User; +- ASP; +- Evaluator。 + +ASP 有两种服务模式: + +- A2MCP:标准 API/MCP 服务,按次收费,瞬时结算; +- A2A:Agent 间协商价格、scope、delivery,资金 escrow,用户确认后释放。 + +这说明 OKX.AI 想做的不只是聊天机器人,而是 Agent commerce marketplace。 + +证据: + +- https://web3.okx.com/onchainos/dev-docs/okxai/asp +- https://web3.okx.com/onchainos/dev-docs/payments/service-seller + +### MCP 工具生态 + +公开研究统计了 177,436 个 MCP tools。软件开发占比很高,action tools 的占比从 27% 上升到 65%。 + +含义: + +```text +Agent 正在从“读信息”变成“改环境/执行动作”。 +``` + +高频需求会从信息检索转向: + +- action safety; +- permissions; +- tool trust; +- cost control; +- observability; +- rollback。 + +证据: + +- https://arxiv.org/abs/2603.23802 + +### Agent hiring / task market + +RentAHuman 这类 marketplace 已经出现,AI agent 可以通过 API/MCP 雇佣人类完成任务,并用 escrow / payment 处理交付。 + +研究也指出这类市场已经带来 abuse / credential fraud / impersonation / referral fraud 等问题。 + +含义: + +```text +Agent 市场一旦有任务和钱,安全、验收、争议会很快变成平台问题。 +``` + +证据: + +- https://www.wired.com/story/ai-agent-rentahuman-bots-hire-humans +- https://arxiv.org/abs/2602.19514 + +### Agentic payment / commerce + +x402、AP2、UCP、Visa / Mastercard agent payments 都说明一个趋势: + +```text +Agent 会开始自动发起商业交易。 +``` + +但相关论文也指出支付协议存在 replay、context binding、paid-but-denied、unpaid-service 等问题。 + +含义: + +支付本身不是终点。Agent commerce 还需要: + +- authorization; +- mandate; +- context binding; +- fraud detection; +- clearing / verification; +- dispute handling。 + +证据: + +- https://arxiv.org/abs/2605.11781 +- https://arxiv.org/abs/2604.11430 +- https://arxiv.org/abs/2606.08790 +- https://www.axios.com/2025/09/16/google-ai-agents-ecommerce-online-shopping + +## 3. 哪些服务需求会很高、调用量很大 + +按调用频率排序: + +### 1. Tool discovery / routing + +问题: + +```text +Agent 现在该调用哪个服务? +``` + +频率: + +极高。每个任务都可能触发多次。 + +付费意愿: + +中等。平台/开发者愿付,普通用户感知弱。 + +竞争: + +强。MCP registry、tool router、Composio/Zapier 类都会做。 + +Leo 适配度: + +中低。需要强平台分发和大量工具索引。 + +### 2. Budget / payment guard + +问题: + +```text +Agent 这次该不该花钱? +是否越过预算或授权边界? +``` + +频率: + +高。每次 paid API / service call 都可触发。 + +付费意愿: + +高。因为直接保护钱。 + +竞争: + +正在形成。支付方、wallet、x402/AP2 middleware 都会做。 + +Leo 适配度: + +中。可做轻量 policy / evidence / metadata filter,但钱包集成是 hard gate。 + +### 3. Credential / permission / action safety + +问题: + +```text +Agent 是否可以访问这个凭证、执行这个 action? +``` + +频率: + +高。尤其 enterprise agent。 + +付费意愿: + +高。 + +竞争: + +强。安全公司、身份权限平台、cloud provider 会进入。 + +Leo 适配度: + +中低。需要安全资质和企业 trust。 + +### 4. Delivery acceptance / verification + +问题: + +```text +Agent 做完了吗?能不能收?证据够不够? +``` + +频率: + +中。每个任务交付触发一次或几次。 + +付费意愿: + +中高。金额越大越强。 + +竞争: + +中。现有 eval/observability 偏开发者,不是 marketplace acceptance。 + +Leo 适配度: + +高。这是当前最适合切入的 wedge。 + +### 5. Dispute / evaluator packet + +问题: + +```text +买卖双方争议时,事实是什么?该怎么判? +``` + +频率: + +低到中。取决于交易量和 dispute rate。 + +付费意愿: + +高。因为每次争议有明确价值。 + +竞争: + +早期。可能会被平台内建。 + +Leo 适配度: + +中高。适合作为 acceptance 的高价扩展。 + +### 6. Reputation / credit scoring + +问题: + +```text +这个 Agent / ASP 历史上可靠吗? +``` + +频率: + +高。每次选服务都可用。 + +付费意愿: + +高,但更像平台资产。 + +竞争: + +平台会强控。 + +Leo 适配度: + +低到中。除非先积累大量验收/争议数据。 + +### 7. Data / research / analysis services + +问题: + +```text +Agent 需要外部数据和分析能力。 +``` + +频率: + +高。 + +付费意愿: + +中高。 + +竞争: + +极强。数据/API/研究服务非常拥挤。 + +Leo 适配度: + +中。可以做垂直 niche,但不适合泛化。 + +## 4. 哪些领域市场容量最高 + +按潜在市场容量排序: + +### Tier 1: Payment / budget / transaction infrastructure + +市场容量最高。 + +原因: + +- 所有 agent commerce 都要经过支付和授权; +- 高频; +- 和钱直接相关; +- 企业/平台愿付。 + +风险: + +- 监管/合规/钱包/安全要求高; +- 大公司和支付网络会进入; +- 一人公司很难直接做核心 payment rail。 + +适合 Leo 的切口: + +```text +pre-payment policy / budget guard / metadata risk check +``` + +不要碰 custody / signing / settlement。 + +### Tier 2: Security / permission / abuse prevention + +市场容量很高。 + +原因: + +- Agent 能执行 action 后,安全风险迅速上升; +- 恶意 MCP server、prompt injection、credential fraud 都是真问题。 + +风险: + +- 需要深安全信誉; +- 销售周期偏企业。 + +适合 Leo 的切口: + +```text +lightweight agent action risk preflight +``` + +### Tier 3: Tool routing / workflow orchestration + +市场容量高,调用频率高。 + +原因: + +- Agent 需要找工具、组合工具、执行工作流; +- Zapier / Composio / MCP registry 都说明这是大方向。 + +风险: + +- 竞争最激烈; +- 平台型玩家强。 + +适合 Leo 的切口: + +```text +不要做泛工具路由,做 trust-aware routing / risk-aware routing。 +``` + +### Tier 4: Verification / acceptance / clearing + +市场容量中高,但取决于交易量。 + +原因: + +- 只要有交易,就需要验收和清算; +- 但早期交易量不足时需求不明显。 + +风险: + +- 太早; +- 容易被平台内建; +- 独立工具感知弱。 + +适合 Leo 的切口: + +```text +先作为 OKX.AI / ASP hackathon wedge,验证平台是否愿意采用。 +``` + +### Tier 5: Domain-specific agent services + +市场容量取决于领域。 + +高潜领域: + +- coding / deployment; +- data / research; +- trading / market data; +- compliance / legal ops; +- customer support; +- commerce / shopping。 + +风险: + +- 每个领域都需要专业深度; +- 泛化困难。 + +适合 Leo 的切口: + +```text +用自身强项做 niche:agent workflow QA / trading research QA / website release QA。 +``` + +## 5. 对当前 Agent Acceptance Gate 的重新判断 + +### 它不是最大市场 + +最大市场在: + +- payment / budget; +- permission / safety; +- tool routing; +- workflow orchestration。 + +### 它是合理 wedge + +原因: + +- 小团队能做; +- 和 OKX.AI ASP 叙事相关; +- 可以快速 demo; +- 可拓展到 dispute / reputation; +- 不必一开始碰钱包和签名。 + +### 最大风险 + +它可能是: + +```text +平台应该内建的功能,而不是第三方 ASP。 +``` + +如果 OKX.AI 自己在 delivery / escrow / dispute flow 内建验收,独立 ASP 空间会变小。 + +### 是否继续 + +继续,但必须改目标: + +```text +不要把 Agent Acceptance Gate 当终局。 +把它当进入 Agent commerce ops 的 wedge。 +``` + +## 6. 更好的机会排序 + +按照 `市场容量 × 高频 × Leo 可做性 × 当前时机` 排序: + +### #1 Agent Spend Guard + +一句话: + +```text +Before an agent spends money, check policy, evidence, budget, and risk. +``` + +为什么好: + +- 高频; +- 跟钱直接相关; +- 比 delivery acceptance 更广; +- 可从 metadata / policy 做起,不碰签名。 + +风险: + +- 接近 payment hard gate; +- 需要和 x402 / OKX Payment SDK / Agentic Wallet 关系清楚。 + +### #2 Agent Acceptance Gate + +一句话: + +```text +Before accepting or releasing payment for an agent delivery, check evidence and gates. +``` + +为什么好: + +- 当前已有 demo; +- 易解释; +- 可参加 hackathon; +- 可作为 spend guard 的后置场景。 + +风险: + +- 频率不如 spend guard; +- 依赖任务市场流量。 + +### #3 Agent Evidence Envelope + +一句话: + +```text +Standard package for agent delivery evidence: artifacts, validation, logs, risk flags, next gate. +``` + +为什么好: + +- 更底层; +- 可被 acceptance、dispute、reputation 复用; +- 不一定需要收费服务,可能成为标准。 + +风险: + +- 标准化很难变现; +- 需要生态采纳。 + +### #4 Dispute Fact Packet + +一句话: + +```text +When buyer and seller disagree, generate a structured fact packet for evaluators. +``` + +为什么好: + +- 高价值; +- 争议场景有强需求; +- 和 OKX Evaluator 角色贴合。 + +风险: + +- 交易量和 dispute volume 未知; +- 低频。 + +### #5 Trust-aware Tool Router + +一句话: + +```text +Route agents to tools based not only on capability, but also cost, risk, and evidence requirements. +``` + +为什么好: + +- 高频; +- 市场大。 + +风险: + +- 竞争强; +- 需要大量工具索引。 + +## 7. 当前最应该做什么 + +不要继续只打磨 demo。 + +应该做市场验证: + +### A. X / OKX community 反应测试 + +发一个问题,不发硬广: + +```text +If AI agents can spend budget and hire services, +what should happen before they pay? + +I think the missing layer is not another agent, +but a transaction gate: +scope, budget, evidence, hard gates, dispute trail. +``` + +看谁回应: + +- OKX / X Layer; +- hackathon builders; +- MCP builders; +- x402 / payment people; +- agent security people。 + +### B. Hackathon discovery + +看 OKX.AI hackathon 具体要求: + +- 是否必须上线 ASP; +- 是否看 real usage; +- 是否有 task hall; +- 是否允许 infra/tooling; +- 是否要 Agentic Wallet。 + +### C. 访谈 5 类人 + +1. ASP builder; +2. MCP tool builder; +3. agent workflow user; +4. payment/x402 builder; +5. evaluator / dispute-minded builder。 + +问: + +- Agent 花钱前你担心什么? +- Agent 交付后你怎么验收? +- 你愿意为哪一步付费? +- 你觉得平台会内建还是第三方服务? + +### D. 用真实任务跑样例 + +至少 20 个: + +- coding agent delivery; +- research agent delivery; +- paid API call decision; +- unsafe action decision; +- failed delivery dispute。 + +目标不是 accuracy,而是找高频 pattern。 + +## 8. 最终判断 + +当前需求结论: + +```text +Agent Acceptance Gate 本身不是已验证刚需。 +Agent transaction trust / spend guard / evidence layer 是更大方向。 +``` + +执行建议: + +```text +继续小成本推进; +不要重仓; +不要急着接钱包/支付; +先用 OKX hackathon 和 X 反应验证 demand。 +``` + +如果市场反馈说明 builders 真正在找: + +- budget guard; +- transaction policy; +- evidence envelope; +- dispute packet; + +那就从 `Agent Acceptance Gate` 扩成: + +```text +Agent Commerce Ops +``` + +如果反馈只是点赞但没人想用,就停止,不继续投入。 + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/marketplace-listing-draft.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/marketplace-listing-draft.md new file mode 100644 index 00000000..6ca6e54d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/marketplace-listing-draft.md @@ -0,0 +1,87 @@ +# Marketplace Listing Draft + +Status: draft_local_only +Target: OKX.AI ASP / A2MCP + +## Service name + +Agent Deliverable Auditor + +## One-line description + +Audits AI agent deliverables for completeness, proof, validation, rollback, hard gates, and buyer-ready next steps. + +## Short description + +Agent Deliverable Auditor checks whether an AI agent's task delivery is safe to accept. Submit the task prompt, agent writeback, artifacts, changed files, validation output, declared hard gates, and next gate. The service returns a structured verdict: `pass`, `needs_review`, or `fail`, with score, missing evidence, risks, buyer summary, and evaluator notes. + +## Buyer problem + +In an agent marketplace, a buyer often receives a polished writeback but still has to decide: + +- Did the agent actually produce the artifact? +- Were files, validation, rollback, and next gate declared clearly? +- Did the agent cross a hard gate like deploy, credentials, payment, wallet, or repo mutation? +- Is the work ready to accept, or should the buyer request clarification? + +This service turns that acceptance check into a repeatable audit. + +## Best for + +- Buyers reviewing Agent task deliveries before accepting work. +- ASPs checking their own delivery packet before sending it to a buyer. +- Evaluators preparing structured notes for a dispute. +- Teams using agent workers with writeback / validation / rollback protocols. + +## Not for + +- Legal, tax, investment, or financial advice. +- Smart contract security assurance. +- Wallet signing, trading, staking, or transaction review. +- Full code review of large diffs. +- Public deployment readiness unless the input includes release validation evidence. + +## Example input + +```json +{ + "task": { + "buyer_goal": "Rename product page copy without changing pricing or deploying.", + "forbidden_actions": ["push", "deploy", "payment_config", "pricing_change"] + }, + "delivery": { + "writeback_text": "...", + "changed_files": ["src/pages/PMQuantPage.tsx"], + "validation": ["tsx syntax pass", "git diff --check pass"], + "next_gate": "buyer approval before push and deploy" + } +} +``` + +## Example output + +```json +{ + "verdict": "needs_review", + "score": 82, + "summary": "Delivery is locally coherent but full build was deferred and buyer must approve wording before public release.", + "missing": ["release build", "buyer decision on old customer promise"], + "risks": ["public release hard gate still open"], + "next_gate": "Buyer reviews copy risk before push/deploy." +} +``` + +## Suggested listing categories + +- Agent QA +- Workflow automation +- Delivery review +- Marketplace acceptance +- Evaluator support + +## Endpoint placeholder + +No public endpoint yet. + +Production endpoint must not be created until Leo approves OKX account / wallet / deploy hard gates. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/million-dollar-opc-strategy-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/million-dollar-opc-strategy-cn.md new file mode 100644 index 00000000..db5d66f5 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/million-dollar-opc-strategy-cn.md @@ -0,0 +1,348 @@ +# 如何冲 OKX.AI 首个 $1M ARR OPC + +Created: 2026-07-02 +Status: strategy_local + +## Signal + +Star_OKX 发帖: + +> Whoever becomes the first One Person Company (OPC) to generate US$1 million in annual revenue on OKX.AI, I will personally donate X BTC to celebrate the milestone. (X >= 1) + +X Layer 同时宣布: + +> OKX AI Genesis Hackathon is live. Build an Agent Service Provider for OKX.AI and compete for a share of $100,000 in prizes. + +Interpretation: + +OKX.AI is explicitly trying to create a market narrative around: + +```text +one person company + agent service provider + real usage + onchain payment +``` + +## Revenue math + +Target: + +```text +$1,000,000 ARR = $83,333 MRR = about $2,740/day +``` + +If only per-call: + +| Price per audit | Calls needed / year | Calls needed / day | +|---:|---:|---:| +| $0.10 | 10,000,000 | 27,397 | +| $0.25 | 4,000,000 | 10,959 | +| $1.00 | 1,000,000 | 2,740 | +| $2.00 | 500,000 | 1,370 | +| $5.00 | 200,000 | 548 | + +Conclusion: + +`Agent Acceptance Gate` cannot reach $1M ARR as a low-priced standalone micro-tool unless it becomes high-volume marketplace infrastructure. + +To reach $1M, it needs one of these: + +1. platform-level placement inside an agent marketplace; +2. high-value dispute / escrow / release gate usage; +3. ASP subscription or team plan; +4. bundle of multiple agent-market operations tools; +5. enterprise/marketplace integration. + +## Product thesis + +The product should not be: + +```text +an audit tool people remember to open +``` + +It should become: + +```text +the default acceptance gate agents call before delivery, payment, release, or dispute. +``` + +Human-facing phrasing: + +```text +Can I accept this agent delivery? +``` + +Agent-facing phrasing: + +```text +Call audit_agent_delivery before accepting, releasing payment, submitting delivery, or voting on dispute. +``` + +## Wedge + +Current wedge: + +```text +Agent Acceptance Gate +``` + +This is a strong wedge because every agent marketplace needs: + +- delivery verification; +- missing evidence detection; +- hard-gate detection; +- buyer-ready summaries; +- dispute-ready fact packets. + +But the wedge alone is not enough. It must expand into: + +```text +Agent Market Trust Layer +``` + +## Expansion path + +### Layer 1: Acceptance Gate + +Current. + +Inputs: + +- task; +- delivery/writeback; +- artifacts; +- validation; +- hard gates; +- next gate. + +Output: + +- pass / needs_review / fail; +- missing evidence; +- risks; +- seller questions; +- buyer summary; +- evaluator notes. + +### Layer 2: Seller Preflight + +ASP calls before submitting work. + +Value: + +- fewer rejected deliveries; +- higher acceptance rate; +- better seller reputation. + +Possible pricing: + +- low-cost per call; +- bundled into ASP monthly plan. + +### Layer 3: Buyer Release Gate + +Buyer agent calls before accepting or releasing escrow. + +Value: + +- avoid paying for incomplete work; +- avoid unsafe deploys; +- evidence-backed rejection. + +Possible pricing: + +- per accepted task; +- percentage-adjacent fixed fee; +- marketplace-integrated fee. + +### Layer 4: Dispute Packet + +Evaluator agent calls during dispute. + +Value: + +- structured facts; +- faster arbitration; +- less subjective argument. + +Possible pricing: + +- higher per call; +- paid by dispute bounty; +- evaluator tooling subscription. + +### Layer 5: Reputation / Credit + +Aggregate outcomes: + +- seller pass rate; +- missing evidence history; +- hard-gate breach history; +- dispute outcomes. + +This is where defensibility starts. + +## How to win the hackathon + +The hackathon asks for ASPs that solve real problems and generate real usage. + +So the submission should not say: + +```text +We built an auditor. +``` + +It should say: + +```text +We built the acceptance layer for OKX.AI agent commerce. +``` + +Demo flow: + +1. Seller Agent submits delivery. +2. Acceptance Gate audits delivery. +3. Result is `needs_review`. +4. Seller Agent fixes missing evidence. +5. Gate returns `pass`. +6. Buyer Agent accepts. +7. Future version releases escrow / preserves dispute evidence. + +## 7-day execution plan + +### Day 0-1: Public signal + +Done: + +- static demo live; +- local repo; +- OpenAPI; +- MCP-style manifest; +- discovery metadata; +- buyer-facing packet. + +Next: + +- publish one X post or quote tweet; +- do not claim revenue or official OKX listing; +- frame as "building the acceptance layer for agent commerce." + +### Day 1-2: Real A2MCP shape + +Build: + +- actual MCP server wrapper; +- tool call `audit_agent_delivery`; +- simple hosted API endpoint; +- request idempotency key; +- structured error responses. + +Hard gates: + +- public endpoint deploy; +- auth/rate limit; +- privacy note. + +### Day 2-3: OKX.AI ASP package + +Prepare: + +- listing description; +- pricing; +- endpoint; +- demo video; +- sample calls; +- terms/boundaries. + +Hard gates: + +- Agentic Wallet / OKX account; +- receiving address; +- payment middleware; +- ASP listing submission. + +### Day 3-5: Usage proof + +Get 10-30 real audits from: + +- Leo worker writebacks; +- Cursor/Claude/Codex task packets; +- public hackathon examples; +- other builders' ASP deliveries if possible. + +Metrics: + +- % needs_review; +- top missing evidence; +- time saved; +- pass-after-fix rate. + +### Day 5-7: Hackathon submission + +Submit as: + +```text +Agent Acceptance Gate: the delivery acceptance layer for OKX.AI ASPs. +``` + +Include: + +- demo URL; +- API docs; +- MCP manifest; +- 5 sample cases; +- live usage numbers; +- clear boundary: not wallet/security/legal advice. + +## What would make this a $1M OPC + +Not the demo. + +Not one API. + +The $1M version is: + +```text +Default acceptance / dispute infrastructure for agent marketplaces. +``` + +It needs: + +- marketplace-level distribution; +- trusted scoring standard; +- usage-based billing; +- seller reputation history; +- dispute integration; +- broad tool schemas beyond Leo's internal workflow. + +## Kill criteria + +Stop pushing if: + +- OKX.AI ASP listing cannot expose this at the delivery/payment/dispute moment; +- agents cannot discover/call it automatically; +- buyers do not understand the result without explanation; +- the product remains a standalone audit page; +- no real users call it after public demo/hackathon exposure. + +## Immediate next move + +Publish a build-in-public post around the category, not the tool: + +```text +Agent marketplaces do not just need more agents. +They need an acceptance layer. + +I built a first prototype: +Agent Acceptance Gate. + +It answers one question before payment/release/dispute: +Can this agent delivery be accepted? +``` + +Do not overclaim: + +- no OKX.AI official listing yet; +- no payment integration yet; +- no wallet actions; +- no security guarantee. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/multi-model-validation-20260702.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/multi-model-validation-20260702.md new file mode 100644 index 00000000..94d5eb89 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/multi-model-validation-20260702.md @@ -0,0 +1,151 @@ +# Multi-model Validation: Agent Deliverable Auditor + +Created: 2026-07-02 +Status: validation_yellow + +## Decision + +The product shape is directionally valid, but it is not ready for direct public / OKX.AI production launch. + +Verdict: `Yellow` + +Meaning: + +- Continue building the buyer-facing acceptance flow. +- Do not submit OKX.AI ASP listing yet. +- Do not deploy a public endpoint yet. +- Do not add wallet, payment, Agentic Wallet, API key, or receiving address. + +## Why it is not Green + +The current CLI / JSON prototype proves that the logic can run, but user perception is still weak unless the product is placed at a high-stakes workflow point: + +```text +before accepting an Agent delivery +before releasing payment / escrow +before public release / deploy +before dispute voting +``` + +Standalone JSON is too abstract. The sellable product must be a buyer-facing acceptance report. + +## Independent validator summaries + +### Buyer demand validator + +Judgment: `Yellow` + +Buyer pain is real, but the product should be positioned as an Agent delivery acceptance gate, not a broad agent audit tool. + +Strongest use cases: + +- delivery acceptance before payment; +- release gate before deployment; +- comparing multiple ASP / contractor deliveries; +- hard-gate compliance check. + +Most likely paying users: + +- ASP QA / delivery owner; +- enterprise AI platform / security / procurement teams; +- third-party evaluators after dispute volume exists. + +Minimum perceptible product: + +```text +Upload repo / PR / run artifact -> pass / needs_review / fail -> missing evidence -> hard-gate red lines -> next gate -> one-page buyer-ready report. +``` + +### Monetization validator + +Judgment: `Yellow` + +Per-call pricing can work because the task is discrete. However, the charge event needs a clear state machine: + +- request submitted; +- audit generated; +- paid / unpaid; +- failed input; +- refund or retry; +- dispute handoff. + +Pricing risk: + +- quick `$0.05-$0.25` may be too low for high-quality review; +- full `$0.50-$2.00` may be viable only if mostly automated; +- evaluator support `$2-$8` is plausible but depends on dispute demand. + +Best first paying segment: + +1. buyers before acceptance; +2. ASPs before delivery; +3. evaluators later. + +### Technical / release validator + +Judgment: `Yellow` + +The CLI/JSON prototype is not enough for public launch. It needs at least one buyer-facing demo/report before publishing. + +Recommended publish order: + +1. local demo page; +2. public-safe writeup / update; +3. optional hosted demo after owner approval; +4. OKX.AI ASP listing only after endpoint, payment, wallet, and compliance gates are explicitly approved. + +## Official OKX.AI constraints + +OKX docs describe ASPs as service providers in the marketplace. A2MCP is standardized API/MCP, charged per call, with instant settlement through OKX Payment SDK. A2A is negotiated service with escrow release after user confirmation. + +Source: + +- https://web3.okx.com/onchainos/dev-docs/okxai/asp +- https://web3.okx.com/onchainos/dev-docs/payments/service-seller + +For DApp/MCP sellers, OKX docs describe payment middleware or reverse proxy options. Unpaid requests can return HTTP 402 before hitting business logic, and the seller still needs a receiving address. + +This confirms the A2MCP shape, but it also confirms why real launch triggers payment / wallet / endpoint hard gates. + +## Refined product name + +Current name: + +`Agent Deliverable Auditor` + +Better buyer-facing name: + +`Agent 交付验收器` + +English positioning: + +`Agent Acceptance Gate` + +Avoid: + +- generic "agent audit"; +- broad "workflow QA"; +- security assurance claims; +- legal / financial / investment advice framing. + +## Launch decision + +Do not directly publish to OKX.AI today. + +Do publish-prep now: + +- buyer-facing local demo; +- one-page validation report; +- public-safe product explainer draft; +- no wallet / endpoint / deploy / ASP submission. + +## Next gate + +If Leo wants public release after reviewing the local demo, choose one explicit channel: + +1. local-only demo review; +2. leolabs `/updates` brief; +3. GitHub public repo; +4. OKX.AI ASP listing. + +Options 2-4 are public/repo/deploy/account gates and require explicit confirmation before action. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/openapi.yaml b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/openapi.yaml new file mode 100644 index 00000000..ff143d50 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/openapi.yaml @@ -0,0 +1,363 @@ +openapi: 3.1.0 +info: + title: Agent Acceptance Gate + version: 0.2.0 + description: | + Public, read-only decision-support API for checking whether an AI agent + delivery can continue to acceptance. The production Worker is deployed at + api.leolabs.me; normal paid service routes retain their existing x402 + contract. The X-Agent reviewer route is enabled only when + XAGENT_REVIEW_ENABLED=true. The service never signs, settles, trades, or + mutates a repository or user account. +servers: + - url: https://api.leolabs.me + description: Production Cloudflare Worker + - url: http://127.0.0.1:8787 + description: Local development server +paths: + /health: + get: + summary: Health check with deployed source commit + responses: + "200": + description: Service is healthy and version-bound + content: + application/json: + schema: + type: object + required: [status, commit] + properties: + status: + type: string + const: ok + commit: + type: string + pattern: '^[0-9a-f]{40}$' + "503": + description: Deployment identity is missing or invalid + /.well-known/xagent-verification.json: + get: + summary: X-Agent same-origin deployment proof + responses: + "200": + description: Submission slug and deployed source commit + content: + application/json: + schema: + type: object + required: [schemaVersion, slug, commit] + properties: + schemaVersion: + type: integer + const: 1 + slug: + type: string + commit: + type: string + pattern: '^[0-9a-f]{40}$' + "503": + description: Deployment identity is missing or invalid + /xagent/agent-delivery-acceptance-audit: + post: + summary: Reviewer-accessible agent delivery audit + description: | + Free review surface for the X-Agent submission. It is available only + while XAGENT_REVIEW_ENABLED=true and does not change the existing x402 + contract of /agent-delivery-acceptance-audit. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CompactAuditRequest" + responses: + "200": + description: Structured audit result + content: + application/json: + schema: + $ref: "#/components/schemas/AuditResponse" + "400": + description: Invalid audit input + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: Review route is disabled + "503": + description: Deployment identity is missing or invalid + /api/sample-audits: + get: + summary: Return bundled sample audits + responses: + "200": + description: Sample audit list + /api/okx-ai-services: + get: + summary: Return local OKX.AI ASP launch-pack service list + responses: + "200": + description: Local ASP service list + /.well-known/agent-service.json: + get: + summary: Machine-readable service discovery metadata + responses: + "200": + description: Agent service manifest + /mcp-tool-manifest.json: + get: + summary: MCP-style tool manifest + responses: + "200": + description: MCP-style tool metadata + /audit-agent-deliverable: + post: + summary: Audit an AI agent delivery + description: | + Use before accepting an AI agent delivery, releasing payment/escrow, + submitting a seller delivery, or preparing dispute review. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AuditRequest" + responses: + "200": + description: Audit result + content: + application/json: + schema: + $ref: "#/components/schemas/AuditResponse" + "400": + description: Invalid request + /world-cup-smart-money-radar: + post: + summary: Return World Cup smart-money movement signals + description: | + OKX.AI ASP candidate endpoint. Returns a public-safe demo report for + World Cup prediction-market smart-money movements. Production launch + requires fresh data, wallet/payment setup, public endpoint deployment, + and ASP listing approval. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + market: + type: string + description: Optional market hint, market id, or "all". + limit: + type: integer + minimum: 1 + maximum: 10 + responses: + "200": + description: Smart-money signal report + content: + application/json: + schema: + type: object + required: [schema_version, service_id, mode, summary, signals, caveats, next_gate] + properties: + schema_version: + type: string + service_id: + type: string + const: world_cup_smart_money_radar + mode: + type: string + summary: + type: string + signals: + type: array + items: + type: object + caveats: + type: array + items: + type: string + next_gate: + type: string + /polymarket-smart-money-radar: + post: + summary: Return Polymarket smart-money movement signals + description: OKX.AI ASP candidate endpoint for all-market Polymarket smart-money tracking. + responses: + "200": + description: Smart-money signal report + /event-probability-crypto-divergence: + post: + summary: Return event probability vs crypto market divergence signals + description: OKX.AI ASP candidate endpoint for event probability and crypto price/funding divergence. + responses: + "200": + description: Divergence signal report + /crypto-market-pulse-report: + post: + summary: Return compact crypto market pulse report + description: OKX.AI ASP candidate endpoint for agent-readable crypto market pulse reports. + responses: + "200": + description: Market pulse report +components: + schemas: + CompactAuditRequest: + type: object + required: [task, delivery_summary] + properties: + task: + oneOf: + - type: string + - type: object + delivery_summary: + type: string + artifacts: + type: array + items: + type: string + changed_files: + type: array + items: + type: string + validation: + type: array + items: + type: string + validation_output: + type: string + rollback_plan: + type: string + hard_gates: + type: array + items: + type: string + next_gate: + type: string + context: + type: object + ErrorResponse: + type: object + required: [error, message] + properties: + error: + type: string + message: + type: string + AuditRequest: + type: object + required: [schema_version, mode, task, delivery, context] + properties: + schema_version: + type: string + const: "0.1" + mode: + type: string + enum: [quick, full, evaluator] + task: + type: object + required: [buyer_goal, surface] + properties: + task_id: + type: string + buyer_goal: + type: string + surface: + type: string + enum: [repo, website, research, data, ops, other] + allowed_actions: + type: array + items: + type: string + forbidden_actions: + type: array + items: + type: string + acceptance_criteria: + type: array + items: + type: string + delivery: + type: object + required: [writeback_text, next_gate] + properties: + writeback_text: + type: string + artifact_paths: + type: array + items: + type: string + changed_files: + type: array + items: + type: string + validation: + type: array + items: + type: string + validation_output: + type: string + rollback_plan: + type: string + hard_gates_declared: + type: array + items: + type: string + next_gate: + type: string + context: + type: object + properties: + repo_state: + type: string + enum: [clean, dirty, unknown, not_applicable] + public_publish_requested: + type: boolean + payments_or_wallets_in_scope: + type: boolean + credentials_in_scope: + type: boolean + notes: + type: string + AuditResponse: + type: object + required: [schema_version, verdict, score, dimension_scores, missing, risks, next_gate, buyer_summary, machine_flags] + properties: + schema_version: + type: string + verdict: + type: string + enum: [pass, needs_review, fail] + score: + type: number + dimension_scores: + type: object + missing: + type: array + items: + type: string + risks: + type: array + items: + type: string + positive_evidence: + type: array + items: + type: string + questions_for_seller: + type: array + items: + type: string + next_gate: + type: string + buyer_summary: + type: string + evaluator_notes: + type: string + machine_flags: + type: array + items: + type: string diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package-lock.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package-lock.json new file mode 100644 index 00000000..daa57649 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package-lock.json @@ -0,0 +1,1595 @@ +{ + "name": "agent-acceptance-gate", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-acceptance-gate", + "version": "0.1.0", + "bin": { + "audit-agent-deliverable": "bin/audit-agent-deliverable.mjs" + }, + "devDependencies": { + "wrangler": "4.133.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260916.1.tgz", + "integrity": "sha512-h/xA7foQncooOpO3HF4IaCR/8NAy1qN5SjL0+nMTT8FgJyYE/SAGiLgSCusXc15yh7upibaYFrjHK5928nFIEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260916.1.tgz", + "integrity": "sha512-triQBeZCJkCAbugMK/2/K3sqY1neUPXMDbWtoGqpKmzWXi4C2VW2Z+VSobQwJCHluP9wu0qP3O8G0+YNmsJCEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260916.1.tgz", + "integrity": "sha512-a4gRHOI2Iv9wRbFkSGqcDFyfgo4pthTKeBUkdKhL7fxyNUghPNs9rHw+d2Q9aZyeOW2TDa2FI7ELg09eDJOPxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260916.1.tgz", + "integrity": "sha512-qWIZZtd1ATaRI53hKBTxpLKGQJcwOwpPsqfIUaRv8ouq2yJkzJxlG9hReacOjpSh1br3edPxysNa5KBOkqpn9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260916.1.tgz", + "integrity": "sha512-Fa14o+wq2QI4/Lsez609sNmoS9dRliSZwgmDdLLG0wZzmf3s7n0Xs2ZDAvaO2iZgH8WEk+hBfHuOeWM0P7LF0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmmirror.com/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmmirror.com/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmmirror.com/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20260916.0-alpha", + "resolved": "https://registry.npmmirror.com/miniflare/-/miniflare-5.20260916.0-alpha.tgz", + "integrity": "sha512-rffIUYE5tPwhvUtVYZ4LEGaQ2XkXdBsq/flJOKODumR3gbBgHFyqPkobQEB7Ocqesz5wSUhpV8C2R57JYOsyhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.4", + "undici": "7.29.0", + "workerd": "1.20260916.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260916.1", + "resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20260916.1.tgz", + "integrity": "sha512-yGPvK1Tg6l80QNp7uIs3fAlWBmw+ftgn3QK4uqH3bHoS+PeJNARjm6HoWGmkYl1zaqFO0iR31sdwnFKHoHOhUQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260916.1", + "@cloudflare/workerd-darwin-arm64": "1.20260916.1", + "@cloudflare/workerd-linux-64": "1.20260916.1", + "@cloudflare/workerd-linux-arm64": "1.20260916.1", + "@cloudflare/workerd-windows-64": "1.20260916.1" + } + }, + "node_modules/wrangler": { + "version": "4.133.0", + "resolved": "https://registry.npmmirror.com/wrangler/-/wrangler-4.133.0.tgz", + "integrity": "sha512-Iv+feMJ3CzBvWcJM1OjtRL/0ujr1Oqmg6H/356hJrnqf8AEkazhUVqjgcRer5HGhuc9KhrsyxUpKTRMnMeUmeA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260916.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260916.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260916.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package.json new file mode 100644 index 00000000..c3ff9396 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/package.json @@ -0,0 +1,28 @@ +{ + "name": "agent-acceptance-gate", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Local-only prototype for an agent-first acceptance gate service.", + "bin": { + "audit-agent-deliverable": "./bin/audit-agent-deliverable.mjs" + }, + "scripts": { + "audit": "node ./bin/audit-agent-deliverable.mjs", + "audit:samples": "node ./bin/audit-agent-deliverable.mjs --samples --pretty", + "deploy:worker": "wrangler deploy", + "serve": "node ./bin/serve-demo.mjs", + "test": "node ./test/run-samples.mjs && node ./test/schema-smoke.mjs && node ./test/http-smoke.mjs && node ./test/wave-b-services-test.mjs && node ./test/x402-worker-test.mjs && node ./test/xagent-contract-test.mjs && node ./test/xagent-submission-contract-test.mjs", + "worker:check": "node --check ./worker/index.mjs", + "test:samples": "node ./test/run-samples.mjs", + "test:schemas": "node ./test/schema-smoke.mjs", + "test:http": "node ./test/http-smoke.mjs", + "test:x402": "node ./test/x402-worker-test.mjs", + "check:upstream-contracts": "node scripts/check-upstream-contracts.mjs", + "test:xagent": "node ./test/xagent-contract-test.mjs", + "test:xagent-submission": "node ./test/xagent-submission-contract-test.mjs" + }, + "devDependencies": { + "wrangler": "4.133.0" + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/pricing.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/pricing.md new file mode 100644 index 00000000..d6628730 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/pricing.md @@ -0,0 +1,89 @@ +# Pricing Draft + +Status: local_pricing_hypothesis +Service: Agent Deliverable Auditor + +## Pricing principle + +Price the call by buyer risk and output depth, not by token count. + +The buyer is paying to reduce acceptance risk: should they accept the Agent delivery, ask for more proof, or escalate? + +## Tier 1: Quick Acceptance Check + +Candidate price: `$0.05 - $0.25` per call + +Use case: + +- small writeback; +- one artifact; +- narrow task; +- buyer needs quick accept / needs-review / fail verdict. + +Output: + +- verdict; +- score; +- missing evidence; +- top risks; +- next gate. + +Limit: + +- no deep dispute notes; +- no line-level code review. + +## Tier 2: Full Delivery Audit + +Candidate price: `$0.50 - $2.00` per call + +Use case: + +- repo or website delivery; +- multiple files or artifacts; +- validation/deploy gates matter; +- buyer needs a structured acceptance packet. + +Output: + +- full dimension scores; +- positive evidence; +- missing evidence; +- risks; +- questions for seller; +- buyer summary; +- evaluator notes. + +## Tier 3: Evaluator Support Packet + +Candidate price: `$2.00 - $8.00` per call + +Use case: + +- dispute preparation; +- higher-value task; +- evaluator wants a structured fact map before voting. + +Output: + +- full audit; +- acceptance criteria map; +- dispute notes; +- seller/buyer position split; +- recommended evidence requests. + +## Do not sell yet + +Do not sell subscriptions, enterprise plans, or managed QA until the per-call product proves demand. + +Do not include wallet, payment, credential, deploy, or staking setup in the first offer. + +## First live pricing recommendation + +If OKX.AI listing reaches hard-gate approval later: + +- start with one A2MCP endpoint; +- choose Tier 1 or Tier 2 only; +- avoid Tier 3 until dispute demand is visible; +- cap the description to delivery acceptance, not broad security or legal judgment. + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/prototype.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/prototype.md new file mode 100644 index 00000000..bb4fac07 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/prototype.md @@ -0,0 +1,83 @@ +# Local Prototype + +Status: local_only + +## What this prototype does + +This is a deterministic local auditor for the `Agent Deliverable Auditor` ASP concept. + +It reads a JSON delivery packet and returns the response shape defined in `service-spec.md`: + +- `verdict` +- `score` +- dimension scores +- missing evidence +- risks +- positive evidence +- seller questions +- next gate +- buyer summary +- evaluator notes +- machine flags + +It does not call OKX, OpenAI, a wallet, a payment endpoint, or any external API. + +## Commands + +Run all sample audits: + +```bash +npm run audit:samples +``` + +Run one input: + +```bash +npm run audit -- sample-inputs/01-pmquant-rename.json --pretty +``` + +Write generated audit JSON files: + +```bash +node ./bin/audit-agent-deliverable.mjs --samples --out generated-audits +``` + +Run the local sample test: + +```bash +npm test +``` + +Start the local HTTP demo/API: + +```bash +npm run serve +``` + +Then open: + +```text +http://127.0.0.1:8787/ +``` + +Local endpoints: + +```text +GET /health +GET /api/sample-audits +POST /audit-agent-deliverable +``` + +Example API call: + +```bash +curl -sS http://127.0.0.1:8787/audit-agent-deliverable \ + -H 'content-type: application/json' \ + --data-binary @sample-inputs/01-pmquant-rename.json +``` + +## Current limitation + +This prototype is intentionally rule-based. It checks structure, validation gaps, deferred release checks, hard-gate signals, dirty state, guard-triggered stops, and read-only boundaries. + +It is not yet a semantic judge. The next useful step is to test whether the rule output is already valuable enough for Leo's own worker acceptance flow. Only after that should an LLM-assisted mode be considered. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-integration-spec.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-integration-spec.md new file mode 100644 index 00000000..b5e5a75c --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-integration-spec.md @@ -0,0 +1,55 @@ +# OKX.AI ASP 集成规格(实施版摘要) + +- 日期:2026-07-03 · 来源:onchainos-skills repo 源码研读(clone 于 scratchpad)+ okx/payments SDK 文档 +- 完整报告见 agent 返回(本文件为实施要点) + +## 架构:两套系统 + +1. **链上身份 + 服务列表**:`onchainos` CLI 创建(ERC-8004 on X Layer) +2. **自托管 x402 endpoint**:用 OKX Payments SDK 自建,真实收款发生在这里(HTTP 402 → 买方签 EIP-3009 → 重放)。listing 的 fee 只是展示价,实际收费由 endpoint 的 402 challenge 决定,需自行保持一致 + +## 注册流程 + +1. Agentic Wallet:`onchainos wallet login ` + OTP → 自动建钱包。**仅邮箱、无 KYC、无助记词**(私钥在服务端 TEE)。收款地址 = 该钱包的 X Layer (chainId 196) 0x 地址 +2. `onchainos agent pre-check --role asp` → `agent create --role asp ... --service '[...]'` → 上传头像(ASP 必须,传文件不能链接)→ `agent activate` +3. **可能存在 beta 白名单门**(error 10016,需申请等邮件批准)——只有真跑 pre-check 才知道是否仍生效 +4. activate 后 **~24h LLM/人工审核**才上架 + +## 多服务模型(对组合方案有利) + +- **一个钱包 = 一个 ASP 身份 = 可挂 N 个服务**(`--service` 是 JSON 数组,可增量 create/update/delete)——W1/F1/F2/S1 全挂一个身份下,正合布局需求 +- 链上操作平台代付 gas,无 per-listing 费用(结算是否抽成未确认,a2a 路径有 fee_bps 字段) + +## Listing 字段硬规则(内容审核会卡) + +- 服务名 5-30 字符、名词短语、不含价格;描述两行(①核心能力 ②用户需提供什么),各 ≤200 全角字符,**禁 URL/0x 地址/技术栈名/免责声明/名人名** +- fee:纯数字字符串(如 `"1"`),单位恒为 USDT,≤6 位小数 +- endpoint:必须 `https://` 公网可达,**上链后永久**(改 = 链上更新交易);拒绝 http/localhost/内网地址 + +## Seller 端技术栈(TS) + +- `@okxweb3/x402-core` + `@okxweb3/x402-evm` + `@okxweb3/x402-express`(有 hono/fastify/next 适配器) +- 网络锁死 `eip155:196`(X Layer mainnet),默认 token USDT0(6 decimals),EIP-3009 gasless +- **OKX 托管 facilitator**(web3.okx.com),verify+settle 不需要自己连链;鉴权用 SA API key(OKX_API_KEY/SECRET/PASSPHRASE)——**这意味着还需要申请一套 API key(hard gate)** +- 计费模式用 `exact`(固定单价);`syncSettle: true` 确认到账再交付 +- 现有 acceptance-gate 的 Node http server 可直接套 express 适配器改造 + +## Hosting 要求 + +- 公网 HTTPS 24/7,URL 永久上链 → **必须用稳定域名**(如 api.leolabs.me 子域反代),换机器不换 URL +- 无正式 SLA,但挂了 = 付费调用失败 + 链上评分受损 +- 数据拓扑建议:本地/SSD 的 PolyData 派生信号定时同步到 serving 机器,endpoint 只读预计算结果(endpoint 不直连重数据库) + +## ⚠️ 最大风险:服务端区域封锁 + +- error `50125`/`80001` = "Service is not available in your region",**API 服务端强制**,公开文档不列封锁国家清单 +- 工具链明文规则:*"Never suggest checking the network environment, using a VPN, or any region workaround"* +- 对大陆 operator:注册/登录/SA API/facilitator 任何一环被 geo 挡 = 整条路死。**必须先探测再投入建设** +- 未决:具体封锁名单、白名单是否仍生效、TS 是否有 MCP-tool wrapper(Go 有;但 A2MCP endpoint 本质是普通 x402 HTTPS API,大概率够用) + +## 执行序(更新) + +0. **Geo 探测**(无 gate,立即):从实际操作位置探 web3.okx.com API 可达性 +1. Leo 批 gate 包 → 邮箱建钱包 → `pre-check` 摸白名单 +2. 改造脚手架为 multi-service x402 host(W1+F1 先行) +3. 部署到稳定域名 → SA API key → listing 提交 → 24h 审核 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-portfolio-plan.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-portfolio-plan.md new file mode 100644 index 00000000..b110f174 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-asp-portfolio-plan.md @@ -0,0 +1,67 @@ +# OKX.AI ASP 多领域布局方案(T0521) + +- 日期:2026-07-03 +- 决策:Leo override「Park+触发器」→ 直接干 + 多领域占位(早进吃平台增长红利) +- 原则:每个服务 = 薄 wrapper(复用现有数据资产 + acceptance-gate 脚手架),单个服务建设成本 ≤1 天;不做重建设 + +## 服务组合(按平台类目布局) + +### 🔥 世界杯(平台 hero 类目,流量最大) +**W1. 世界杯聪明钱雷达**(vs World Cup Alpha 已售51) +- 数据:PolyData 全量 Polymarket 数据 → 世界杯市场的 top 盈利地址持仓变动 +- 差异化:竞品只给 top-50 地址列表;我们给「持仓变动事件流」(谁在加仓/翻转),对 agent 更可操作 +- 定价:1 USDT/call +- ⏰ 时效:世界杯期间流量红利,**优先级最高,先发** + +### 金融(核心类目,Leo 主场) +**F1. Polymarket 聪明钱追踪·全市场版** +- Task 端有同名挂单需求(未被满足);W1 的全市场泛化,同一套代码两个 listing +- 1-2 USDT/call + +**F2. 事件概率 vs 币价背离信号** +- PM 隐含概率 × perp funding/spot 背离 → trading agent 的事件风险输入 +- 白区(官方 tradekit 明说不做信号层),无竞品 +- 1-2 USDT/call + +**F3. 加密市场脉动报告**(对应 Task 端挂单需求) +- 资金流 + 异动 + fear/greed 聚合 readout,trader 数据管线现成 +- 1 USDT/call + +### 软件服务(零成本占位) +**S1. Agent 交付验收审计**(acceptance auditor 本体) +- 已建成已测试(npm test 5/5),直接上架 = 沉没成本变期权 +- 定价对齐 $1+:Full audit 1 USDT/call(放弃原 $0.05-0.25 塌陷带定价) +- 预期低销量,纯占位 + 万一 A2A 纠纷场景起量就是先发 + +### 暂不做 +- 生活/艺术创作:无资产适配,不硬凑 +- Evaluator/Arbitrator:需 stake ≥100 OKB + 24h uptime + slashable,资金 gate 且运维重,观察 + +## 建设顺序 + +1. **W1 + F1**(同一套聪明钱代码,两个 listing,吃最热类目) +2. **S1**(几乎零工作量,改定价即可) +3. **F2 → F3**(新数据管线接入,各 ≤1 天) + +## 共享基建(做一次全组合复用) + +- acceptance-gate 的 HTTP server / billing-event protocol / discovery metadata / OpenAPI 脚手架 → 抽成 multi-service host +- OKX Payment SDK 集成一次,所有服务共用 +- 统一 idempotency / 计费事件 / 健康检查 +- 数据依赖:PolyData(外接 SSD/VPS)→ 需要确定 serving 拓扑(本地数据如何喂线上 endpoint,等集成规格返回后定) + +## Hard gate 打包清单(等集成规格确认细节后,Leo 一次批) + +| # | Gate | 状态 | +|---|---|---| +| 1 | Agentic Wallet 创建(邮箱) | 待批 + 用哪个邮箱 | +| 2 | 收款地址/crypto 收入 | 方向已被「直接干」覆盖,金额阈值待定 | +| 3 | Deploy 目标(VPS/Workers/其他) | 等集成规格 → 提方案 | +| 4 | Payment SDK 集成 | 待批 | +| 5 | ASP listing 提交(×4 服务) | 待批 | +| 6 | 大陆资格 | ToS 无明确条款;风险自担推进(Leo 决策默认接受,可复核) | + +## Review 节点(写死,防多点布局变时间黑洞) + +- 上架后 4 周:全组合 <100 有机付费调用/周 且平台 GMV 无增长 → 降维护模式(不下架、停新增、注意力回主线) +- 平台周 GMV 破 $10K 或单服务 >50 单/周 → 加码扩品类 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-independent-market-research.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-independent-market-research.md new file mode 100644 index 00000000..62a3d84c --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-independent-market-research.md @@ -0,0 +1,89 @@ +# Claude 独立市场重估 — OKX.AI / Agent Commerce(Phase 1 盲评) + +- 日期:2026-07-03 +- 方法:deep-research workflow(102 agents / 5 搜索角度 / 20 信源 / 93 条 claim 提取 / 25 条对抗验证:22 confirmed · 3 refuted) +- 约束:**未读本 repo 任何产品代码/文档**,纯外部证据盲评(按 CLAUDE_INDEPENDENT_RESEARCH_PROMPT.md Phase 1 要求) +- Run ID:wf_08dca8da-99d(journal: ~/.claude/projects/-Users-zhangxu/323b8d08-9e03-4895-8576-6b1030109b64/subagents/workflows/wf_08dca8da-99d/journal.jsonl) + +## TL;DR + +OKX.AI(~2026-06-30 上线,BETA)是垂直整合平台:市场、支付、托管、验收/仲裁、核心交易工具**全是平台自营面**。市场战略上大(Bain $300-500B US / McKinsey $3-5T global by 2030)但当下极小且掺水(x402 有机量仅 ~$28-37K/天,约一半交易是刷的)。当下真实高频层是 **A2MCP pay-per-call 数据服务**。**独立验收/验证产品不是正确切入点**——验收已捆绑在平台托管流里(用户签收放款 + 3 天自动验收 + staked Evaluator 仲裁)。一人公司的可防守切入点 = 注册**自有数据 A2MCP 服务**(预测市场信号 / 跨市场数据 / okx-trade-mcp 164 工具未覆盖的交易分析),单次调用定价 $1+,副业挂 Evaluator 赚 OKB bounty。 + +## 1. Market structure map(confidence: high, 3-0) + +- 双边市场:**Agent Marketplace**(ASP 列服务+定价)+ **Task Marketplace**(发任务、交付后付款) +- 三角色:Users / ASPs / staked Evaluators +- 叙事:"one person, one company, $1M/year"(OPC);$1M 是愿景数字不是奖金 +- 服务分类:**A2MCP**(标准化 pay-per-call MCP/API:数据查询、价格 feed、工具 API;需 OKX Payment SDK,无议价)vs **A2A**(议价+托管+用户签收才放款) +- 声誉绑定 OKX Agentic Wallet 单一链上身份 + +## 2. High-frequency service demand map + +- 天然高频层 = **A2MCP pay-per-call**(数据查询/价格 feed/工具 API)——OKX 注册文档自己点名的类型 +- MCP 生态供给已成规模(官方 registry ~9.6K servers,SDK ~97M 月下载)但 **<5% 注册 server 有真实使用**,变现层缺失(MCP 协议无原生支付) +- A2A 议价层 = 高客单低频,且依赖 Task Marketplace 真实吞吐(BETA 期未知,可能只有供给侧) + +## 3. Highest-revenue opportunity ranking(当下 vs 战略) + +| 排名 | 机会 | 当下有需求? | 战略有趣? | +|---|---|---|---| +| 1 | 自有数据 A2MCP 服务($1+/call 价值密集型) | ✅ 平台指定高频道 | ✅ | +| 2 | Evaluator staking 赚 bounty(副业) | ⚠️ 机制未验证是否已 live | ✅ | +| 3 | A2A 高客单交付服务 | ❌ 吞吐未知 | ✅ | +| 4 | 独立验收/验证产品 | ❌ 平台已捆绑 | ⚠️ | +| 5 | 支付/托管/钱包中间件 | ❌ 平台自营 | ❌ | + +定价证据:x402 支付分布中 $1+ 从 49% 涨到 95% of value,10c-$1 段从 46% 塌到 4% → **sub-dollar 微支付叙事弱,按价值密集调用定价 $1+**。 + +## 4. Risks / platform-owned areas + +**平台自营面(别建)**:Agentic Wallet(TEE/session keys/20+ chains)、Payment SDK(X Layer 零 gas)、APP 协议全生命周期(quote→negotiate→escrow→meter→settle→dispute)、内置托管合约、staked Evaluator 仲裁网络(GenLayer 提供 dispute 基础设施)、okx-trade-mcp(164 tools / 11 modules 覆盖 OKX 交易全流程)。 + +**验收/验证不是第三方产品面**(3-0 verified):用户签收放款 + 3 天 auto-accept + rejection→arbitration 全在平台托管流内;Evaluator 是「可参与的角色」(stake OKB 赚 bounty)不是「可自建的产品层」。 + +**其他风险**: +- 平台 days-old BETA,所有能力声明是营销拷贝不是运行数据;escrow/disputes 部分 2026-04 时还标 "coming soon" +- 需求侧证据全部来自 x402/Base 代理指标,且约一半是 wash/self-dealing + PING memecoin 投机 +- 多轨割裂风险:Google AP2 / Stripe Machine Payments / AWS AgentCore / x402 v2 与 OKX APP 互不兼容 → OKX 独占集成有搁浅资产风险 +- Hackathon 奖池仅 14K USDT(最近一季已结束)= 营销曝光非收入;但 X Layer 有 $100M 生态基金 +- 大陆合规叠加:卖预测市场衍生数据是否踩 T0520 关闭线(卖数据 ≠ 促成投注,但需 Leo 自行法律判断) + +## 5. Recommended wedge(synthesis, medium confidence) + +**注册自有数据 A2MCP 服务**: +- 预测市场/Polymarket 衍生信号、跨市场数据 feed、okx-trade-mcp 未覆盖的交易分析 +- 理由:(a) A2MCP 是平台指定高频道 (b) 数据/价格 feed 是 OKX 文档自己举的例子 (c) Leo 的 Polymarket 数据护城河与 OKX 一方工具差异化 (d) 卖数据/分析而非促成投注,避开赌博邻近变现(仍需合规复核) +- 副收入:permissionless Evaluator staking 赚 OKB bounty(**hard gate:staking 涉及资金,需 Leo 批准**) +- **不要建**:支付轨、托管、钱包、独立验收/验证产品、与 okx-trade-mcp 重复的 OKX 交易工具 + +## 6. 7-day execution plan + +1. D1-2:注册 ASP(**hard gate:账号/钱包,需 Leo 批准**),读 A2MCP 注册要求 +2. D2-4:把 2-3 个已有 Polymarket 数据 endpoint 包成 A2MCP 服务(OKX Payment SDK 集成 = **hard gate**) +3. D4-5:定价 $1+/call,写 listing 文案 +4. D5-7:埋点 call counts / revenue,观察平台 BETA 有机需求 +- 全程小赌注:不做大建设,先验证平台有没有买方 + +## 7. Kill criteria + +- 上架 3-4 周后 <100 有机付费调用/周 或 <$50/周收入 → kill +- 平台 BETA 指标显示 Task Marketplace 吞吐接近零(纯供给侧)→ kill 或 park +- 预测市场数据销售出现任何大陆合规红旗 → 立即 kill(对齐 T0520 结论) +- OKX APP 轨道被 AP2/Stripe/x402 v2 明显边缘化 → 降级为观察 + +## Open questions(未决) + +1. OKX.AI BETA 实际吞吐(任务量/ASP 收入/A2MCP 调用量)——现在到底有没有买方? +2. Evaluator 具体经济学(最低 OKB stake、单案 bounty、slashing 实操)+ 大陆个人能否注册 +3. 预测市场衍生数据变现是否清过 T0520 合规线 +4. 多支付轨割裂:押注 OKX 独占是否搁浅资产风险 + +## Refuted claims(对抗验证杀掉的) + +1. "escrow 和支付是平台自营原语、非第三方机会"的**绝对化表述**(1-2)——APP 名义上是开放标准,边界是方向性的不是绝对的 +2. "APP 覆盖全周期因此 escrow/disputes 是平台自营"的推论表述(1-2) +3. "APP 已上线且跨 Solana/Ethereum 全周期可用"(0-3)——dispute 等部分未 live + +## 信源质量 + +Primary:okx.ai 官网/tutorial、okx.com learn(OKX.AI + APP)、X Layer hackathon 页、github.com/okx/agent-trade-kit、APP whitepaper、Chainalysis x402 报告。Secondary:TheBlock、CoinDesk、SiliconAngle。注意:AP2/UCP 细节、RentAHuman 类市场、eval 公司(LangSmith/Braintrust/Arize)的 claim 未通过验证进入结论集,报告在这几个邻域偏薄。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-phase2-comparison.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-phase2-comparison.md new file mode 100644 index 00000000..4f958123 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-claude-phase2-comparison.md @@ -0,0 +1,54 @@ +# Claude Phase 2 — 独立结论 vs Codex Repo 对比 + +- 日期:2026-07-03 +- 前置:Phase 1 盲评见 `2026-07-03-claude-independent-market-research.md`(未读 repo 先出结论) +- Repo 状态:可运行的确定性规则引擎(npm test 5/5 通过)+ 完整策略文档,已被 hard gate 停在本地;仅 demo 页公开 + +## 对比分析 + +### 1. Repo 与独立市场论点吻合处 + +- **A2MCP pay-per-call 是正确车道**:Codex 推荐以 A2MCP(固定 schema、按调用付费)上架、不做 A2A、不做 Evaluator staking——与我的独立结论一致 +- **Codex 自己已察觉核心风险**:`market-demand-research-cn.md` 坦承 acceptance gate「不是最大或最被验证的需求」且「可能是平台内建 feature」;multi-model validation(2026-07-02)给了 Yellow / 直接上线 Red。我的外部证据把这个怀疑坐实了 +- **Hard gate 纪律**:双方完全一致,repo 停在正确的位置 + +### 2. 过拟合 Codex 假设处 + +- **需求假设被平台事实推翻**:验收流程已捆绑进 OKX 托管流(用户签收放款 + 3 天 auto-accept + staked Evaluator/GenLayer 仲裁),第三方「验收门禁」没有独立产品面。Codex 的怀疑是「可能被平台做掉」;外部证据是「已经被平台做掉了」 +- **收入模型悬空**:$85K MRR 目标 vs 整个 x402 轨道有机量仅 ~$28-37K/**天**(且约一半交易是刷的)——目标 MRR 接近整条支付轨的真实规模 +- **定价撞塌陷带**:Quick tier $0.05-0.25 落在已塌陷的 sub-dollar 段(10c-$1 从 46% 跌到 4% of value);市场用脚投票 $1+ 价值密集调用 +- **5 层扩张(声誉/信用层)= 撞平台自营面** +- **数据点冲突待查**:repo 引 Genesis Hackathon $100K 奖池;我验证到的 Build X Hackathon 仅 14K USDT(最近一季已结束)。可能是两个不同活动,需核实 + +### 3. Keep / Change / Kill + +**Keep:** +- `src/auditor.mjs` 确定性审计引擎 → 转内部用途:Leo 自己的 multi-agent 工作流验收(对齐 WORKER_WRITEBACK_PROTOCOL 的 artifact/validation/writeback 验收),这是它今天就有真实用户(Leo 本人)的场景 +- HTTP server / billing-event protocol / discovery metadata / OpenAPI 脚手架 → 这是**通用 A2MCP 服务管线**,换掉 payload 即可复用 +- 全部 OKX.AI 集成知识(A2MCP vs A2A、Payment SDK、402 计费、idempotency) +- sample-inputs/outputs 六个案例 → 内容素材(build in public) + +**Change:** +- ASP 切入点从「验收审计」pivot 到「自有数据 A2MCP 服务」:Polymarket 衍生信号 / 跨市场数据 / okx-trade-mcp 164 工具未覆盖的交易分析,$1+/call +- repo 定位从「产品」改为「A2MCP 服务脚手架 + 内部验收工具」 + +**Kill:** +- 独立验收/验证作为对外收入产品 +- $1M OPC 5 层扩张策略(撞平台自营面) +- sub-dollar 定价层 +- dispute-tier 收入线(依赖未验证的纠纷量 + Evaluator 是角色不是产品层) + +**New direction:** +- 用现有脚手架包 2-3 个 Polymarket 数据 endpoint 成 A2MCP 服务,小赌注验证平台买方是否存在 + +**Next 24h:**(全部只读/本地,不触 gate) +1. 核实 Genesis Hackathon $100K vs Build X 14K 差异 +2. 浏览 OKX.AI BETA marketplace 实际吞吐信号(listing 数、任务数)——回答「有没有买方」 +3. 列出候选数据 endpoint 清单 + 每个的差异化理由 +4. 写预测市场数据变现 vs T0520 合规线的对照 memo,交 Leo 判断 + +**Next 7d:** +- 合规过线 + Leo 批准后:ASP 注册(gate)→ Payment SDK 集成(gate)→ 上架埋点 → 3-4 周 kill criteria 观察(<100 有机付费调用/周 或 <$50/周 → kill) + +**Hard gates:**(不变,全部需 Leo 显式批准) +- OKX.AI 账号 / Agentic Wallet / API key / 钱包地址 / 签名 / 交易 / staking / 支付集成 / ASP 上架 / 公开 repo / push / deploy / leolabs 发布 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-compliance-memo-a2mcp-data-service.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-compliance-memo-a2mcp-data-service.md new file mode 100644 index 00000000..4bee4e22 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-compliance-memo-a2mcp-data-service.md @@ -0,0 +1,31 @@ +# 合规对照 Memo — 金融数据类 A2MCP 服务 vs T0520 关闭线 + +- 日期:2026-07-03 +- 对照源:memory `project-pm-paid-business-closed-mainland`(含 2026-07-02 T014 reconcile refine) +- 结论性质:判断依据整理,**拍板 = Leo** + +## T0520 关闭线的现行版本(2026-07-02 refine 后) + +原判定「一刀切杀 PM 付费业务」已收窄为:**只杀「归集/托管用户资金」那一档**。 +- 最硬钩子:CFTC commodity pool + 刑法 303「组织参与国(境)外赌博罪」——触发点是资金池/组织下注,不是卖分析 +- T014 付费 X-Ray(付费分析服务)已被 Leo informed 放行,先例成立 +- 唯一保留硬线:**不归集、不托管用户资金** + +## 拟议 A2MCP 服务逐项对照 + +| 维度 | 拟议形态 | 是否触线 | +|---|---|---| +| 产品 | 数据/信号/分析 feed,按调用付费 | ✅ 同 T014 已放行类别 | +| 资金 | 不碰用户资金,无 vault,无代客交易 | ✅ 不触唯一硬线 | +| 导流 | 不带投注 referral,不引导用户去 PM 下注;买方是 trading agent 非中文散户 | ✅ 比 T014(affiliate 已跨轴)更干净 | +| 受众 | OKX.AI 全球 agent 生态,非中文社群营销 | ✅ 不构成 237/42 号文「付费导流」场景 | + +## 残余灰点(新增,T0520 未覆盖) + +1. **加密货币收款**:A2MCP 结算走 USDT/X Layer 到收款钱包。大陆个人持续性收 crypto 服务收入本身是灰区(不同于 T0520 的赌博轴,属外汇/crypto 经营轴)。缓解:个人零星服务收入 vs 「经营行为」的界线,金额小时风险低;金额大了需要重新评估主体结构 +2. **OKX 平台本身**:OKX 对大陆用户的服务限制(ASP 注册是否有地区限制待 agent 返回确认) +3. **信号 vs 投顾**:卖行情/数据/统计分析 OK;若 listing 文案写成「跟单赚钱/保证收益」会滑向投顾/荐股形态。文案守住「data & analytics」定位即可 + +## 建议 + +方向本身不被 T0520 阻挡(refine 后允许区),真正要你拍板的是灰点 1:**接受用个人钱包收 crypto 服务收入吗?金额阈值设多少触发重新评估?** 这也是 ASP 注册(hard gate)前必须回答的问题。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-next24h-findings-and-candidates.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-next24h-findings-and-candidates.md new file mode 100644 index 00000000..6852dc5a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-03-next24h-findings-and-candidates.md @@ -0,0 +1,48 @@ +# Next 24h 四项核实结果 + 候选 endpoint 清单 + +- 日期:2026-07-03 +- 数据来源:两个后台研究 agent 实测浏览 okx.ai(zh-hans)+ web 交叉核实;合规对照见 `2026-07-03-compliance-memo-a2mcp-data-service.md` + +## ① Hackathon 数据冲突 — 已裁决 + +- **「Genesis Hackathon $100K」查无此事**:OKX.AI 上线报道(~15 篇)、官方 learn 页、web3.okx.com 均无。`million-dollar-opc-strategy-cn.md` 中该数字视为编造/混淆,基于它的策略段落作废 +- 真实事件:Build X Hackathon 14,000 USDT,2026-04-15 已截止 +- $100M X Layer 生态基金真实存在(2025-08 公告)但无公开自助申请入口,BD 导向 +- ASP 上手路径极轻:`npx skills add okx/onchainos-skills`,Agentic Wallet 仅需邮箱(不需 OKX 交易所账号),收 USDT/USDG + +## ② OKX.AI marketplace 实测吞吐(load-bearing 数字) + +Task Marketplace 实时计数(2026-07-03,okx.ai/zh-hans/tasks): +- **总成交额 $268**(上线 10 天累计)· 已发布 9,610 · 待接单 6,268 · 已完成 1,101 → 单任务均值 ~$0.24 +- 结论:**供给侧堆积 + PR,真实经济吞吐接近零**。原 kill criteria(<$50/周)在平台层面已经预触发 + +Agent 供给侧(分类:世界杯🔥/金融/软件服务/生活/艺术创作): +- 头部全是世界杯营销位:WorldCupCaller 已售140(0.5 USDT)、World Cup Alpha 已售51(1 USDT) +- 金融类非世界杯 ASP 全部个位数销量:CertiK 44、FundingArb 4、Stable Auto Earn 3 +- 需求侧任务里有**未被满足的真实金融需求**:「Polymarket 聪明钱信号追踪」「加密市场脉动报告」「BTC/ETH/SOL 市场概况」等挂单无对应 ASP;但也有大量 X 关注互刷(0.01-0.88 USDT)灌水 + +**关键佐证 Leo 直觉**:平台上卖得动的(世界杯 Alpha=聪明钱跟单 51 单、赔率信号 140 单)恰恰全是「帮人赚钱」类服务——验证「金融/赚钱依据」是正确类目,只是整个市场还太小。 + +## ③ 候选 endpoint 清单(按观察到的需求排序) + +| # | 服务 | 需求证据 | Leo 资产 | 竞争 | 定价 | +|---|---|---|---|---|---| +| 1 | **Polymarket 聪明钱追踪(全市场版)** | Task 端有同名挂单需求;World Cup Alpha(仅世界杯版)51 单 = 品类最好成绩 | profile-address 分析 + PolyData 全量数据 | World Cup Alpha 只做世界杯,无通用版 | 1-2 USDT/call | +| 2 | **事件概率 vs 币价背离信号**(PM 隐含概率 × perp funding/spot) | 白区:官方 tradekit 明说「signal generation depends on external analysis」 | prediction-trader 跨市场基建 | 无 | 1-2 USDT/call | +| 3 | 资金流/异动雷达(加密市场脉动) | Task 端「加密市场脉动报告」挂单 | trader 数据管线 | FundingArb 仅 4 单(品类未证) | 1 USDT/call | +| 4 | 组合风控 readout | 白区但零需求证据 | lb-api/PnL 工具链 | 无 | 观察 | + +首发建议:#1 + #2(一个接现成需求、一个占白区),复用 acceptance-gate 的 HTTP/计费/discovery 脚手架。 + +## ④ 合规 & 资格(两道未清的门) + +1. **大陆 ASP 资格未确认**:注册 KYC-light(邮箱即可)但查不到 okx.ai ToS 是否排除 PRC 居民;OKX 2021 已退出大陆。「没查 KYC ≠ 大陆可用」 +2. **crypto 收入灰点**:ASP 收 USDT = 持续性 crypto 服务收入,见合规 memo 灰点 1,需 Leo 拍板 + +## 总裁决(修正后) + +- 方向类目正确(金融/帮 agent 赚钱),Leo 直觉被平台销量数据佐证 +- 但 **$268 平台总 GMV = 这现在不是收入 lane,是一张便宜期权 + 内容素材** +- 诚实定位:低成本 listing 实验(占位 + build in public 素材 + 学 agent commerce 一手经验),不做收入预期 +- **推进前置条件**(都过才动,全是 hard gate):① Leo 拍板 crypto 收入灰点 ② 确认大陆 ASP 资格 ③ Leo 批准 Agentic Wallet 创建 +- **Park 触发器**(不满足条件就挂起,条件到了再看):平台周 GMV 突破 $10K 或出现独立 ASP 收入实锤报道 → 重新评估 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-04-okx-ai-first-listing-launch-pack.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-04-okx-ai-first-listing-launch-pack.md new file mode 100644 index 00000000..58497d97 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-04-okx-ai-first-listing-launch-pack.md @@ -0,0 +1,153 @@ +# OKX.AI 首发 Listing 包 — World Cup Smart Money Radar + +- 日期:2026-07-04 +- Task:T0521 +- Repo:agent-acceptance-gate +- 状态:local launch pack only;未部署、未接钱包、未接支付、未提交 listing + +## 选择 + +首发服务:`World Cup Smart Money Radar` + +批量候选: + +1. `World Cup Smart Money Radar` → `/world-cup-smart-money-radar` +2. `Polymarket Smart Money Radar` → `/polymarket-smart-money-radar` +3. `Event Probability Crypto Divergence` → `/event-probability-crypto-divergence` +4. `Crypto Market Pulse Report` → `/crypto-market-pulse-report` + +原因: + +- OKX.AI 当前相对有销量的是 World Cup / smart-money / 数据类服务。 +- `World Cup Alpha` 同类服务已卖出 52 单,说明平台上至少有少量买方理解这个需求。 +- 这个服务可以复用 Leo 的 Polymarket 数据资产,且合规定位是 data and analytics,不托管资金、不执行交易。 + +## OKX.AI 首发 Listing 文案草案 + +Service name: + +```text +World Cup Smart Money Radar +``` + +Line 1: + +```text +Tracks profitable World Cup prediction-market wallets and highlights position changes, sides, and confidence. +``` + +Line 2: + +```text +Provide a market name or use all markets; returns compact data-only signals for agent research workflows. +``` + +Fee: + +```text +1 +``` + +Category: + +```text +World Cup +``` + +Endpoint: + +```text +POST https:///world-cup-smart-money-radar +``` + +## API shape + +Request: + +```json +{ + "market": "winner", + "limit": 5 +} +``` + +Response fields: + +- `summary` +- `signals[].market_id` +- `signals[].address_label` +- `signals[].side` +- `signals[].action` +- `signals[].notional_usdt` +- `signals[].seven_day_pnl_usdt` +- `signals[].confidence` +- `signals[].rationale` +- `caveats` + +## Hard gates before real submission + +- Leo confirms Agentic Wallet email. +- Leo confirms receiving wallet/payment setup. +- Leo confirms OKX/payment SDK/API key path. +- Leo confirms stable production endpoint domain. +- Leo confirms OKX.AI ASP listing submission. +- Production endpoint must use fresh data, not demo data. + +## Today’s fastest path after Leo confirms gates + +1. Create Agentic Wallet / ASP identity. +2. Deploy the current HTTP server behind a stable HTTPS endpoint. +3. Replace demo smart-money rows with the fastest available fresh Polymarket-derived snapshot. +4. Add OKX payment middleware or OKX-required charging wrapper. +5. Submit this one listing. +6. After acceptance, clone the same host shape for F1/F2/F3. + +## Batch listing drafts + +### Polymarket Smart Money Radar + +Line 1: + +```text +Tracks profitable Polymarket wallets across markets and highlights position changes, sides, and confidence. +``` + +Line 2: + +```text +Provide a market, topic, or all; returns compact data-only smart-money signals for research agents. +``` + +Fee: `1` + +### Event Probability Crypto Divergence + +Line 1: + +```text +Compares prediction-market event probability changes with crypto spot and funding moves. +``` + +Line 2: + +```text +Provide an event or asset; returns divergence signals for agent research workflows. +``` + +Fee: `1` + +### Crypto Market Pulse Report + +Line 1: + +```text +Summarizes crypto market flows, anomalies, leverage conditions, and watch items for agents. +``` + +Line 2: + +```text +Provide an asset or use all markets; returns a compact data-only market pulse report. +``` + +Fee: `1` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-genesis-hackathon-battle-pack.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-genesis-hackathon-battle-pack.md new file mode 100644 index 00000000..cbdec14d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-genesis-hackathon-battle-pack.md @@ -0,0 +1,51 @@ +# OKX AI Genesis Hackathon 作战包(T0521) + +- 采集:2026-07-05 · 来源:XLayerOfficial 官方推串(x.com/XLayerOfficial/status/2072662619979387264,2026-07-02 发布)+ 官方 Google 表单实拍 +- ⏰ **报名截止:2026-07-17 00:00 UTC**(北京时间 7/17 08:00) + +## 报名四步(官方原文) + +1. 构建一个具有明确现实世界用例的 ASP +2. 提交上市申请:okx.ai/tutorial/asp(✅ 我们已走 CLI,agentId 3977 审核中) +3. **成功上架后**,在 deadline 前填 Google 表单: + https://docs.google.com/forms/d/e/1FAIpQLSfIAgP_WmMGtZ5qyW_LnKZonsjyfOYwV3bduRwiuN4oBmcqjQ/viewform +4. 在 X 发布 demo 帖,带 **#Okxai** 标签,解释你的 ASP + +## 表单字段(已实拍确认) + +- ASP Name → `Leo Labs` +- Agent ID → `3977`(要求"listed 后收到的 ID",需过审) +- ASP Description → listing 完整描述 +- X Account Handle → `@runes_leo` +- **X Participation Post (Link)** → 要求:介绍 ASP + 用例 + **≤90 秒 demo 视频**;鼓励讲产品故事/build 过程/用户场景 +- Telegram Handle → 待 Leo 定 + +## 奖金结构($100K 总池,4/5 推原文) + +| 赛道 | 奖金 | 我们的适配 | +|---|---|---| +| 最佳产品 | $10K/$6K/$4K | 中 | +| 商业潜力 | $10K/$6K/$4K | 中 | +| **营收火箭** | $10K/$6K/$4K | **高——需要真实收入,收款闭环是入场券** | +| **金融副驾驶** | 3×$2,500 | **直接命中**(smart money 数据信号) | +| 软件实用工具 | 3×$2,500 | S1 acceptance gate 可打 | +| 生活伴侣 / 艺术创作 | 各 3×$2,500 | 不打 | +| **社交热议** | 10×$1,000 | build-in-public 内容线顺手打 | + +- 评审维度(5/5 推):产品质量、用例强度、市场契合度、创新性、可靠性、长期潜力、社交影响力 +- 所有获奖者获 OKX 生态**营销与合作伙伴支持**(= T6 OPC 叙事位的直接入口) +- 另有注册链接:web3.okx.com/xlayer/build-x…(5/5 推,待核对是否必需) + +## 打法 + +- 主攻:金融副驾驶(W1+F1 直接命中)+ 社交热议(内容线) +- 冲刺:营收火箭——前提是 7/17 前收款闭环跑通且有真实付费调用 → **SA API key 紧迫性升级** +- 多服务 = 多赛道:S1 上架可加打软件实用工具 + +## 倒计时关键路径(12 天) + +1. listing 过审(在途,audit 24h 级)→ 拿"listed"资格 +2. F1 + S1 追加上架(F1 代码已就绪) +3. SA API key → x402 收款 → 制造真实付费调用记录 +4. ≤90s demo 视频(视频管线:走独立 runtime 渲染,见 feedback-video-render-runtime) +5. X 参赛帖(#Okxai)→ 填表 → 完成报名 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-hackathon-demo-video-script.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-hackathon-demo-video-script.md new file mode 100644 index 00000000..8ca286e5 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-hackathon-demo-video-script.md @@ -0,0 +1,29 @@ +# Genesis Hackathon Demo 视频脚本 v1(≤90s,#Okxai 参赛帖用) + +- 定位:faceless 屏录+动效字幕风(对齐 Leo 视频管线),英文配音/字幕(评委国际化),中文版后出 +- 叙事线:不讲"我做了个工具",讲"one person + agents 在 OKX.AI 从 0 开始做 OPC"——把参赛帖本身变成 OPC 叙事的第一集 + +## 分镜(85s) + +| 时码 | 画面 | 旁白/字幕 | +|---|---|---| +| 0-8s | okx.ai 首页标语 "One person, one company, $1M a year" → 切 Leo Labs ASP 卡片(#3977) | I'm one person. This is my agent company on OKX.AI — Leo Labs. | +| 8-20s | 痛点:Polymarket 页面快速滚动 + 钱包地址流 | Prediction markets leak alpha: the most profitable wallets move first. But no agent can see it — until now. | +| 20-45s | **核心 demo**:终端/agent 调用 World Cup Smart Money Radar → 402 → 支付 → 返回真实 JSON(真实钱包缩写、7d PnL、confidence 高亮) | An agent calls my radar, pays 1 USDT on X Layer, and gets live smart-money signals: who's loading up, which side, how profitable they've been. Real data, straight from Polymarket order flow. | +| 45-60s | 第二服务 F1 全市场版调用({topic:"bitcoin"})→ 信号返回;闪 ASP 服务列表(多服务组合) | Same engine, any market — World Cup today, Bitcoin tomorrow. One ASP, a growing factory of data services. | +| 60-75s | 链上收款记录 / Agentic Wallet 余额变化(若有真实付费调用就用真图;没有就用 testnet/结构图,**不伪造**) | Every call settles in stablecoins, on-chain, automatically. No invoices. No employees. | +| 75-85s | 收尾卡:Leo Labs · Agent #3977 · #Okxai · "Building the OPC playbook in public" | One person, one company — and the agents do the work. Find Leo Labs on OKX.AI. | + +## 素材清单 + +- [ ] okx.ai 首页 + Leo Labs listing 页录屏(过审后录) +- [ ] 终端调用录屏:402 challenge → 支付 → live JSON(x402 接好后录,asciinema/终端录屏加高亮) +- [ ] Polymarket 页面 B-roll +- [ ] 链上 settle 记录截图(OKLink X Layer tx) +- [ ] 收尾卡设计(design-system 起手) + +## 制作注意 + +- 长 render 走独立 runtime(Codex/Terminal),不在 CC 后台 bash 跑(feedback-video-render-runtime) +- voice_text 改动后 voice-align-captions.py 必须 --force +- 红线:60-75s 段没有真实付费记录就不 claim 收入,画面用协议流程图代替 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-launch-prep-pack.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-launch-prep-pack.md new file mode 100644 index 00000000..6215d6be --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-launch-prep-pack.md @@ -0,0 +1,44 @@ +# 预备包:hackathon 表单预填 + S1/F2 listing 草稿(T0521) + +- 日期:2026-07-05 · 全部为"过审即用"的预备材料 + +## A. Hackathon 表单预填(W1 过审后 5 分钟内可提交) + +| 字段 | 值 | +|---|---| +| ASP Name | `Leo Labs` | +| Agent ID | `3977` | +| ASP Description | Core capability: provides prediction-market smart money tracking and crypto market data signals for agent research workflows. Each service accepts simple JSON inputs such as a market keyword and a result limit, and returns compact data-only signals. | +| X Account Handle | `@runes_leo` | +| X Participation Post (Link) | ⏳ 发帖后填(帖含 ≤90s demo 视频 + #Okxai) | +| Telegram Handle | `runesleo` | + +表单:https://docs.google.com/forms/d/e/1FAIpQLSfIAgP_WmMGtZ5qyW_LnKZonsjyfOYwV3bduRwiuN4oBmcqjQ/viewform + +## B. F1 listing 草稿(W1 过审后 48h 内提交,两行结构已按踩坑手册) + +- serviceName: `Polymarket Smart Money Radar`(27 字符 ✓ 与 agent 名不同 ✓) +- serviceDescription(两行 \n 分隔): + - L1: `Tracks profitable Polymarket wallets across all markets and reports position changes, sides, notional size, and a confidence score for each signal.` + - L2: `Provide market or topic as a keyword such as bitcoin or a market name, or all, plus limit from 1 to 10; returns compact data-only smart money signals.` +- serviceType: `A2MCP` · fee: `1` · endpoint: `https://api.leolabs.me/polymarket-smart-money-radar`(✅ 已部署 live) + +## C. S1 listing 草稿(Agent Delivery Acceptance Audit,软件实用工具赛道占位) + +- 前置:worker 需加 POST /agent-delivery-acceptance-audit 路由(复用 src 现有 audit 逻辑,x402 agent 完工后加,≤半天) +- serviceName: `Agent Delivery Acceptance Audit`(31 字符 ⚠️ 超 30,用 `Agent Delivery Audit Gate`(25)✓) +- serviceDescription: + - L1: `Audits an agent task delivery against its task goal, artifacts, and validation evidence, and returns pass, needs review, or fail with missing items.` + - L2: `Provide task, delivery summary, artifacts, and validation as JSON text fields; returns a compact audit verdict with risks and buyer summary.` +- serviceType: `A2MCP` · fee: `1` · endpoint: `/agent-delivery-acceptance-audit`(待路由上线) + +## D. F2 排期(x402 完工后开建,≤1 天) + +- 事件概率×币价背离:PM 隐含概率变动 vs perp funding/spot 动量(数据源:Gamma + OKX 公开行情 API),白区无竞品 +- serviceName 候选: `Event Probability Divergence Radar`(34 超)→ `Event Price Divergence Radar`(28 ✓) + +## E. 参赛帖(发布时走 leo-style skill 出稿,要点先钉住) + +- 主角 W1 + 工厂故事线;英文;#Okxai;≤90s demo 视频 +- 钩子方向:`I'm one person. My agent company just opened on OKX.AI.`(对齐 Star Xu OPC 叙事 + 蓝海参赛帖) +- 红线:无真实收入前不 claim 收入;不暗示 OKX 官方背书 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-opc-longterm-strategy-v1.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-opc-longterm-strategy-v1.md new file mode 100644 index 00000000..9b4187ee --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-05-opc-longterm-strategy-v1.md @@ -0,0 +1,93 @@ +# OKX.AI OPC 长期战略 v1(T0521) + +- 日期:2026-07-05 +- 状态:active strategy · 每两周 review 一次本文件 +- 前置研究:million-dollar-opc-strategy-cn.md · 2026-07-03 四份 research · 2026-07-05 OPC 奖励调研(Explore agent) + +## 一、事实基础(2026-07-05 调研确认) + +1. **"OPC" 不是奖励计划,是平台叙事**:okx.ai 主标语 "The future belongs to OPC: one person, one company, $1M a year"。没有报名表、没有评选、没有奖金池。 +2. **两个真实奖励锚点**: + - Star Xu 个人承诺:第一个在 OKX.AI 做到 $1M 年收入的 OPC,捐 ≥1 BTC 庆祝(社交承诺,非合同)。 + - X Layer「OKX AI Genesis Hackathon」:$100K 总奖池,建 ASP 参赛。 +3. **平台现实**:BETA 上线 10 天全平台 GMV 仅 $268;闭测标杆 ASP = CertiK(安全评估)/ CoinAnk(付费行情)/ GenLayer(争议基础设施)。金融数据类是唯一验证过卖得动的类目(World Cup Alpha 51+ 单)。 +4. **收入机制**:无评奖发钱,纯交易结算(USDT/USDG,x402 pay-per-call 或 A2A escrow)。链上 reputation 逐笔累积、跨交互持久。 + +**推论**:冲 OPC = 把真实收入做起来 + 让 OKX 官方在讲 OPC 故事时非提到我们不可。收入和叙事双线,缺一不可。 + +## 二、定位 + +``` +Leo Labs = OKX.AI 上的 prediction-market & crypto 数据信号 ASP 组合 +一个钱包 · 一个 ASP 身份 · N 个数据服务 · 全部薄 wrapper 复用自有数据资产 +``` + +差异化护城河(按可防御性排序): +1. **PolyData 全量 Polymarket 数据资产**(竞品只有 top-50 列表,我们有持仓变动事件流) +2. **链上 reputation 先发累积**(平台早期,每一笔都在建历史) +3. **多服务组合占位**(需求撞上来时我们已经在货架上) + +## 三、目标分层(诚实版) + +| 层级 | 目标 | 判定 | 概率自评 | +|---|---|---|---| +| T1 存活 | 首个 listing 过审 + 首笔有机付费调用 | 上架后 2 周内 | 高 | +| T2 组合 | 4 服务上架(W1/F1/S1/F2)+ 收款闭环跑通 | 4 周内 | 高 | +| T3 早期信号 | 全组合 ≥100 有机付费调用/周 | kill line 反向 | 中 | +| T4 Hackathon | Genesis Hackathon 提交 + 分奖 | 按官方 deadline | 中 | +| T5 平台红利 | 平台周 GMV 破 $10K 且我们份额 ≥5% | 平台增长挂钩 | 低-中 | +| T6 OPC 叙事位 | OKX 官方内容引用 Leo Labs 作 OPC 案例 | 任意时点 | 低-中 | +| T7 北极星 | $1M ARR(Star Xu 的 BTC) | — | 极低,方向锚 | + +**纪律**:T3 是 4 周 kill line(<100 调用/周且平台无增长 → 降维护模式)。T5-T7 不投前置成本,只在平台数据证明增长后加码。这条线的本质是**低成本期权**,不是 all-in。 + +## 四、五条工作流(Workstreams) + +### WS1 · 产品组合(建设顺序锁定) +1. ✅ W1 World Cup Smart Money Radar — endpoint 已部署,listing 待提交 +2. F1 Polymarket Smart Money Radar 全市场版(同代码二次 listing,W1 过审后 48h 内提交) +3. S1 Agent Acceptance Gate(已建成,改定价即上,零成本占位) +4. F2 事件概率×币价背离(白区无竞品)→ F3 Market Pulse +5. 观察项:Evaluator/Arbitrator 角色(质押 ≥100 OKB + 7×24 + slashable,仅当组合有收入后评估) + +规则:单服务建设 ≤1 天,超了就砍范围。listing 文案严守审核规则(禁 URL/技术栈/名人名/免责声明)。 + +### WS2 · 收入闭环(当前最大缺口) +- 现状:endpoint 无 402 挑战 = 实际免费,listing fee "1" 只是展示价 +- 路径:`@okxweb3/x402-express` 接 OKX 托管 facilitator → **需要 SA API key(hard gate 待 Leo)** +- 策略选择:首个 listing 可以先免费跑(换调用量和 reputation),但 **2 周内必须闭环收款**,否则调用量再大也是零收入 +- 收款地址 = Agentic Wallet X Layer 0x1e1a…16e15(已建) + +### WS3 · 数据质量(信任根基) +- P0:demo 数据 → live Polymarket 数据(2026-07-05 已派 agent 实施中) +- 上游失败降级返回 200 + degraded + caveats,绝不 5xx(付费调用失败伤链上评分) +- 中期:PolyData 派生信号预计算 → 定时推送 serving 层,endpoint 只读快照 +- 数据诚实红线:拿不到的字段置 null + caveats 说明,不编造(reputation 是长期资产) + +### WS4 · 叙事与分发(OPC 故事线) +- **Build in public 主线**:「一个人 + agents 在 OKX.AI 上从 $0 开始做 OPC」——这个过程本身就是 Leo 内容资产(对齐 T310 网站 + X) +- 节奏:里程碑驱动(上架/首单/首周数据/hackathon),不日更凑数 +- 每篇先过 asset_decision 路由(网站资产优先,X 分发回链) +- Genesis Hackathon 提交包:用 million-dollar 文档里的 acceptance layer 叙事 + 真实调用数据 +- 红线:不夸大(没收入不说收入,没官方合作不暗示) + +### WS5 · 合规与风控(既定边界) +- 允许区:卖数据/分析;硬线:不归集/不托管用户资金、不代客交易 +- 大陆 operator 灰点:ToS 无明确条款,风险自担推进(Leo 已决策),出现 50125/80001 region block 立即停下评估 +- crypto 收入金额阈值:待定(当前量级可忽略,月入 >$500 时回来定) + +## 五、节奏与 Review + +| 节点 | 动作 | +|---|---| +| 每周一 | 拉调用量/GMV/平台整体数据 → 一条 build-in-public 素材判断 | +| 2026-07-11 | 首次 review(listing 审核结果 + live 数据质量) | +| 上架 +2 周 | 收款闭环必须完成,否则停新增 listing 先补 | +| 上架 +4 周 | kill line 判定(<100 调用/周 → 维护模式) | +| 每 2 周 | 本文件 review:目标层级达成情况 + 平台 GMV 趋势 | + +## 六、当前 Hard Gates(待 Leo) + +1. ⏳ ASP listing 提交命令(本次已被 classifier 拦,等 Leo 批准 exact command) +2. ⏳ SA API key 申请(WS2 收款闭环前置) +3. ⏳ crypto 收入金额阈值(可延后) diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-demand-side-and-playbooks.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-demand-side-and-playbooks.md new file mode 100644 index 00000000..f5789b50 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-demand-side-and-playbooks.md @@ -0,0 +1,17 @@ +# 需求侧扫描 + 头部打法拆解(T0521 · 2026-07-06) + +## 关键事实 +- 公开任务大厅数据仅对已上架 ASP 开放(recommend-task/find-jobs 对审核中 agent 全拒)→ 过审=解锁需求数据+接单资格 +- 验证热赛道:预测市场(169+161双爆款)/聪明钱(6+家)/代币DD(8+家,BUY-WATCH-SKIP范式)/稳定币收益(103) +- 供给过剩:>50% 服务 sold=0,头部集中 + +## 头部打法 +1. AlphaCopy 日票:一次 x402 付费(0.1)=24h 白名单不限次拉流,169单 +2. CoinAnk 铺货:80×0.01 碎API+模板描述,SKU 覆盖吃长尾,695单 +3. WorldCupCaller 文案:反幻觉叙事+硬核数字+行动闭环链接,0.5×161单 +4. Barker/Otto 漏斗:0.001 数据读取引流→0.05 执行服务变现 + +## 第三批提案(过审后) +- P7 日票模式:聪明钱雷达加 0.5/24h 不限次档(全量数据 vs AlphaCopy 40账户差异化) +- P8 Token DD Verdict:一句话审计闸门 0.05(复用审计框架+公开安全API) +- 文案升级:全服务描述按反幻觉范式重写 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-marketplace-catalog-scan.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-marketplace-catalog-scan.md new file mode 100644 index 00000000..6f9a8bf8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-06-marketplace-catalog-scan.md @@ -0,0 +1,28 @@ +# OKX.AI Marketplace 全目录竞争地图(T0521) + +日期: 2026-07-06 · 方法: onchainos agent search ~130 关键词×分页, 按 agentId 去重, 饱和于 323 ASP + +## 核心数字 +- 唯一 ASP: 323 · 有销量: 61 (19%) · 扫描范围累计销量: 2,096 单 +- 类目分布(数量/销量): 软件 81/1082(52%) · 金融 62/715(34%) · 世界杯 6/257(12%) · 生活 26/75 · 艺术 9/11 · 未分类僵尸 142/0 +- 销量 86% 集中在金融+软件 + +## Top 10 销量 +1. CoinAnk OpenAPI 695(衍生品数据API,fee 0.01) 2. AlphaCopy 169(PM聪明钱,0.1,日订阅打法) 3. Onchain Data Explorer 165(OKX官方链上API) 4. WorldCupCaller 161(0.5) 5. OnChain Arb Scout 137(套利扫描,0.1) 6. Barker Yield 103 7. Otto AI 62 8. SoulMirror 60 9. CertiK 53 10. World Cup Alpha 52 + +## 撞车与空白 +- **聪明钱=红海**: AlphaCopy(169)/World Cup Alpha(52)/SentryX(28)/聪明钱猎手 等 7+ 家;SentryX 已做聪明钱×安全分复合 +- **背离/divergence=空白**: 无一家以背离命名;相邻套利类 Arb Scout 137 单验证价差需求真实 → F2 是最佳切入 +- **Market pulse**: 数据层被 CoinAnk 碾压(695单/0.01);但"多源数据→可执行判断"决策层稀缺(仅 Fan Token Regime 37/macrolens 2) → 判断层是机会 +- 安全/AML 扎堆(10家);通用生产力(email/pdf/写作)全部 0 销量=伪需求区;艺术类需求极弱 + +## 定价 +- 服务 fee 中位数 0.08, 主力带 0.1-1, 判断/报告类敢定 1-3 +- 数据 API 极低价走量(CoinAnk 0.01), AlphaCopy 日订阅不限次已验证有效 +- **我们 4 服务全定 1 USDT 偏贵**: 对齐建议 → 信号类 0.1-0.5/次, 或过审后试日订阅模式 + +## 战略结论 +1. F2(背离雷达)从占位股升级为主力股——空白区+需求已验证 +2. W1/F1 聪明钱线差异化必须强调"持仓变动事件流 vs 结论喊单"+考虑降价到 0.5 +3. 下一个工厂产品方向: 决策层(多源 pulse→仓位建议), 不做原始数据(打不过 CoinAnk) +4. 参赛帖叙事不变(OPC 故事), 但 demo 里 F2 权重提升 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-asp-factory-backlog.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-asp-factory-backlog.md new file mode 100644 index 00000000..bf24d4de --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-asp-factory-backlog.md @@ -0,0 +1,238 @@ +# Leo Labs ASP 工厂 Backlog(2026-07-07) + +状态:**active** · 主 SSOT:本文件 · 关联:`2026-07-07-okx-wave1-roadmap.md` · Hackathon 截止 **2026-07-17 08:00 北京时间** + +## 原则 + +1. **两步走**:Phase 1 把现成能力商品化 → Phase 2 从素材库挖新需求。 +2. **自用 + 外用**:Codex/Hermes 与外部 agent 同调 `api.leolabs.me`。 +3. **卖 gate,不卖 voice**:内容线卖 verify / slop-check / readiness,不整包卖 `leo-style` / 账号发布。 +4. **工厂流水线**:`skill/repo 逻辑 → api.leolabs.me endpoint → onchainos listing → GitHub README`(目标 **1–3 天/SKU**)。 +5. **ASP 不调 relay**:四订阅 lane(Claude/Codex/Cursor/Grok)仅 Leo 自用;外人 x402 服务 = 规则 + 公开 API,**零 LLM 边际成本**。 + +--- + +## 一个 ASP,四条产品线 + +``` +Leo Labs (#3977) @ api.leolabs.me +├── Agent Trust → Delivery Audit Gate(护城河) +├── PM Intelligence → **PM Event Analyst(通用核)** + Preflight + 品类插件/可选 SKU +│ 框架 SSOT: research/2026-07-09-pm-event-analyst-framework.md +│ 双面契约: research/2026-07-12-pm-event-analyst-dual-surface-contract.md +│ (人面=T0530 Copilot Research Unit · agent 面=/pm-event-readout · OKX=渠道) +│ (聪明钱/Regime 等非主线,不优先扩) +├── Research → Token DD → 对齐 Codex asset-dd skill(另对照;非薄 verdict 门面) +└── Creator Ops → Content Verify / Slop Check / Publish Gate / Visual Spec +``` + +**对外叙事(2026-07-09 修正)**:尖刀 = skill 级厚度(事件分析 / 投研),不是薄接口货架占位。 +**PM 线挂法**:一个 Agent 能力 + 一个主服务;品类先 plugin,§准入过关再拆 SKU。 + +--- + +## Phase 1 — 上架队列 + +### Wave A — 零开发(本周 · 等 OKX 审核) + +| # | SKU | 状态 | 动作 | 门禁 | +|---|-----|------|------|------| +| A1 | World Cup Smart Money Radar | listing 审核中 | 过审即可 | OKX listed | +| A2 | Polymarket Smart Money Radar | listing 审核中 | 过审即可 | OKX listed | +| A3 | Agent Delivery Audit Gate | listing 审核中 | 过审即可 | OKX listed | +| A4 | Event Price Divergence Radar | listing 审核中 | 过审即可 | OKX listed | +| A5 | Crypto Market Regime Radar | `live_unlisted` | `onchainos create` + activate | listed 后 | +| A6 | World Cup Upset Alert | `live_unlisted` | `onchainos create` + activate | listed 后 | +| A7 | v18 Quote 推 + listing 截图 | 草稿见 `2026-07-07-v18-quote-and-hackathon-post-draft.md` | 审核通过后发 | publish gate | +| A8 | Hackathon 参赛帖 + ≤90s demo | 脚本待 v2 | #Okxai + 填表 | listed 后 | + +**Wave A 完成后 SKU 数**:6 listed(+2 自 A5/A6)。 + +--- + +### Wave B — 轻包装(各 1–3 天 · 审核期可并行写代码) + +| # | 新 SKU | 复用来源 | 路径 | 定价建议 | 赛道 | +|---|--------|----------|------|----------|------| +| B1 | **Token DD Verdict** | `asset-dd` Quick + `auditor.mjs` | `POST /token-dd-verdict` | 0.05 | Finance + Utility | ✅ code `live_unlisted` | +| B2 | **PM Trade Preflight** | `pm-decision-card` 规则层 | `POST /pm-trade-preflight` | 0.1 | Finance + Best Product | ✅ code `live_unlisted` | +| B3 | **PM Event Readout** | `pm-event-readout` skill | `POST /pm-event-readout` | 0.1 | Finance | ✅ code `live_unlisted` deployed | +| B4 | **Content Verify API** | `content-verify` + 规则层 | `POST /content-verify-claims` | 0.1 | Software Utility | ✅ code `live_unlisted` | +| B5 | **Content Slop Check** | `publish-gate` / `stop-slop` 规则层 | `POST /content-slop-check` | 0.05 | Software Utility · **未实现** | + +**Wave B 优先级**:B1 → B2 → B4 → B3 → B5(黑客松 demo 主角:B2 + A3 + B1)。**B5 排 Wave C 后**,不挡 7/17。 + +**B5 不做**:GLM/relay 改写、`skill-api` Hono 迁移、调用方代烧 Leo 订阅额度。 + +--- + +### Wave C — 数据线扩展(各 2–5 天 · Wave B 后) + +| # | 新 SKU | 复用来源 | 路径 | 定价建议 | +|---|--------|----------|------|----------| +| C1 | **PM Profile API** | `polymarket-toolkit` CLI | `POST /pm-profile` 等 | 0.05 | +| C2 | **Elite Pool Lookup** | `polymarket-data` T014 | `POST /pm-elite-pool-lookup` | 0.1 | +| C3 | **Publish Readiness Gate** | `publish-gate.py` | `POST /publish-readiness` | 0.1 | +| C4 | **Visual Spec API** | `leo-visual-router` + `content/brand/card-layouts` | `POST /visual-spec` | 0.05 | + +--- + +### 不做 ASP(自用 / 合规 / 敏感) + +| 资产 | 原因 | +|------|------| +| `leo-style` / `tg-publish` / `xhs-publish` / `distribute` | 账号、人格、部署 hard gate | +| `prediction-trader` / `pm-manual-trading-lab` playbook | 执行、下单、内部 SSOT | +| `strategy-report` | VPS 实盘隐私 | +| `tg-reader-mcp` / `wechat-reader` | Session / 环境绑定 | +| 6551 转售类 MCP | 第三方 API 依赖 | + +--- + +## Phase 2 — 需求挖矿(素材库) + +| 层 | 路径 | 用法 | +|----|------|------| +| 社群痛点 | `leo-vault/domains/内容创作/社群需求池.md` | ≥3 次标 🔥 → 评估 SKU | +| 外部信号 | `leo-vault/domains/内容创作/外部信号池.md` | 周捞 3 条 triage | +| OKX 需求研究 | `research/2026-07-06-demand-side-and-playbooks.md` | 定价/打法参照 | +| 日更过堂 | `~/.claude/cache/today-todos-{DATE}.json` → `morning_intake_queue` | 当天路由 | +| 路由协议 | `Documents/Codex/EXTERNAL_SIGNAL_TO_OWNED_ASSET_ROUTING_20260630.md` | drop → watch → queue → worker | + +### 已采集 · 待消化(首批) + +| # | 需求信号 | 可能 SKU / 资产 | +|---|----------|-----------------| +| 1 | PM 跟单工具 + 幽灵订单 | Toolkit API / PMQuant 模块 | +| 2 | TG/吃单延迟 250ms | 研究 brief,非立即 API | +| 3 | Oracle / 结算源脆弱性 | `/blog` 或 Risk Case | +| 4 | Dry Run vs Live 幻觉 | 策略内容 + Preflight 叙事 | +| 5 | PMQuant Risk Case Library | 课程后补 | +| 6 | OKX 任务端全市场聪明钱 | 已有 PM radar,观察转化 | +| 7 | 背离信号 | 已 live(A4) | +| 8 | Token DD BUY-WATCH-SKIP | **B1** | + +**节奏**:每周从素材池捞 **1 条** → `demand_check` → 能 1–3 天包的进 Wave B/C。 + +--- + +## Hackathon 多池映射(2026-07-17 截止) + +| 赛道 | 拿什么打 | 优先级 | +|------|----------|--------| +| Software Utility | A3 Audit + B4 Verify + B1 Verdict | A | +| Finance Copilot | A4 Divergence + A5 Regime + B2 Preflight | A | +| Social Buzz | 参赛帖 + build 线程 + #Okxai | A | +| Best Product / Business Potential | 多 SKU 互调 + OPC 故事 | B | +| Revenue Rocket | 免费试用 + 自调用 tx | C(彩票) | +| Lifestyle / Art | — | 不打 | + +**Demo v2 主线(90s)**:Audit → Verdict → Preflight 三连调 + 402 settle。 + +--- + +## Wave B 规格摘要 + +### B1 — Token DD Verdict + +- **输入**:`asset`(ticker / contract / URL)、`tier`(`quick` | `standard` 仅 quick 首版) +- **输出**:`verdict_bucket`(`avoid` | `watch_only` | `research_position` | `tiny_speculative` | `conviction`)、`score_0_100`、`pillars[]`(五支柱 ✅/⚠️/➖)、`hard_stops[]` +- **实现**:`auditor.mjs` 模式 + 公开安全 API(honeypot 等);**非 LLM 终审** +- **GET sample**:固定合约样例 +- **文案**:rule-based research gate,not investment advice + +### B2 — PM Trade Preflight + +- **输入**:`market_url` 或 `condition_id`、`side`(`yes` | `no`)、`size_usd`(可选) +- **输出**:`action`(`trade` | `watch` | `skip`)、`confidence`、`reasons[]`、`risk_flags[]` +- **实现**:Gamma 市场元数据 + 流动性/价差规则 + `pm-decision-card` 阈值;**read-only,无下单** +- **差异化**:交易前闸门,非聪明钱榜单 + +### B3 — PM Event Readout + +- **输入**:`market_url` 或 `condition_id` +- **输出**:`event_summary`、`priced_in[]`、`uncertainty[]`、`tradability`(`high` | `medium` | `low`) +- **形态**:首版 API;复杂案可转 escrow task + +### B4 — Content Verify API + +- **输入**:`claims[]` + `sources[]`(URL 或摘录) +- **输出**:`consensus`、`conflicts[]`、`unsupported[]`、`verdict`(`pass` | `needs_review` | `fail`) + +### B5 — Content Slop Check(规则版 · 替代 Humanize) + +- **输入**:`text`(必填)、`locale`(`zh` | `en`,默认 `en`) +- **输出**: + - `verdict`:`pass` | `needs_edit` | `fail` + - `slop_score_0_100`(越高越像 AI 模板腔) + - `slop_flags[]`:`{ id, severity, excerpt, hint }` + - `readability`:`{ avg_sentence_len, listicle_density, hedge_word_count }` + - `suggested_actions[]`(只给编辑方向,**不改写正文**) +- **规则层(首版,无 LLM)**: + - 套话/空洞词表(中英):`delve` / `landscape` / `值得注意的是` / `综上所述` 等 + - 结构腔:三连列表密度、破折号滥用、全大写标题段 + - 模糊断言:无数字的「显著」「大量」「革命性」 + - 重复 n-gram(同段 3+ 次) +- **与 B4 分工**:B4 = 断言 vs 来源;B5 = 文本腔调 vs 发布可读性 +- **定价**:0.05 USDT;可与 B4 组合叙事「Creator Ops 双闸门」 +- **远期 LLM(单独 gate)**:仅当单价 ≥ 成本×3 且 Leo 开 paid API gate;或 BYOK;**永不接 relay** + +--- + +## 审核通过后 Runbook(Leo 一声「listing」) + +**前置**:Agent #3977 四服务审核通过;Leo 明确授权 `onchainos create`。 + +```bash +cd ~/Projects/agent-acceptance-gate +bash scripts/okx-batch-listing-draft.sh # 打印 6 条 validate/create 草稿 +# 逐条 review → onchainos agent validate-listing → create → activate +bash scripts/okx-asp-self-call.sh # 自调用留 tx 证据 +``` + +| 序 | SKU | endpoint | +|----|-----|----------| +| 1 | Crypto Market Regime Radar | `/crypto-market-regime-radar` | +| 2 | World Cup Upset Alert | `/world-cup-upset-alert` | +| 3 | Token DD Verdict | `/token-dd-verdict` | +| 4 | PM Trade Preflight | `/pm-trade-preflight` | +| 5 | PM Event Readout | `/pm-event-readout` | +| 6 | Content Verify Claims | `/content-verify-claims` | + +**+48h**:录 demo v2 → v18 推(publish gate)→ #Okxai 参赛帖 → 填表(截止 7/17 08:00 北京)。 + +文案 SSOT:`research/2026-07-07-okx-listing-copy-bilingual.md` · 代码:`worker/service-catalog.mjs` → `PENDING_OKX_LISTING_COPY`。 + +--- + +## 执行日历(默认) + +| 窗口 | 动作 | +|------|------| +| 现在 → 审核过 | 并行写 B1/B2 Worker route + 测试;不动新 listing activate | +| 审核 +48h | A5/A6 listing;参赛帖 + demo v2;自调用 10 次留 tx | +| 审核 +1 周 | B1–B4 各上线 1 个;刷新 marketplace 扫描 | +| 持续 | Phase 2 每周 1 条需求 → SKU 或内容/课程队列 | + +--- + +## 验证清单(每个新 SKU) + +- [ ] `npm test` + `worker:check` +- [ ] GET public sample +- [ ] POST free_trial + x402 付费路径 +- [ ] `onchainos validate-listing` + `create`/`update` +- [ ] README 示例请求/响应 +- [ ] 参赛 demo 可录屏 + +--- + +## 变更日志 + +| 日期 | 变更 | +|------|------| +| 2026-07-07 | 初版:Phase 1 Wave A/B/C + Phase 2 素材库 + Hackathon 映射 + B1–B5 规格摘要 | +| 2026-07-07 | **B1/B2 已实现**(Worker `live_unlisted`):`/token-dd-verdict` · `/pm-trade-preflight`;**deployed** api.leolabs.me `23d6ea4c` | +| 2026-07-07 | **B5 改规格**:Humanize+GLM → **Content Slop Check** 规则版;ASP 不调 relay;加审核通过后 batch listing runbook | +| 2026-07-07 | **审核等待 checkpoint**:watcher `new=0`;8/8 GET sample 200;demo 三主角 GET 高亮 OK;`npm test` 全绿;v18+参赛帖草稿落盘 | +| 2026-07-09 | **头像再拒**(圆角/白底)+ `[U1] beta`;策略改为再拒时 **Big Pack**。Leo 定稿头像=`content/brand/leo-labs-avatar-1024.jpg`。runbook+print脚本+Batch2 brief 就绪;**审核中不上链**。预检:禁词0 + 10×402 | diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-hackathon-demo-video-script-v2.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-hackathon-demo-video-script-v2.md new file mode 100644 index 00000000..85074b96 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-hackathon-demo-video-script-v2.md @@ -0,0 +1,64 @@ +# Genesis Hackathon Demo 视频脚本 v2(≤90s,#Okxai) + +- 日期:2026-07-07 · 替代 v1(v1 以 smart-money radar 为主,冲奖弱) +- 定位:faceless 屏录 + 动效字幕 · **英文旁白/字幕**(评委)· 中文版后出 +- 叙事:**Agent 工厂三角栈** — Trust → Research → PM Preflight(不是又一个聪明钱雷达) + +## 主角线(85s) + +| 时码 | 画面 | 旁白/字幕 | +|------|------|-----------| +| 0–8s | okx.ai「One person, one company」→ Leo Labs #3977 卡片(过审后录真 listing) | I'm one person. This is Leo Labs — my agent company on OKX.AI. | +| 8–18s | 三个标签闪过:Audit Gate · Token DD · PM Preflight | Agents don't just need signals. They need gates before they ship, research, or trade. | +| 18–32s | 终端 ① `POST /agent-delivery-acceptance-audit` → `pass` / `needs_review` 高亮 | First: an agent hires my Audit Gate — did the worker actually deliver evidence? | +| 32–48s | 终端 ② `POST /token-dd-verdict` → `verdict_bucket` + pillars 高亮 | Second: Token DD Verdict — quick research gate, not another chatbot report. | +| 48–65s | 终端 ③ `POST /pm-trade-preflight` → `action: watch` + `risk_flags` 高亮 | Third: PM Trade Preflight — trade, watch, or skip before a prediction-market order. | +| 65–75s | 服务列表 6+ SKU 快闪 · 免费试用 → 402 流程图(**无真 tx 不 claim 收入**) | One ASP, many services. Try free, then pay per call on X Layer. | +| 75–85s | 收尾卡:Leo Labs · #3977 · #Okxai · Building the OPC playbook in public | One person, one company — agents do the work. | + +## 录屏命令(生产环境) + +```bash +# 1 Audit(compact input) +curl -sS -X POST https://api.leolabs.me/agent-delivery-acceptance-audit \ + -H 'content-type: application/json' \ + -d '{"task":"Ship health endpoint","delivery_summary":"Added GET /health; npm test passes.","artifacts":["worker/index.mjs"],"validation":["npm test"]}' + +# 2 Token DD Verdict +curl -sS -X POST https://api.leolabs.me/token-dd-verdict \ + -H 'content-type: application/json' \ + -d '{"asset":"ETH"}' + +# 3 PM Trade Preflight +curl -sS -X POST https://api.leolabs.me/pm-trade-preflight \ + -H 'content-type: application/json' \ + -d '{"slug":"will-egypt-win-the-2026-fifa-world-cup","side":"yes","size_usd":100}' + +# 4 Event Readout +curl -sS -X POST https://api.leolabs.me/pm-event-readout \ + -H 'content-type: application/json' \ + -d '{"slug":"will-egypt-win-the-2026-fifa-world-cup"}' +``` + +自调用批跑:`bash scripts/okx-asp-self-call.sh` + +## 素材清单 + +- [ ] okx.ai Leo Labs listing(**审核通过后**) +- [ ] 上述三条 curl 终端录屏(asciinema 或 Terminal 高亮) +- [ ] GET sample 三连(可选 B-roll) +- [ ] 402 协议流程图(无真实付费 tx 时用) +- [ ] 收尾卡 + +## 红线 + +- 不 claim 收入/销量,除非有真实链上 settle 截图 +- 不暗示 OKX 官方背书 +- 聪明钱 radar 仅作 SKU 列表一闪,**不作主 demo** + +## 赛道对应 + +- Software Utility → Audit Gate +- Finance Copilot → Verdict + Preflight +- Best Product → 三服务互调 + 工厂叙事 +- Social Buzz → 本帖 + #Okxai diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-leo-labs-services-multimodel-review-input.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-leo-labs-services-multimodel-review-input.md new file mode 100644 index 00000000..91e542eb --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-leo-labs-services-multimodel-review-input.md @@ -0,0 +1,75 @@ +# Multi-model review input: Leo Labs OKX.AI services (#3977) + +Date: 2026-07-07 +Reviewer question: If a buyer agent or human browses OKX.AI and sees these 4 services, how credible are they? Is pricing fair? What should change before first public tweet + Hackathon? + +## Seller context + +- ASP: Leo Labs, Agent ID 3977, 0 sales, approvalStatus listed (avatar update under re-review) +- Seller: solo INTJ builder, Polymarket quant + AI workflow, has PolyData asset (not fully exposed in APIs yet) +- Strategy: cheap option on OKX.AI + build in public + Genesis Hackathon (deadline 2026-07-17) +- Payment: x402 enabled on production — **unpaid POST returns HTTP 402, 1 USDT per call** (amount 1000000 atomic, 6 decimals) + +## Listed services (all 1 USDT listing fee) + +### 1. World Cup Smart Money Radar +- Endpoint: POST https://api.leolabs.me/world-cup-smart-money-radar +- Claims: tracks profitable World Cup PM wallets, position changes, side, notional, confidence +- Implementation: LIVE — scans Polymarket Gamma (world-cup tag) + data-api trades (takerOnly, min $500) + lb-api 7d profit + positions enrichment. Max 3 markets scanned per call, 120s cache. +- Caveats in code: heuristic, can be wrong/stale, not investment advice +- Competition: WorldCupCaller 166 sales @ 0.5 USDT, World Cup Alpha 52 @ ?, AlphaCopy 171 @ 0.1 (PM smart money leader) + +### 2. Polymarket Smart Money Radar +- Endpoint: POST https://api.leolabs.me/polymarket-smart-money-radar +- Claims: all-market PM smart money, same signal shape as W1 +- Implementation: LIVE — same pipeline as W1 but market discovery via public-search / volume fallback +- Competition: RED OCEAN — AlphaCopy, SentryX, multiple "聪明钱" ASPs + +### 3. Agent Delivery Audit Gate +- Endpoint: POST https://api.leolabs.me/agent-delivery-acceptance-audit +- Claims: audits agent task delivery vs goal/artifacts/validation → pass/needs_review/fail +- Implementation: LIVE — **deterministic rule-based auditor** (regex flags, scoring dimensions), NOT an LLM judge. Good for hard-gate detection, dispute triage. Open-source project: agent-acceptance-gate +- Competition: CertiK 53 sales (security), GenLayer (disputes). Few direct "delivery acceptance" competitors on marketplace +- Unique angle: agent marketplace QA / escrow safety layer + +### 4. Event Price Divergence Radar +- Endpoint: POST https://api.leolabs.me/event-price-divergence-radar +- Claims: PM event probability 24h move vs OKX spot 24h momentum → divergence signals +- Implementation: LIVE — Gamma public-search per asset + OKX v5 ticker, thresholds prob 2% vs spot 0.3% +- Competition: **named divergence niche largely empty**; adjacent OnChain Arb Scout 145 sales @ 0.1 + +## Marketplace benchmarks (2026-07-07 scan) + +- 358 ASPs, 675 service slots, ~2982 cumulative orders, ~$777 rough GMV +- ~78% ASPs zero sales +- Fee median ~0.08 USDT; data APIs often 0.01-0.1; signal/report类 0.1-1 +- Our 4 services all at **1 USDT** — internal research already flagged as **expensive vs competitors** + +## Technical risks buyers might notice + +1. **Pay-before-try**: x402 wall — no free sample call on listed endpoints (buyer must pay 1 USDT to see output quality) +2. **Smart money definition**: "profitable" = leaderboard 7d PnL + large taker trades heuristic, not verified on-chain PnL audit +3. **Scan limits**: only 3 PM markets per radar call — may miss user's topic +4. **Audit gate limits**: rule-based, English-centric patterns; not full code review or legal/compliance +5. **No on-listing proof**: listing descriptions can't include URLs to GitHub or sample JSON +6. **Category mismatch?**: Agent listed under SOFTWARE_SERVICES but 3/4 are finance data signals + +## Not yet listed but deployed live on same host + +- Crypto Market Regime Radar, World Cup Upset Alert (also 1 USDT x402) + +## Review tasks + +For each of the 4 listed services, please answer: +1. **Buyer first impression** (1-2 sentences): trustworthy / skeptical / confused? +2. **Product-market fit** on OKX.AI today: high / medium / low — why? +3. **Pricing verdict**: keep 1 USDT / lower to X / free tier — with competitor anchor +4. **Top credibility gap** and **one fix** (listing copy, price, free sample, product scope, or kill) + +Then synthesize: +- Which 1-2 services to lead with in tweet + Hackathon demo +- Recommended price table for all 4 +- Any service to pause/rename/reposition before public launch +- Overall: 可继续 / 修改后继续 / 停止扩服务先修核心 + +Be blunt. Leo prefers truth over ego. Platform GMV is tiny (~$800) — this is reputation + optionality play, not revenue yet. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-listing-copy-bilingual.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-listing-copy-bilingual.md new file mode 100644 index 00000000..34d584e0 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-listing-copy-bilingual.md @@ -0,0 +1,158 @@ +# OKX Listing 文案(双语规范 · 2026-07-07) + +SSOT 代码:`worker/service-catalog.mjs` → `OKX_LISTING_COPY` + `PENDING_OKX_LISTING_COPY` + +## 产品原则(Leo 2026-07-08) + +- **主用户**:人 + 调用 ASP 的 AI Agent;listing 页必须 **L1 英文 + L2 中文**(同字段两行),不能只英文。 +- **截图**:只用 **okx.ai 真页自截**;禁止 CLI/HTML 拼的假预览当配图。 +- **定价展示**:旧截图因价高废弃;Wave 1 链上价为 0.05–0.2 USDT/次 + 每 IP 每服务 1 次免费试用。过审后重截。 +- **更新时机**:**审核中不零碎 update**。默认等本轮结果;若再拒 / Leo 说「listing」→ 按 `research/2026-07-09-okx-resubmit-big-pack.md` **一次打包**(sharp 头像 + 双语 + 现有 4 服务 + 6 个 unlisted create)。若本轮直接 listed → 24h 内刷双语(若仍纯英文)+ 真页截图,新 SKU 同日一批 create。 + +## 规范 + +| 字段 | 语言 | 限制 | +|------|------|------| +| `name` | 英文 `Leo Labs` | 品牌名 | +| `description` L1 | 英文:ASP 能力总述 | 无 URL | +| `description` L2 | **中文总述** + 谁该用 | 同上 | +| `serviceName` | **英文**,≤30 字符 | 与 ASP 名 `Leo Labs` 区分 | +| `serviceDescription` L1 | 英文:干什么 + 输出什么 | 无 URL、无技术栈堆砌 | +| `serviceDescription` L2 | **中文一句** + 输入提示 | 同上 | +| API path / `service_id` / JSON | 英文 | 不改 | + +## Agent #3977 简介(2026-07-24 尖刀版 · 过审后 update) + +审核中不零碎 update。定稿文件:`research/2026-07-24-tip-knife-agent-description.txt` + +``` +L1: Pay-per-call data gates for agents: live PM/crypto signals that go stale, pre-trade decision cards, and delivery/publish checks. JSON in, structured verdict out — not a chatbot, not trade tips. +L2: 给 Agent 的按次付费数据闸:会过期的预测市场/加密信号、下单前决策卡、交付与发布检查。JSON 进、结构化结论出;非聊天、非喊单。 +``` + +旧版(目录感,勿再作为主简介): +``` +L1: Data-only gates for agents and solo builders: prediction-market signals, delivery audits, token DD, and trade preflight. JSON in, structured verdict out — not chatbots or trade tips. +L2: 给 Agent 和独立开发者的数据闸门:预测市场信号、交付验收、代币尽调、下单前检查。JSON 进、结构化结论出;不是聊天机器人,也不是喊单。 +``` + +链上 service id(update 用):以 `onchainos agent service-list --agent-id 3977` 为准(2026-07-24:**30207+ / 36661+**,共 26 服务)。 + +--- + +## 已上架(approvalStatus=6 已拒 · 可再提大包时 `onchainos update`) + +| ID | serviceName | 价 (USDT) | endpoint | +|----|-------------|-----------|----------| +| 29496 | World Cup Smart Money Radar | 0.1 | `/world-cup-smart-money-radar` | +| 29497 | Polymarket Smart Money Radar | 0.05 | `/polymarket-smart-money-radar` | +| 29498 | Agent Delivery Audit Gate | 0.2 | `/agent-delivery-acceptance-audit` | +| 29499 | Event Price Divergence Radar | 0.1 | `/event-price-divergence-radar` | + +### 29496 World Cup Smart Money Radar + +``` +L1: Heuristic World Cup prediction-market wallet signals from large public trades and 7-day leaderboard stats; data only. +L2: 世界杯预测市场聪明钱雷达:大额成交与7日盈利钱包信号。输入 market 关键词(如 winner、队名或 all)+ limit 1-10。 +``` + +### 29497 Polymarket Smart Money Radar + +``` +L1: Heuristic Polymarket wallet signals from recent large trades; topic search with limited coverage per call. +L2: Polymarket 全市场聪明钱雷达。输入 market/topic 关键词(如 bitcoin 或 all)+ limit 1-10;数据信号,非投资建议。 +``` + +### 29498 Agent Delivery Audit Gate + +``` +L1: Rule-based audit of agent task delivery vs goals and evidence; returns pass, needs review, or fail. +L2: Agent 交付验收闸门:对照任务目标与证据,输出 pass/需复核/fail。输入 task、delivery_summary、artifacts、validation。 +``` + +### 29499 Event Price Divergence Radar + +``` +L1: Flags where 24h prediction-market probability moves diverge from 24h OKX spot momentum on major crypto assets. +L2: 事件概率与币价背离雷达:PM 24h 概率变动 vs OKX 现货 24h 动量。输入 asset(bitcoin/ethereum/solana 或省略查主流)。 +``` + +--- + +## 待 create(listed 后 `onchainos create`) + +| service_id | serviceName | 价 | endpoint | +|------------|-------------|-----|----------| +| crypto_market_regime_radar | Crypto Market Regime Radar | 0.1 | `/crypto-market-regime-radar` | +| world_cup_upset_alert | World Cup Upset Alert | 0.1 | `/world-cup-upset-alert` | +| token_dd_verdict | Token DD Verdict | 0.05 | `/token-dd-verdict` | +| pm_trade_preflight | PM Trade Preflight | 0.1 | `/pm-trade-preflight` | +| pm_event_readout | PM Event Readout | 0.1 | `/pm-event-readout` | +| content_verify_claims | Content Verify Claims | 0.1 | `/content-verify-claims` | + +### Crypto Market Regime Radar + +``` +L1: Blends OKX spot momentum, perp funding/premium and Polymarket drift into risk_on/off/neutral/mixed with explainable score. +L2: 加密市场状态雷达:现货动量+资金费率+PM 情绪 → risk_on/off/neutral 及 0-100 分。输入 focus/asset 关键词 + limit。 +``` + +### World Cup Upset Alert + +``` +L1: Flags profitable wallets entering low-probability World Cup outcomes; potential upset positioning signals only. +L2: 世界杯冷门预警:盈利钱包涌入低概率赛果。输入 market 关键词(winner/队名/all)+ limit 1-10;数据信号,非投注建议。 +``` + +### Token DD Verdict + +``` +L1: Quick rule-based token research gate; optional DEX liquidity scan for EVM contracts; returns avoid/watch/research buckets. +L2: 代币快速尽调闸门:规则引擎输出 avoid/观望/可研究等分桶;EVM 合约可查 DEX 流动性。输入 asset(ticker 或合约地址)。 +``` + +### PM Trade Preflight + +``` +L1: Read-only eligible/watch/skip gate before a Polymarket order; checks liquidity, price zone, and spread. eligible ≠ buy tip. +L2: 预测市场下单前检查:eligible/观望/跳过,只读不下单。eligible 表示机械检查通过,不是买入建议。输入 market_url 或 slug + side(yes/no),可选 size_usd。 +``` + +### PM Event Readout + +``` +L1: Football-ready event evidence card: same-event market matrix, fixture-aware tradability, and football/tennis category depth when available. Not a buy tip. +L2: 预测市场事件解读卡(Football-ready):同场矩阵、赛程/fixture 可交易性,足球/网球品类深度可选。输入 market_url 或 slug;不下单。 +``` + +### Content Verify Claims + +``` +L1: Rule-based check that publish claims overlap caller-supplied source excerpts; pass, needs_review, or fail. +L2: 发布前断言核查:对照你提供的原文摘录核对数字/关键词。输入 claims[] + sources[].text;不抓网页。 +``` + +--- + +## 对外中文对照(X / 参赛帖用,非 listing 字段) + +| 英文 SKU | 中文人话 | +|----------|----------| +| Agent Delivery Audit Gate | Agent 交付验收闸门 | +| Event Price Divergence Radar | 事件概率×币价背离雷达 | +| PM Trade Preflight | 预测市场下单前检查 | +| PM Event Readout | 预测市场事件解读卡 | +| Content Verify Claims | 发布前断言核查 | +| Token DD Verdict | 代币快速尽调闸门 | +| Crypto Market Regime Radar | 加密市场状态雷达 | +| World Cup Smart Money Radar | 世界杯聪明钱雷达 | +| Polymarket Smart Money Radar | Polymarket 聪明钱雷达 | +| World Cup Upset Alert | 世界杯冷门预警 | + +--- + +## 下一步 + +1. **今晚再提大包**(approvalStatus=6):对 **29496–29499** 一次 update 刷双语 + 定稿头像;见 `research/2026-07-09-tonight-submit-checklist.md` +2. **同批 create 6 个 unlisted**:`validate-listing` → `create` ×6 → **一次** `activate`(命令稿:`research/2026-07-09-okx-big-pack-commands.sh`) +3. **参赛帖 / 视频**:明天黑客松视频推文;今晚不做 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-wave1-roadmap.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-wave1-roadmap.md new file mode 100644 index 00000000..8b1b7075 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-okx-wave1-roadmap.md @@ -0,0 +1,61 @@ +# Leo Labs OKX 路线图(2026-07-07) + +状态:**Wave 1 审核中 · ASP 工厂 active · Hackathon 并行** +**主 Backlog SSOT**:`research/2026-07-07-asp-factory-backlog.md` + +--- + +## Wave 1 — 优化 → 审核 → 发推 ✅ 代码完成 + +| 步骤 | 动作 | 状态 | +|------|------|------| +| 1 | Worker:分服务定价 + 每 IP 每服务 1 次免费试用 + GET sample | ✅ | +| 2 | Cloudflare deploy + KV `TRIAL_KV` | ✅ 2026-07-07 | +| 3 | onchainos:4 服务改价 + listing 文案 + activate | ✅ 审核中 | +| 4 | 等 OKX 审核(头像 + 服务更新) | 待 | +| 5 | Leo 截图 listing → 发 v18 Quote 推 | 待审核通过 | + +### 定价(链上 listing + x402) + +| 服务 | 新价 (USDT) | +|------|-------------| +| Agent Delivery Audit Gate | 0.2 | +| Event Price Divergence Radar | 0.1 | +| World Cup Smart Money Radar | 0.1 | +| Polymarket Smart Money Radar | 0.05 | + +计费:每 IP 每 path 首次 POST 免费 → 之后 x402;GET = public sample。 + +--- + +## Wave A — 零开发上架(见 backlog § Wave A) + +- 过审后:**Regime Radar** + **Upset Alert** listing +- **Hackathon**:参赛帖 + demo v2 + 填表(截止 7/17 08:00 北京) + +--- + +## Wave B — 轻包装 SKU(审核期并行开发) + +优先级:**B1 Token DD Verdict** → **B2 PM Trade Preflight** → B4 → B3 → B5 +规格:`asp-factory-backlog.md` § Wave B 规格摘要 + +--- + +## Wave C — 数据线(Wave B 后) + +PM Profile API · Elite Pool Lookup · Publish Readiness Gate · Visual Spec API + +--- + +## Phase 2 — 需求挖矿 + +素材库与每周节奏见 `asp-factory-backlog.md` § Phase 2。 + +--- + +## 本阶段不做 + +- 不整包卖 leo-style / 账号发布类 skill +- Revenue Rocket 不押宝(有 tx 即可) +- 不堆第 N 个 smart-money radar SKU diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-v18-quote-and-hackathon-post-draft.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-v18-quote-and-hackathon-post-draft.md new file mode 100644 index 00000000..34fe9e82 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-07-v18-quote-and-hackathon-post-draft.md @@ -0,0 +1,216 @@ +# v2.2 Quote + Hackathon 参赛帖草稿(Audit + Football · 2026-07-10) + +状态:**local draft · 未发布/未提交表单 · publish gate 未跑**。仅在 listing 终态确认通过、截图与 v2.2 视频复核后使用。 + +战略对齐:视频尖刀是 **Agent Delivery Audit Gate + Football Event Analyst**;Tennis 与 research gates 只作组合提示,不在 48 秒里展开。 + +## Video structure QA(对应已渲染 v2.2) + +- **Hook(0–6s)**:`I'm one person. This is Leo Labs — my agent company on OKX.AI.` 先交代人物、产品与平台。 +- **Retain(6–34s)**:先问“工人真的交证据了吗”,再展示 Football 同场 8 linked events covering 350 markets,形成第二个信息增量。 +- **Reward(34–48s)**:收束到 Audit + Football 尖刀与 one-person-company / OPC build-in-public 叙事。 +- `external_model_review`: done · see `2026-07-10-t0521-hackathon-model-review.md` + inline model-review below. + +model-review: grok+codex 2026-07-10 · first pass=revise (trade wording / listing tense / 350 markets口径) · fixes applied in v2.3 copy · verdict: ship_after_listing + +--- + +## Demand / Asset(v4.4) + +```yaml +demand_check: demand_matched +audience_demand: "Agent builders / PM traders who need gates before ship, research, or analyze an event — not another sentiment feed" +top_performer_pattern: "hackathon demo posts that lead with one concrete proof clip + one sharp product claim + #Okxai; skipped live X search this pass — pattern from prior #Okxai scan in T0521 context" +leo_artifact: "Leo Labs #3977 · api.leolabs.me · demo v2.2 Audit+Football 48.3s · 8 linked events covering 350 markets evidence in frames" +reader_takeaway: "Before accepting agent work, ask for evidence; before sizing a football view, demand the full same-event matrix — not one price" +publish_or_park: publish_candidate +text_quality_tier: main_publish +ship_gate: listing_approved_then_leo_public_publish +``` + +--- + +## Creator System 检查 + +- 内容承诺等级: reusable_asset +- 先试再推荐: 本地 demo v2.2 已渲染并通过音画校验;listing 通过后再公开推荐入口 +- 给步骤: 看 48s demo → 打开 api.leolabs.me → 调 Audit / Football +- 给截图: listing 页截图(过审后)+ demo 帧(8 linked / 350 markets) +- 给入口: api.leolabs.me · OKX.AI Agent #3977(过审后) +- 积累回访理由: one-person-company 闸门组合可持续加品类,不靠单次喊单 +- 读者入口: X 参赛帖 + demo 视频;次入口 api.leolabs.me +- 目标指标: 黑客松表单提交成功;社交热议赛道可见;不 claim 收入 +- next_required_asset: listing 通过截图 + 发帖 URL 回填表单 + +--- + +## Algorithm 5 Check + +- topic_pk: OKX.AI hackathon · agent gates · Audit + Football event matrix +- not_dwelled: open on “one person / burned by empty delivery” not feature laundry list +- profile_follow: builders who want proof-before-accept + PM event matrix depth +- share_trigger: quoted_line: "If the worker said done — where is the evidence?" +- author_diversity: same_topic_24h_count: 0 (no Leo #Okxai hackathon post in last 24h) +- opening_pattern: first_person_pitfall → gate thesis → 48s demo proof + +### 发后24h复盘计划 + +- primary_metric: impressions + bookmark on hackathon post +- dm_share_proxy: replies asking endpoint / how to call +- profile_follow_signal: profile visits from #Okxai traffic +- negative_signal_watch: “is this trading advice?” / overclaim on 350 markets +- decision_rule: if bookmarks≥3 or useful reply≥2 → quote with listing screenshot; else leave as form evidence only + +--- + +## A. v18 Quote(listing 过审 + 截图后 · `light_quote`) + +```yaml +demand_check: demand_matched +audience_demand: "Agent builders / PM traders who need gates before ship, research, or analyze an event — not another sentiment feed" +leo_artifact: "Leo Labs #3977 listed on OKX.AI · api.leolabs.me · Audit + Football live" +publish_or_park: publish_candidate +text_quality_tier: light_quote +``` + +### recommended(英文 · 配已核验的 OKX listing 截图) + +More signals won't save a bad agent. + +You need a gate before you accept work. +You need the full event matrix before you analyze an event. + +Leo Labs (#3977) on OKX.AI: +Audit Gate + Football Event Analyst. + +api.leolabs.me + +### v18 alt(中文 · 同截图) + +Agent 不缺信号,缺的是闸门。 + +Leo Labs 已在 OKX.AI 上架(#3977)——先验收 Agent 是否真的交了证据,再把同场足球市场展开成完整矩阵,而不是复读价格。 + +api.leolabs.me + +### Humanizer / Voice + +```text +- text_quality_tier: light_quote +- final_text_taste: AI=3.0 / YOU=57.0 / scope=final_text_only (quote EN) · warning_margin +- liuren-edit-pass: pass +- voice-layer v3 audit: 6/7 clean · N/7 hits #brochure-lite (acceptable for light_quote) +``` + +**发前必做**:`publish-gate.py` · 附 OKX listing 页截图 · 不 claim 收入 · listing 未过审禁止发 + +--- + +## B. Hackathon 参赛帖(demo 视频 READY · listing 通过后 · `main_publish`) + +表单:https://docs.google.com/forms/d/e/1FAIpQLSfIAgP_WmMGtZ5qyW_LnKZonsjyfOYwV3bduRwiuN4oBmcqjQ/viewform + +| 字段 | 值 | +|------|-----| +| ASP Name | Leo Labs | +| Agent ID | 3977 | +| X Handle | @runes_leo | +| TG | runesleo | +| Participation Post | ⏳ 发帖后回填链接 | + +### recommended(中文主帖 · Leo 2026-07-11 拍板:中文优先,英文没流量) + +writer_model: grok-4.5 · writer_route: leo-grok · non_grok_prose: none + +#### LIVE(过审后发 · 默认) + +一个人做 Leo Labs。 +OKX.AI Agent #3977,已上架。 + +被 agent 坑过太多次。 +说交付了,证据没有。 +说跑完了,打开一看是空的。 + +后来改了:不先堆信号,先装闸门。 +工人说做完了——证据在哪? +过不了,就不收。 + +48 秒 demo 两把尖刀: +Agent Delivery Audit Gate,卡空口交付。 +Football Event Analyst,同场 8 个关联事件、覆盖 350 个市场,一次摊开,不复读单个赔率。 + +卖闸门,卖证据。不喊单。 + +入口 api.leolabs.me +#Okxai + +#### PRE_LISTING(审核中口径 · 一般不发) + +一个人做 Leo Labs。 +OKX.AI Agent #3977,提审冲上架。 + +被 agent 坑过太多次。 +说交付了,证据没有。 +说跑完了,打开一看是空的。 + +后来改了:不先堆信号,先装闸门。 +工人说做完了——证据在哪? +过不了,就不收。 + +48 秒 demo 两把尖刀: +Agent Delivery Audit Gate,卡空口交付。 +Football Event Analyst,同场 8 个关联事件、覆盖 350 个市场,一次摊开,不复读单个赔率。 + +卖闸门,卖证据。不喊单。 + +入口 api.leolabs.me +#Okxai + +### 英文稿(降级备查 · 不主推) + +I'm one person. Leo Labs is live on OKX.AI (#3977). + +I got burned by agents that said "done" with no evidence. +So I built gates first — not another signal feed. + +48s demo: +1/ Audit Gate — did the worker hand over proof? +2/ Football — 8 linked events covering 350 markets → one event view. Not a price reprint. + +api.leolabs.me #Okxai + +### Humanizer / Voice + +```text +- text_quality_tier: main_publish +- language_primary: zh (Leo override 2026-07-11: EN low traffic) +- writer_model: grok-4.5 +- writer_route: leo-grok +- non_grok_prose: none +- final_text_taste: pending_recheck_after_leo_taste +- liuren-edit-pass: pending_leo_review +- voice-layer v3 audit: pending_leo_review +- manual_polish_gate: pending_leo_review +- preview: /Users/zhangxu/Projects/_inventory/2026-07-10/t0521-hackathon-post-preview.html +``` + +--- + +## C. 发序(仍是 hard gate;Leo 明确批准后才执行) + +1. 只读核验 listing 已通过,并截取无敏感信息的 listing 图;审核中不做零碎 update。 +2. 复核 `okx-asp-demo-v2.2-football-audio-fixed.mp4` 的整片音画、时长与字幕;确认画面文案为 **8 linked events covering 350 markets**。 +3. 对本文件跑 `prepublish-check.py --type video`,并人工确认证据一致。 +4. **Quote** + listing 截图(public publish gate)。 +5. **参赛帖** + ≤90s demo + `#Okxai`(public publish gate)。 +6. 填 Google 表单并回填 Participation Post(account/form submission gate;截止 **2026-07-17 08:00 北京**)。 + +--- + +## Change log · 2026-07-10 v2.3(model-review 后) + +- 统一口径:`8 linked events covering 350 markets` +- 去掉 / 弱化交易建议口吻:`trade a match` → `analyze a match`;`match thesis` → `event view` +- listing 未过审时英文主帖用 `building Leo Labs for OKX.AI`,不用 `is live on` +- 删 EN 主帖 “Tennis and research gates sit beside them”(审稿:brochure) +- 独立 review 回执:`research/2026-07-10-t0521-hackathon-model-review.md` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-pm-event-analyst-framework.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-pm-event-analyst-framework.md new file mode 100644 index 00000000..b32de39d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-pm-event-analyst-framework.md @@ -0,0 +1,218 @@ +# PM Event Analyst · 产品架构框架(可扩展 SSOT) + +日期:2026-07-09 +状态:**active_framework** +Owner thread:product_distribution(T0521) +实现 repo:`agent-acceptance-gate` +本地 skill 源:`~/.codex/skills/pm-event-readout` + `pm-decision-card` + 品类 skills + +关联: + +- 对照样例:`research/2026-07-09-pm-event-readout-contrast.md` +- 品类 brief(并入本框架,不再单独当总产品):`research/2026-07-09-pm-category-analyst-batch2-brief.md` +- 工厂总表:`research/2026-07-07-asp-factory-backlog.md` +- **双面契约(人面 Copilot ↔ agent readout)**:`research/2026-07-12-pm-event-analyst-dual-surface-contract.md` · mapper `src/pm-event-to-copilot-research-unit.mjs` +- 投研线(并行、不抢):Codex 优化 `asset-dd-and-opportunity-evaluation` → 另文 ASP 包装 + +--- + +## 1. 一句话 + +**一个 Agent 能力 = 通用预测市场事件分析;品类是加深插件;对外先卖一个主服务,验证后再拆品类 SKU。** + +不是「只有网球/足球/天气/Musk 四种能分析」,也不是「一上来四个平行服务」。 + +--- + +## 2. 分层架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Agent(身份) Leo Labs / PM Analyst │ +│ 一个 ASP 钱包身份 · N 个服务槽 · 统一信任与入口 │ +└─────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ +┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ 主服务(先做) │ │ 品类服务(后加) │ │ 配套服务 │ +│ PM Event │ │ tennis/football │ │ Preflight │ +│ Analyst │ │ weather/musk/… │ │ (eligible/watch/ │ +│ 任意 slug │ │ 更深 · 可更高价 │ │ skip) │ +└───────────────┘ └─────────────────┘ └──────────────────┘ + │ │ + ▼ ▼ +┌───────────────────────────────────────┐ +│ 本地 skill 源(持续优化) │ +│ pm-event-readout(通用核) │ +│ pm-decision-card(动作卡 · 默认可选) │ +│ pm-*-match / weather / musk(插件) │ +└───────────────────────────────────────┘ +``` + +| 层 | 是什么 | 不是什么 | +|----|--------|----------| +| **Agent** | 身份 + 叙事入口 | 不是「每个品类一个 Agent」 | +| **L0 通用核** | 任意事件读出框架 | 不是体育专用 | +| **L1 品类插件** | 四种(及未来新类)加深规则 | 不是唯一能分析的范围 | +| **L2 决策卡** | buy/size/no_trade(可选下游) | 不是 v1 主卖点;不下单 | +| **配套** | Preflight 机械可交易检查 | 不是事件分析本身 | + +--- + +## 3. L0 通用核(任意事件) + +**触发**:已知 `slug` / `condition_id` / `market_url`。 +**本地权威字段**:`pm-event-readout` Required Readout。 + +最低对外 JSON(ASP v1 必须具备): + +| 字段组 | 要求 | +|--------|------| +| 身份与价格 | event, market, current_price, event_time, event_time_source | +| 矩阵 | `event_matrix[]`, `matrix_status`, `related_market_count`, `missing_market_groups` | +| 含义 | `market_implied_view`, `base_case`, `what_is_already_priced`, `what_may_not_be_priced` | +| 诚实边界 | `key_uncertainties`, `sources_read`, `tradability`, `fixture_status` | +| 路由 | `category`(generic 或已注册品类), `next_decision_card_needed`, `hard_gate` | + +硬规则: + +1. **事件概率 ≠ 交易吸引力**(读出不输出下单指令)。 +2. 矩阵缺失或外部锚冲突时,`tradability` **不得**标 `high`。 +3. 禁止与价格矛盾的模板句(对照样例里 82% 却写「mid-range」那种)。 +4. ASP **不调** Leo 四订阅 relay;v1 = 规则 + 公开 API(Gamma 等)。 +5. Strip Leo-only:`bankroll_pct`、个人仓位、内部 gate 文案。 + +**对照验收盘**:Fed July hold(见 contrast 文件)——能回答矩阵、跨市锚差、hold@0.82 是否已贵。 + +--- + +## 4. L1 品类插件(可扩展注册表) + +品类 = 在 L0 之上叠加的规则/数据适配器,**不是**独立产品线名称。 + +| category id | 本地 skill | 加深内容(摘要) | ASP SKU 策略 | +|-------------|------------|------------------|--------------| +| `generic` | `pm-event-readout` | 仅 L0 | 主服务默认 | +| `macro_fed` | (可用 L0 + Fed 锚) | FOMC 矩阵 + FedWatch 类锚 | 先做进主服务适配器,不急独立 SKU | +| `football` | `pm-football-match` | 阵容/赛程/动机/同事件矩阵 | 主服务 plugin → 验证付费后再拆 SKU | +| `tennis` | `pm-tennis-match` | fixture 核验 + 全矩阵 | 同上 | +| `weather` | `pm-weather-ladder` | 站点/METAR/ladder/split | **更像该独立 SKU**(输出形态特殊) | +| `musk` | `pm-musk-count` | 发推档 ladder | **更像该独立 SKU** | +| (未来)`election` / `crypto_event` / … | 新 skill | 按 §6 准入 | 先 plugin,后 SKU | + +主服务响应里带: + +```json +"category": "generic", +"category_depth": "core_only", +"plugins_available": ["football", "tennis", "weather", "musk"] +``` + +命中已实现插件且 `depth=category`(或买家调品类 endpoint)时:`category_depth: "enriched"`,并多返回品类块(matrix 形状、fixture 核验等)。 + +--- + +## 5. 对外服务怎么挂(Agent 一个 · 服务可增) + +### 5.1 现在就规划的槽位 + +| 服务 | path(建议) | 何时上 | 定价带 | +|------|--------------|--------|--------| +| **PM Event Analyst**(主) | `/pm-event-analyst`(或升级现 `/pm-event-readout`) | **下一项实现** | 0.2–0.5 | +| PM Trade Preflight | `/pm-trade-preflight` | 已有;文案 `eligible` | 0.1 | +| (可选)Weather Ladder Analyst | `/pm-weather-analyst` | L0 稳 + 天气样例过关 | 0.3–0.5 | +| (可选)Musk Count Analyst | `/pm-musk-analyst` | 同上 | 0.3–0.5 | +| (可选)Football / Tennis Analyst | `/pm-football-analyst` 等 | 有付费/复购信号再拆 | 0.3–0.5 | + +### 5.2 默认挂法 + +- **先只宣传 / 深做主服务**(通用核)。 +- 品类默认以 **plugin 字段** 活在主服务里。 +- 仅当 §6 准入通过,才 `onchainos create` 独立品类服务。 + +### 5.3 与旧 10 SKU 关系 + +| 旧 SKU | 在本框架中的位置 | +|--------|------------------| +| `/pm-event-readout` | **被主服务升级/替换**(薄复读不够收费) | +| `/pm-trade-preflight` | 配套保留 | +| 聪明钱 / Upset / Regime / Divergence | **非本框架主线**;红海或观察中,不按本架构优先优化 | +| Token DD | **投研线**(Codex skill → 另框架),不并进 PM Event Analyst | +| Delivery Audit / Content Verify | 其他产品线(Trust / Creator) | + +--- + +## 6. 新品类 / 新服务准入(以后持续用) + +新开一个 category 或独立 SKU 前,必须书面回答: + +1. **需求名**:买家会搜什么?平台上是否已有销量旁证? +2. **真实调用场景**:谁、何时、输入、输出、为什么愿付(不是「POST 回 JSON」)? +3. **相对 L0 增量**:没有品类规则是否明显更差?增量是否值得加价? +4. **对照样例**:同一活跃盘,L0-only vs L0+plugin,厚度差可见。 +5. **技术边界**:能否规则 + 公开 API?若需 LLM → escrow/异步/BYOK,不塞进同步薄 endpoint。 +6. **合规**:只读分析;无托管/下单/账户 mutation。 +7. **挂法**:先 plugin 还是直接独立 SKU?(默认先 plugin。) + +任一题答不清 → **不开发、不 listing**。 + +--- + +## 7. 本地优化 ↔ 线上 ASP 同步节奏 + +``` +优化本地 skill(Codex/Cursor) + → 更新本注册表 category / 字段 + → 对照样例(同盘 L0 vs 加深) + → 实现/升级 worker endpoint + → npm test + 同盘回归 + → (Leo)deploy + → (Leo)listing 文案 / create|update + → 冻结审核期不零碎改 +``` + +原则: + +- **Skill 可以持续优化**;链上 listing 按大包/明确授权再动。 +- 宣传只讲 **已达 skill 级厚度** 的服务;薄接口不配做黑客松门面。 +- 投研(Asset DD)与本框架 **并行**:Codex 优化 skill 期间 Cursor 不抢;两边各自对照样例后再谈上架。 + +--- + +## 8. 实现顺序(当前默认) + +| 步 | 内容 | 状态 | +|----|------|------| +| 0 | 架构框架本文 | **done** | +| 1 | Fed 对照样例(证明 L0 厚度) | **done**(contrast md) | +| 2 | 实现 L0:`event_matrix` + 诚实 tradability + 宏观锚适配器 | **done**(schema 0.2) | +| 3 | 路径:升级 `/pm-event-readout`(保留 path,schema 0.2) | **done** | +| 4 | Musk ladder plugin(形状样例) | **done** — **禁止当尖刀/宣传主角** | +| 5 | 独立品类 SKU | 仅付费/复购信号后 | +| 6 | **Football plugin(尖刀)** | **done**(FRA–MAR 350 markets · Worker 已上) | +| 6b | **Tennis plugin(同标准)** | **done**(2026-07-09 · Muchova–Gauff 15 markets · BO3/BO5 · domination check · 见 `pm-tennis-plugin-contrast.md`) | +| 7 | Weather plugin | 等 probation 解除 + 对照样例 | +| — | Asset DD ASP / Pro Pack | **Codex skill v2.2 stage_ready** · Cursor 已交产品站 mock(`_inventory/.../asset-dd-pro-pack-product-site/`);独立站 deploy / checkout 仍 Leo gate | + +--- + +## 9. 非目标(写死,防回潮) + +- 不为「占位」批量 create 聪明钱类同质 SKU。 +- 不把十几个薄接口当黑客松主叙事。 +- 不把四种体育/天气/Musk schema 当成「只能分析这四类」。 +- 不在同步 x402 里假装完整 LLM Standard/Full 报告(投研线另议)。 +- 审核中不零碎 update;大包需 Leo 授权。 + +--- + +## 10. Next gate + +Leo 确认本框架可作 PM 线长期 SSOT 后: + +1. Cursor 实现 L0(步 2–3)。 +2. Codex 继续 Asset DD skill;稳后开投研对照样例。 +3. 品类加深按 §6 排队,不并行铺四个独立服务。 + +**Writeback 提案(待 Leo/Codex)**:T0521 `next_action` 改为「PM Event Analyst L0 实现 + Asset DD 等 Codex skill」;本文件路径写入 task context。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-tonight-submit-checklist.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-tonight-submit-checklist.md new file mode 100644 index 00000000..abf5f44f --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-09-tonight-submit-checklist.md @@ -0,0 +1,74 @@ +# 今晚 Agent #3977 再提审核清单 · 2026-07-09 + +给 Leo 的本地 readiness 清单。默认 **不上链**;授权后按大包一次做完。 + +权威 runbook:`research/2026-07-09-okx-resubmit-big-pack.md` +命令打印稿:`bash research/2026-07-09-okx-big-pack-commands.sh`(默认 print-only) + +当前状态:**已提交 · Listing under review**(2026-07-09 晚大包完成;回执 `_inventory/2026-07-09-listing-bigpack/RECEIPT.md`) + +--- + +## 已就绪 + +- [x] 链上实况:`approvalStatus=6`(已拒,头像);4 服务 id **29496–29499** 仍在 +- [x] 定稿头像本地文件:`~/Projects/content/brand/leo-labs-avatar-1024.jpg`(1024×1024,直角、非白底) +- [x] **无字母候选**(推荐优先试):`~/Projects/content/brand/okx-avatar-1024-signal-only-candidate.jpg`(仅橙信号弧 + 深底,无 L) +- [x] 双语 Agent 简介 + 4 已有服务文案(catalog `OKX_LISTING_COPY` + listing md) +- [x] 6 个待 create 文案(`PENDING_OKX_LISTING_COPY`);PM Event Readout 已升为 Football-ready(同场矩阵 / fixture / 足球/网球品类深度;不下单) +- [x] `/polymarket-smart-money-radar` catalog `mode: 'live'`(对外无 beta) +- [x] 禁词:catalog + listing md 对 beta / test / -dev = **0 hit** +- [x] `onchainos agent validate-listing`(Agent 简介 + 4 update + 6 create)→ **`pass: true`** +- [x] 大包执行顺序与 print 脚本已写好 +- [x] 10 个 endpoint Worker 已部署;GET sample 可用(本机部分路径曾 403/需 UA,workers.dev `/health` OK) + +### 已有 4 服务(update 用) + +| id | Name | Fee | Path | +|----|------|-----|------| +| 29496 | World Cup Smart Money Radar | 0.1 | `/world-cup-smart-money-radar` | +| 29497 | Polymarket Smart Money Radar | 0.05 | `/polymarket-smart-money-radar` | +| 29498 | Agent Delivery Audit Gate | 0.2 | `/agent-delivery-acceptance-audit` | +| 29499 | Event Price Divergence Radar | 0.1 | `/event-price-divergence-radar` | + +### 待 create 6 个 + +| Name | Fee | Path | +|------|-----|------| +| Crypto Market Regime Radar | 0.1 | `/crypto-market-regime-radar` | +| World Cup Upset Alert | 0.1 | `/world-cup-upset-alert` | +| Token DD Verdict | 0.05 | `/token-dd-verdict` | +| PM Trade Preflight | 0.1 | `/pm-trade-preflight` | +| PM Event Readout | 0.1 | `/pm-event-readout` | +| Content Verify Claims | 0.1 | `/content-verify-claims` | + +### 大包执行顺序(授权后) + +按 `research/2026-07-09-okx-big-pack-commands.sh` 打印稿: + +1. 预检:头像文件 + 禁词扫描(catalog **+** listing md)+ 可选 10×402 +2. `onchainos agent upload` 定稿头像 → 记下 `PICTURE_URL` +3. `service-list --agent-id 3977` 刷新 id +4. **一次** `update`:picture + Agent 双语 + 4 服务双语(29496–29499) +5. 逐条 `validate-listing` → `create` ×6 +6. **一次** `activate` +7. 冻结;盯邮件 / watcher + +--- + +## 已执行(Leo「按你的建议来」) + +- [x] 头像 B 上传(signal-only) +- [x] 一次 update:picture + 双语 Agent + 4 update + 6 create +- [x] 一次 activate → **Listing under review**(AI 质检建议通过) +- [x] 服务现为 **30207–30216**(共 10) + +--- + +## 明确不做(今晚 / 明天再看) + +- **视频推文 / 黑客松 demo 视频** → **明天** +- 发推、内容展示、黑客松报名宣传 +- Batch 2 PM Category Analyst 上架 +- commit / push(除非 Leo 另开 gate) + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-10-t0521-hackathon-model-review.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-10-t0521-hackathon-model-review.md new file mode 100644 index 00000000..b087d026 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-10-t0521-hackathon-model-review.md @@ -0,0 +1,15 @@ +# Multi-model review + +共识: 修改 + +## grok (invoke) +结论: 修改 +风险: Quote 句「before you trade a match」偏交易建议口吻;8 linked/350 markets 须与 demo 画面严格一致否则 overclaim;listing 未过不可 main_publish +动作: Quote 改 trade→「size a match thesis」或「call a match」;删/口语化 EN「Tennis and research gates sit beside them」;主帖保留 ship_after_listing,listing 过后再发 +reviewer: Grok + +## codex (http_responses) +结论: 修改 +风险: “Leo Labs is on OKX.AI”在 listing 通过前会误导状态;“同场其他 350 个市场”与“8 linked events / 350 markets”口径不一致;“before you trade a match”和“match thesis”有交易建议暗示 +动作: 发布阈值设为 listing 通过;统一改为“8 linked events covering 350 markets”;将“trade a match / match thesis”改为“analyze an event / event view” +reviewer: Codex diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-12-pm-event-analyst-dual-surface-contract.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-12-pm-event-analyst-dual-surface-contract.md new file mode 100644 index 00000000..06d082ae --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-12-pm-event-analyst-dual-surface-contract.md @@ -0,0 +1,86 @@ +# PM Event Analyst · Dual-Surface Contract v0.1 + +日期:2026-07-12 +状态:**active_contract** +产品名:**PM Event Analyst** +关联:T0521(agent 渠道)· T0530(人面 Copilot)· `2026-07-09-pm-event-analyst-framework.md` · `copilot.read-model.v0.1` + +## 一句话 + +**同一个事件分析内核**:人面读 Research Unit,agent 面调结构化 readout;OKX / MCP / 其他店只是 agent 面 adapter。不是两套产品。 + +## Surfaces + +| Surface | 消费者 | 契约 / path | 计费 | +|---------|--------|-------------|------| +| **Human** | Prediction Copilot(T0530) | `copilot.read-model.v0.1` Research Unit | 产品内(现有 Copilot 路径) | +| **Agent** | 外部 agent / Leo 自用 Codex | `POST /pm-event-readout`(schema `0.2`,`service_id=pm_event_readout`) | x402 @ `api.leolabs.me` | +| **Channel** | OKX.AI ASP #3977 等 | 挂同一 agent endpoint | 平台发现 + x402 | + +人面 **不是** Weather Market Lab(T0531)或内容站(T0532)。共享数据组件 ≠ 合并产品。 + +## Origin + +``` +skill / rules (pm-event-readout + category plugins) + │ + ▼ +api.leolabs.me /pm-event-readout ← agent origin (live) + │ + ├── OKX.AI listing adapter (under review freeze) + ├── future MCP / ChatGPT Apps adapters + └── toCopilotResearchUnit() ← human-facing projection (this contract) + │ + ▼ + Copilot UI / bot cards (T0530) +``` + +当前实现真相:agent 核在 `src/pm-event-readout.mjs`;人面投影在 `src/pm-event-to-copilot-research-unit.mjs`。 +Copilot 现网仍走 `/api/v2/analyze` → SimpleAnalysis → read-model adapter;**本契约定义目标合流形状**,不授权本批改 Copilot runtime wiring。 + +## 字段映射(Agent → Human) + +| Agent (`pm_event_readout` 0.2) | Human (`copilot.read-model.v0.1`) | 规则 | +|--------------------------------|-----------------------------------|------| +| `event_slug` / `market` / Gamma ids | `market.platform=polymarket`, `marketId`, `slug`, `eventId`, `title` | platform 固定 polymarket until multi-venue | +| `generated_at` | `freshness.asOf` | ISO 原样 | +| `matrix_status` | `freshness.status` | `complete`→complete;`incomplete`→partial;缺矩阵/价→missing | +| `tradability_reasons` | `freshness.partialReasons` | 仅当 status=partial | +| `sources_read` 长度 / 矩阵有价 | `evidence.*` | hasPrices / hasLiquidity 从 matrix 行推导 | +| `hard_gate` | `compliance.analysisAllowed` | `no_orders_no_account_mutation` → allowed=true;其它硬拒 → false | +| `tradability` | `decision.eligibility` | **从不**映射为 `BET`(L0 分离事件可读性与下单) | +| `tradability` high/medium | `eligibility=OBSERVE` | confidence 粗映射:high=0.7 / medium=0.5 | +| `tradability` weak/low/其它 | `eligibility=AVOID` | confidence=0.3;`low` 与 `weak` 同级 | +| `base_case` + `market_implied_view` | `summary.en`(+ optional `summary.zh` if present) | 拼接,不发明新句 | +| 全量 agent JSON | `extensions["pm-event-readout.v0.2"]` | 完整保留;UI 可忽略 | + +### Decision 硬规则(双面共用) + +1. L0 Event Analyst **不输出 buy/sell/size**;`eligibility=BET` 只允许来自未来的 Preflight / Decision Card 下游,不来自本投影。 +2. `freshness.status=missing` → `decision` 必须 `null`。 +3. `compliance.analysisAllowed=false` → `decision` 必须 `null`。 +4. 禁止根级天气 / 内容站 / 下单字段(与 Copilot v0.1 forbidden keys 对齐)。 + +## Agent → Human 验收 + +- 纯函数:`toCopilotResearchUnit(agentPayload, { unitId })` +- 单测:`test/pm-event-dual-surface-test.mjs` +- `validate`-style:投影结果必须带 `schemaId=copilot.read-model.v0.1`,且无 forbidden keys。 + +## Human → Agent(目标方向,本批不接线) + +Copilot 选中市场后,应用同一 `slug|condition_id|market_url` 调 `/pm-event-readout`(或内部同函数),再投影回 Research Unit——消灭「UI 一套、OKX 一套」漂移。接线排在 T0530 Chrome E2E 恢复之后。 + +## 非目标(本批) + +- 不改 OKX listing / activate(审核冻结) +- 不 deploy Cloudflare Worker +- 不改 Copilot 生产 bundle / `/api/v2/analyze` +- 不把聪明钱 / Regime / Upset 升为主产品叙事 + +## Next gates + +1. Leo 或 Codex 接受本契约为 PM 线双面 SSOT。 +2. Mac trust/Chrome 恢复 → T0530 authenticated E2E。 +3. E2E 后:Copilot 读路径改调同一 origin(或共享 `assessPmEventReadoutLive`)。 +4. OKX listed 后:渠道仍挂 `/pm-event-readout`;对外品牌名统一 **PM Event Analyst**。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-advance-plan-wtp-first.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-advance-plan-wtp-first.md new file mode 100644 index 00000000..3b3cf6d0 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-advance-plan-wtp-first.md @@ -0,0 +1,108 @@ +# 推进计划 · 付费意愿优先 · 2026-07-24 + +**原则(Leo 拍板)**:核心是别人愿不愿付。黑客松与赚钱都服从这条。 +**决策卡 = 可演示叙事假设**,不是已验证商业主线。 +**商业主线 = 过审后有机付费落在哪条 path,就锁哪条。** + +--- + +## 现在卡在哪 + +| 阻塞 | 状态 | 谁做 | +|---|---|---| +| Listing 过审 | **under review / not listed** | 等平台;我们只保 endpoint 绿、文案不伤审 | +| 有机付费样本 | ≈无 | 过审后自然发生;**禁止自买** | +| 参赛物料 | demo 脚本/社交草稿已有;**未录未发** | Leo 录/发;我备脚本 | +| 主线锁定 | 未锁 | 用付费数据锁,不靠先验 | + +--- + +## 三阶段推进(按时间) + +### P0 · 本周到提交截止(~7/27 23:59 UTC)· 「能参赛 + 不自证方向」 + +目标:满足官方硬条件;演示用尖刀;**不把未验证方向写成商业真理**。 + +| # | 动作 | 完成定义 | Owner | +|---|---|---|---| +| 1 | 盯过审 | `approvalLabel` 变 listed / 可公开展示 | Agent 可查;变了立刻通知 | +| 2 | 录 ≤90s demo | 主画面:`pm-decision-card` **或**(若你更信信号)`finance-cockpit` / smart-money —— **选你认为买家更愿付的那条**;副线 publish/audit 可切 | Leo | +| 3 | 发 #OKXAI 帖 | 用已有草稿;强调「付费按次闸/信号」,不吹 GMV | Leo | +| 4 | 交官方表单 | ASP 信息 + X 帖链接 | Leo | +| 5 | 冻结扩 SKU | 不新做 C 档;pnl-audit **不写代码** | Agent | +| 6 | 文案去目录感(可选) | Agent 简介改成 2–3 个价值主张 +「按次付」;不列 26 个名字 | 你一说「改简介」我就改并候 `update` | + +**Demo 选哪条(按付费直觉,不按我之前的偏好)**: + +``` +若你觉得「下单前检查」有人付 → decision-card +若你觉得「行情/聪明钱会变」有人付 → finance-cockpit 或 sports-cockpit / smart-money +若你觉得「发文/交件验收」有人付 → publish-readiness / delivery-audit +``` + +选一条录透;另一条当 10 秒彩蛋即可。 + +### P1 · 过审后 7–14 天 · 「用钱投票」 + +| # | 动作 | 完成定义 | +|---|---|---| +| 1 | 只观测、少改产品 | 记录:每条 path 的有机付费次数(平台 sold / 自有日志若有) | +| 2 | 锁主线 | **付费最多的 1–2 条 path = 商业主线**;演示叙事可保留决策卡 | +| 3 | 加深主线 | 只加深有付费的;无付费的停止讲故事 | +| 4 | Kill | 有曝光仍持续 0 付费的 SKU → 降维护,不加功能自证 | +| 5 | pnl-audit | 仅当「验证钱包成绩单」方向出现付费信号或你下令 → 再开工 | + +### P2 · 主线有复购之后 · 「护城河」 + +- PolyData / OSS 加深主线信号质量 +- 同 API 多通道(降低单平台风险) +- 订阅/额度仅在 L1 复购成立后 + +--- + +## 每日/每周节奏 + +**提交前每日** + +1. `onchainos agent get --agent-ids 3977` → 是否过审 +2. endpoint `/health` 绿 +3. 物料:demo / 帖 / 表单缺哪块补哪块 + +**过审后每周一** + +| 指标 | 用途 | +|---|---| +| 有机付费调用(总) | 活着没有 | +| Top path 付费占比 | 锁主线 | +| 付费失败/5xx | 信誉红线 | +| A/B/C 叙事是否还在吹 0 付费产品 | 纪律 | + +--- + +## 我(Agent)下一步默认做什么 + +在你不改口的前提下,默认推进顺序: + +1. **可查**:继续可复查 listing;过审立刻写回执 +2. **可写**:若你回「改简介」→ 出一版尖刀简介(2–3 价值点,不绑死商业主线) +3. **可备**:按你选的 demo 主 path,把 curl/高亮字段收成一页录屏卡 +4. **不做**:新 SKU、自买、未授权 activate/付费、本周实现 pnl-audit + +--- + +## 无人付费杀线(已写入暂时结论 · 硬纪律) + +详见 `research/2026-07-24-temporary-conclusion-wtp-and-kill-line.md`: + +- 过审后 2 周 ≈0 付费 → 只改分发,不加深功能 +- 过审后 4 周仍无复购 → **维护模式**(撤注意力) +- 再 4–8 周仍无 → 期权到期 + +短期最后一搏物料:`research/2026-07-24-short-term-last-push.md` + +--- + +## 需要你拍板的一件事(只问这个) + +**Demo 主画面**默认已按「市场更有人付信号」选 **B(finance-cockpit)**;决策卡作彩蛋。 +若要改回 A/C,说一声即可。 \ No newline at end of file diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-agent-platform-roadmap-hackathon-to-money.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-agent-platform-roadmap-hackathon-to-money.md new file mode 100644 index 00000000..fd9e93d0 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-agent-platform-roadmap-hackathon-to-money.md @@ -0,0 +1,203 @@ +# Agent 服务路线图:黑客松奖项 × 长期赚钱 + +- 日期:2026-07-24 +- 身份:Leo Labs ASP #3977 · api.leolabs.me +- 问题:按 Genesis 细分类别奖项要求,同时服务「长期在 Agent 服务平台上赚钱」,怎么规划? +- 结论一句话:**黑客松是渠道加速器与叙事窗口;赚钱靠「别人愿意付的价值 + 边缘按需履约 + 多平台可迁移收款」。** +- **2026-07-24 修正**:商业主线不先验锁死「决策闸」;**以有机付费落点锁定**。决策卡只作可演示假设。推进细节见 `2026-07-24-advance-plan-wtp-first.md`。 + +--- + +## 0. 两个时钟,不要混成一个计划 + +| 时钟 | 截止/节奏 | 成功定义 | 资源占比(建议) | +|---|---|---|---:| +| **H · 黑客松** | ~2026-07-27 23:59 UTC | 过审可展示 + ≤90s 可复现 demo + 有机调用证据 + 对齐至少 2 个细分奖 | 70%(本周) | +| **M · 赚钱机器** | 上架后持续;4 周 kill line | 有机付费调用复购 + GMV 可讲 + 成本趋近 0 | 30% 本周 / **之后 100%** | + +纪律: + +- 本周一切「新 SKU」若不能同时服务 H 奖项叙事 **且** 加深 M 的 A 档钉子 → **不做**。 +- 黑客松结束 ≠ 产品结束;H 的产出必须可继承为 M 的资产(listing、收据、demo 片、reputation)。 + +--- + +## 1. 细分奖项 → 我们押什么 + +| 奖项 | 评委大概看什么 | 我们现有弹药 | 本周动作(只做这些) | 长期继承 | +|---|---|---|---|---| +| **金融副驾驶** | 能否当 Agent 的金融决策辅佐 | finance-cockpit · regime · divergence · smart money · upset · pm-trade-preflight · **pm-decision-card** · token-dd | **主叙事**:一张 decision-card / cockpit 演示「买前闸」 | 成为旗舰 SKU 组;加深信号质量 | +| **营收火箭** | 真实收款 / GMV / 增长 | x402 已开;历史有机单;#3977 货架 | **证据包**:有机调用截图/链上结算;**禁止自刷**;过审后尽量有新有机单 | 平台 GMV 份额;跨平台复用同 endpoint | +| **软件实用工具** | 非金融也好用的 Agent 工具 | budget-preflight · delivery-audit · publish-readiness · verify · slop | demo 第二条线:发文/交件闸(90s 内可切) | Agent DevTools 线;平台无关刚需 | +| **社交热议** | 话题、传播、#Okxai | build-in-public · 多场景货架故事 | 一条清晰帖:OPC + 按次付 + 决策卡;带 #Okxai | 内容资产;获客漏斗入口 | + +**不主攻**(除非官方新增硬要求):纯娱乐/纯社交产品、重 UI dApp、需要 7×24 人工值守的服务。 + +**奖项组合策略(诚实)**: + +``` +主申:金融副驾驶(产品最贴) +兼报:营收火箭(有收款就冲;没有就别硬吹) +加分:软件实用工具(S1 工具链一条 demo) +保底:社交热议(内容成本低,10×$1K 池) +``` + +--- + +## 2. 长期怎么在 Agent 服务平台上赚钱(商业模型) + +### 2.1 平台只提供三件事 + +1. **发现**(货架 / 搜索 / Agent 编排入口) +2. **收款**(x402 / A2A / 托管结算) +3. **信誉**(链上成交与评分) + +**履约与差异化必须在你自己的边上。** +我们已选的模型:`Cloudflare Worker 边缘按需 + 规则/公开 API + x402` —— **卖家电脑不必在线、不必 LLM、边际成本≈0**。这是能规模化赚钱的前提。 + +### 2.2 钱从哪来(优先级) + +| 层级 | 收入形态 | 谁付 | 我们该卖什么 | +|---|---|---|---| +| **L1 按次钉子(现在)** | pay-per-call | 其他 Agent / 自动化脚本 | A 档工作流闸(budget / audit / publish / trade-preflight / decision-card / cockpit) | +| **L2 订阅/额度(平台成熟后)** | monthly / credit pack | 高频 Agent 运营方 | 同一组钉子的「月度额度」——**只在平台支持且 L1 已证明复购后做** | +| **L3 数据溢价** | 更高单价 SKU | 研究型 Agent | PolyData 深度信号、校准分、cohort —— **护城河** | +| **L4 多平台套利** | 同核心多上架 | 各平台买家 | 核心逻辑在自己域名;OKX / 其他 Agent 市集只是通道 | + +**不靠**:SKU 数量、自买刷单、一次性黑客松奖金(奖金是期权,不是商业模式)。 + +### 2.3 单位经济(心里要有数) + +``` +单次收入 ≈ listing 标价(USDT) +单次成本 ≈ CF 请求费 + 上游 API(多数公开免费)+ 0 LLM +毛利 ≈ 极高 +瓶颈 ≠ 算力,= 发现与复购 +``` + +因此路线图的核心 KPI 不是「上了多少 SKU」,而是: + +1. **有机付费调用 / 周** +2. **复购率(同一 buyer 或同一 agent 回访)** +3. **A 档收入占比**(C 档发现 SKU 占比应下降) +4. **履约失败率**(付费 5xx 直接伤信誉) + +--- + +## 3. 产品路线图(三阶段) + +### Phase H(现在 → 提交截止)· 「可演示的赚钱原型」 + +**目标**:评委 90 秒内看懂「Agent 付费 → 得决策/闸门 → 可复购」。 + +| 优先级 | 事项 | 完成定义 | +|---|---|---| +| P0 | Listing 过审 / 可演示 | #3977 可公开展示或有审核进度可述 | +| P0 | Demo 脚本固定 | 金融副驾驶:`pm-decision-card` 或 `finance-cockpit`;工具线:`publish-readiness` 或 `delivery-audit` | +| P0 | 收款证据 | 有机 x402 成功单(历史亦可)+ 架构说明「边缘按需」 | +| P1 | 参赛物料 | ≤90s 片 / 帖 / 表单;#Okxai;不写禁词 | +| P1 | A 档文案统一 | listing 强调 `value_loop` / agent_loop,不把 C 档包装成旗舰 | +| P2 | 停止扩 C 档 SKU | 天气/政治/宏观/比赛卡只作发现,不再平行拆 | + +**本阶段明确不做**:LLM Agent 常驻、任务厅冷触达主线、Encode Final(除非你另开指令)、自买刷 GMV。 + +### Phase M1(提交后 2–4 周)· 「复购与杀线」 + +**目标**:验证是不是真有人愿意反复付钱。 + +| 动作 | 说明 | +|---|---| +| 只加深 A 档 | decision-card 质量、audit 规则、cockpit 信号、regime/divergence 诚实降级 | +| 观测 KPI | 有机调用/周、A 档占比、失败率 | +| Kill line(继承 OPC 战略) | 约 4 周:若平台无增长且我们有机调用仍极低 → **降维护模式**,停扩货架,保留收款与信誉 | +| 内容 | 里程碑驱动 build-in-public,不日更 | + +### Phase M2(平台或调用起来之后)· 「护城河与通道复制」 + +| 动作 | 说明 | +|---|---| +| PolyData / OSS 加深 | 把独特数据变成更高价 L3 SKU,而不是新薄包装 | +| 订阅/额度 | 仅当平台支持且 L1 复购成立 | +| 多平台上架 | 同一 `api.leolabs.me` 接到其他 Agent 市集 / x402 生态(通道多元化,降低单平台风险) | +| 可选 LLM 产品 | **单独定价含成本**;永不让免费/低价 SKU 吞 LLM 成本 | + +--- + +## 4. 架构路线图(支撑赚钱,而不是炫技) + +``` +现在(已对齐赚钱): + Buyer Agent → OKX 货架 → x402 → api.leolabs.me (CF) → 规则 + 公开 API → JSON + +近端加深: + 同上 + 更强 A 档逻辑 + 可选快照缓存(降上游依赖)+ 统一 value_loop 字段 + +中期(有收入再做): + PolyData 预计算任务 → R2/KV 快照 → Worker 只读 + (仍不必 7×24 卖家在线) + +慎做: + 强制常驻 Agent 进程才能履约 + 每个 SKU 绑一个 LLM + 把履约绑死在单一平台内置 runtime(失去多通道) +``` + +**原则**:平台可换,**域名上的付费闸与数据逻辑不可换。** + +--- + +## 5. 组织/节奏(一个人 + Agent) + +| 频率 | 做什么 | +|---|---| +| 黑客松本周每日 | 过审状态 / demo 一次跑通 / 有机调用是否增加 / 物料是否齐 | +| 每周一(之后) | 调用量 · GMV · A/B/C 占比 · 是否触发 kill line | +| 每两周 | 重读本路线图 + `opc-longterm-strategy`:平台是否还值得加码 | + +Hard gates 不变:不自动公开支付、不自买刷量、密钥不进仓库。 + +--- + +## 6. 一张图:奖项要求如何喂给长期赚钱 + +```mermaid +flowchart TD + H[黑客松窗口] --> D[Demo 与叙事] + H --> L[Listing 与信誉] + H --> E[收款证据] + D --> A[A档钉子产品] + L --> Disc[平台发现] + E --> Trust[链上信任] + A --> Pay[按次复购] + Disc --> Pay + Trust --> Pay + Pay --> M1[M1 验证复购] + M1 -->|过杀线| M2[护城河 + 多通道] + M1 -->|未过杀线| Maint[维护模式低成本期权] + M2 --> ARR[长期平台收入] +``` + +--- + +## 7. 立即执行清单(按优先级) + +1. **冻结 C 档扩 SKU**;资源只进 A 档加深与 demo。 +2. **定死两条 demo 路径**:金融副驾驶(decision-card/cockpit)+ 软件工具(publish/audit)。 +3. **准备营收火箭证据包**(有机单 + x402 + 边缘履约说明)。 +4. **社交帖一条讲透**:OPC · 按次付 · Agent 买前闸 · #Okxai。 +5. **提交后切换 M1**:看复购,不看 SKU 数;4 周杀线严格执行。 +6. **Encode / 任务厅**:非主线;仅监控或你另下指令。 + +--- + +## 8. 与旧战略的关系 + +- 继承 `2026-07-05-opc-longterm-strategy-v1.md` 的 T1–T3 与 kill line。 +- 修正旧组合叙事:不再以「World Cup 单点」为中心,改为 **PM/Crypto/Sports 通用决策闸 + Agent DevTools**。 +- 修正「多服务占位」:占位有用,但 **赚钱靠 A 档密度,不靠 C 档宽度**。 + +--- + +## 9. 一句话路线图 + +> **本周用金融副驾驶 + 工具闸 + 真实收款证据打黑客松;之后用同一套边缘按需 A 档钉子验证复购;过杀线再加深数据护城河并复制到多平台——平台是收银台与橱窗,生意在你自己的 API 上。** diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-competitive-asp-and-hackathon-posts.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-competitive-asp-and-hackathon-posts.md new file mode 100644 index 00000000..d222f2d6 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-competitive-asp-and-hackathon-posts.md @@ -0,0 +1,151 @@ +# 竞品与参赛帖整体分析 · Leo Labs #3977 · 2026-07-24 + +方法: +- 公开 X/GitHub 可检索的 Genesis 参赛物(样本,非全量;X 帖本身常 403) +- `onchainos agent search` 抽样刷新货架(133 个去重 ASP) +- 对照 07-06/07 目录扫描 + 今日 live + +**一句话**:市场上**明显更好、且同赛道碾压你**的产品不多;真正碾压销量的是**另一类生意**(廉价数据 API / 病毒生活工具 / 品牌物料)。同赛道里你缺的不是「再多一个场景卡」,而是**过审 + 一条尖刀叙事 + 有机复购**。 + +--- + +## 1. Listing 状态(刚才查的) + +| 项 | 值 | +|---|---| +| Agent | Leo Labs **#3977** | +| Approval | **Listing under review**(`approvalDisplayStatus=2`) | +| Status | **not listed**(公开展示仍受限) | +| Online | 1(endpoint 侧在线) | +| Services | **26** 条已 create | +| 历史销量信号 | agentInfo `salesCount` 曾见 **1**(极低;勿当营收火箭主证据) | + +→ 黑客松硬门槛仍是:**过审上架**。产品厚度够不够评,先过这关。 + +截止日期口径(官网 Build X):提交可延至 **2026-07-27 23:59 UTC**(以官方页为准)。 + +--- + +## 2. X 上别人的参赛帖在秀什么? + +公开可核验的「参赛型」样本(GitHub README 自报 #OKXAI / demo): + +| 项目 | 卖点 | 形态 | 对你意味着什么 | +|---|---|---|---| +| **SentriAgent** (#5103) | “Agent 碰钱前的 trust/risk 层” | token/wallet risk MCP + x402;有 90s X demo 链接 | **叙事极干净**:一句话 + 4 tools。与 CertiK/token-DD 红海重叠;你的 `token-dd`/`audit` 同族,但他们**演示更尖** | +| **Aura Card** | 付 0.5U 生成 vibe 卡片 | 社交玩具 + 完美 x402 演示 | **社交热议向**;产品深度弱,但「付费→立刻有结果」演示极爽。评委友好,不是长期护城河 | +| **ASP LaunchPad** | 帮别人把脚本变成 ASP | meta 工具(脚手架/文案/readiness) | 软件工具赛道叙事;与你的 `publish-readiness`/`budget` 同属「Agent 基建」,但他们卖的是**上架工厂** | +| **EVIDIQ** | Agent 身份/信誉 trust score | PROCEED / DO-NOT-PROCEED | 又一个 trust gate;货架 sold 仍低(样本里 ~4) | + +**观察(重要)**: + +1. 多数参赛帖赢在 **一条尖刀 + 90s 付费闭环演示**,不是 26 个 SKU 目录。 +2. 很多「很好看」的应用 = **协议演示正确**(402→支付→200),不等于数据护城河。 +3. 你现在的公开描述仍偏「货架目录」(天气/政治/比赛卡…)——对发现有用,对**评委/买家第一印象偏稀释**。 +4. 尚未看到谁公开甩出「明显完整碾压 Leo 的 PM 决策闸全家桶」;更多是 **单点 tip / 聪明钱 / 安全分 / vibe**。 + +--- + +## 3. 现有 ASP 市场:谁真的在卖? + +### 3.1 销量头部(今日抽样 Top) + +| sold≈ | ASP | 本质 | +|---:|---|---| +| 10k+ | PixelBrief | 品牌/视觉物料(艺术) | +| 1.8k | ScoutGate | ASP 匹配/发现 meta | +| 1.6k+ | Onchain Data Explorer / CoinAnk / CoinWM | **原始/聚合数据 API**(极低价走量) | +| 900+ | Quiver / AgentFund | 期权情报 / 市场扫描 | +| 500+ | 这个能吃吗? / Argus | 生活病毒 / 合约审计 | +| 300+ | Barker Yield | 收益雷达 | +| 179 | **AlphaCopy** | **Polymarket 聪明钱**(你的直接对标红海) | +| 149 | OnChain Arb Scout | 套利扫描 | +| 105 | CertiK | 安全 API(官方级背书) | + +### 3.2 和你同族的「闸门 / 副驾驶」 + +| ASP | 点什么 | sold≈ | 判读 | +|---|---|---:|---| +| AlphaCopy | PM 聪明钱 | 179 | **同赛道 GMV 标杆**;你不要用 smart-money 单品硬刚 | +| Predict-Raven 等 | PM 机会推荐 | ~23 | 喊单/推荐型;合规与长期信任弱于「闸门」 | +| PA Decision Lab | 单市场深度读 | 2 | 决策层存在但未起量 | +| QTrade Guard / PreFlight / SentriAgent | 交易前检查 | 0–4 | **赛道被验证为「像样」但还没人跑出来** → 你的 decision-card 仍有窗口 | +| Keryx Finance Copilot | 名是副驾驶,顶服务却是 price feed | 37 | 名字好 ≠ 决策层;别被 branding 吓到 | +| latch402 | x402 readiness | 26 | 协议工具,非金融 alpha | + +**市场结构诚实结论**: + +``` +卖得动的大头 = 数据水龙头 + 病毒生活/创意 +金融里卖得动 = 聪明钱/套利/收益扫描(信号) +「决策闸 / preflight」= 正确方向,但全场都还没做出第二个 AlphaCopy +``` + +你的组合(decision-card / cockpit / budget / audit / publish)在**产品逻辑上比多数参赛玩具更像可复购 ASP**;在**市场结果上仍远落后于数据水龙头与 AlphaCopy**。 + +--- + +## 4. 有没有「明显比我更好」的? + +分三层答: + +### A. 整体市场(跨品类)——有,而且很多 + +CoinAnk、PixelBrief、「能吃吗」、官方 Onchain Data —— **销量与分发碾压**。 +这不是「你产品差」,是**品类与获客结构不同**。别用他们的 sold 数衡量金融闸门成败。 + +### B. 同赛道金融/PM ——部分更好(在单一维度) + +| 维度 | 谁更好 | 你怎么应对 | +|---|---|---| +| 聪明钱销量/心智 | **AlphaCopy** | 不主打雷达;主打 **decision-card / cockpit / pnl-audit** | +| 安全背书 | **CertiK** / Argus | token-dd 只做轻闸,不碰审计品牌战 | +| 原始行情覆盖 | CoinAnk / Quiver | 继续「判断层」,不做第二 CoinAnk | +| 演示叙事清晰度 | SentriAgent / Aura | **砍目录感,一条尖刀 90s** | +| PM 事件矩阵深度 | 未见公开同等厚度 | 你的 event-readout/plugins 仍是差异点,但别当主标题 | + +### C. 「Agent 工作流钉子」全家桶 —— 未见明显更好 + +把 **budget → decision-card → delivery-audit → publish-readiness** 串成 Agent 可编排回路,公开货架上仍稀缺。 +这是你该在帖子里讲的,而不是 26 个 endpoint 名。 + +--- + +## 5. 对黑客松奖项的启示 + +| 奖项 | 市场现实 | 你的打法 | +|---|---|---| +| 金融副驾驶 | 名字滥;真决策层未起量 | **Decision Card 一条线**讲透;cockpit 作配菜 | +| 营收火箭 | 头部是数据/病毒;你 sales≈1 | 有有机单就展示架构+收据;**别硬刚 CoinAnk 数字** | +| 软件实用工具 | ScoutGate/API2ASP/latch402 很卷 | audit + publish + budget 够用;演示「Agent 发货前闸」 | +| 社交热议 | Aura 这类玩具更易传播 | 靠 OPC 故事 + 90s 爽感,不靠 SKU 数 | + +--- + +## 6. 战略含义(接到路线图) + +1. **不要因为「别人帖子好看」就再扩 C 档 SKU。** +2. **要输也输在:过审慢、叙事稀释、有机复购未起** —— 这些是 Phase H 该修的。 +3. **真正值得跟的竞品动作**: + - AlphaCopy:订阅/高频信号分发(你可用 A 档复购替代,不必抄聪明钱) + - Sentri/Aura:一句话 + 付费瞬间出结果 +4. **M1 差异化加深**:`pm-pnl-audit`(含费 PnL 审计)—— AlphaCopy 卖「跟谁」,你卖「这成绩单是不是真的」。 + +--- + +## 7. 置信度与缺口 + +- X 全量 #OKXAI 时间线未能完整抓取(平台 403)→ 参赛帖结论是**样本级**,不是普查。 +- soldCount 是平台字段,**≠ 精确 GMV**,且可能含试单/活动。 +- 07-07 全目录 358 ASP;今日是关键词抽样 133 —— 头部趋势一致即可用。 + +--- + +## 8. 给你的直接回答 + +> 有没有明显比我更好的应用? + +- **跨市场**:有(数据 API / 病毒工具),但不在同一评价坐标系。 +- **同赛道聪明钱**:AlphaCopy 明显更会卖。 +- **同赛道「Agent 决策/交付闸」**:**没有看到明显整体更好的**;多数参赛物叙事更尖、产品更薄。 +- **你最大的相对风险**:货架看起来杂、listing 还在审、销量未起 —— 不是「技术被全面吊打」。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-expand-shortlist-honest.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-expand-shortlist-honest.md new file mode 100644 index 00000000..c4b162f5 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-expand-shortlist-honest.md @@ -0,0 +1,20 @@ +# 还能扩什么?· 诚实短名单 · 2026-07-24 + +问:按现有工作流 / 能提供的服务,还有没有值得扩的新 SKU? + +## 结论 + +**按「A 档可复购钉子」标准,几乎见底。** +不是 0,但是 **黑客松本周不该再开新 SKU 战线**。 + +| 候选 | 来源 | 是否本周做 | 原因 | +|---|---|---|---| +| `/pm-pnl-audit` | polymarket-toolkit 含费 PnL | **延后 M1** | 真 A 档(审计钱包成绩单),但要移植/边缘化,挤占 demo 时间 | +| `/pm-v2-readiness` | toolkit V2/CTF FAQ | 延后 | 窄、真痛点,但买家池小 | +| `/pm-daily-universe-gate` | growth-engine | 不做除非重包装 | 易退化成又一个 scanner,与 decision-card/cockpit 重叠 | +| 更多场景卡 / humanizer / video / x-engine | 各 sibling | **不做** | C 档或要 LLM/cookie,破坏零边际成本模型 | + +**已覆盖的工作流**(再拆 SKU = 薄包装): +budget · delivery-audit · publish/verify/slop · trade-preflight · decision-card · finance/sports cockpit · smart-money/upset · token-dd · regime/divergence · profile/brier · event-readout + 场景发现卡 + +→ **按路线图推进 Phase H:demo + 证据 + A 档加深,不扩货架。** diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-okxai-social-draft.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-okxai-social-draft.md new file mode 100644 index 00000000..beb477e3 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-okxai-social-draft.md @@ -0,0 +1,31 @@ +#Okxai · Leo Labs social draft · 2026-07-24 + +## EN(主发) + +I'm one person building an agent company on OKX.AI. + +Leo Labs ships **paid gates** other agents can call before they trade or publish: + +• PM Decision Card — skip / watch / manual-review (not a buy tip) +• Publish & delivery audit — proof before an agent ships +• Finance / sports cockpits — re-run when the market moves + +Pay per call (x402). Fulfillment is edge-on-demand — no always-on laptop, no LLM key required for these gates. + +OPC in public. Agents do the work. + +#Okxai #OKXAI Genesis + +## ZH(可选附) + +一个人在 OKX.AI 上做 Agent 公司。 +Leo Labs 卖的是**按次付费闸门**:下单前决策卡、发文/交件验收、行情 cockpit。 +边缘按需履约,卖家电脑不必在线。 + +#Okxai + +## 红线 + +- 无有机收入截图 → 不写 GMV / 销量数字 +- 不暗示官方背书 +- 不把 C 档场景卡写成旗舰 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-phase-h-demo-and-submit-pack.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-phase-h-demo-and-submit-pack.md new file mode 100644 index 00000000..55bfc7c9 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-phase-h-demo-and-submit-pack.md @@ -0,0 +1,71 @@ +# Phase H · Demo + 提交证据包 · 2026-07-24 + +对齐路线图:金融副驾驶主线 + 软件工具副线 + 营收/社交证据。 +**尖刀已从 Football Event Analyst 换成 PM Decision Card**(买前闸,可复购 A 档)。 + +## 0. 扩货架裁决(已定) + +几乎见底 → **本周不扩新 SKU**。延后候选见 `research/2026-07-24-expand-shortlist-honest.md`。 + +## 1. 两条 ≤90s Demo(固定) + +### A · 金融副驾驶(主) + +旁白要点:Agent 下单前付一笔 → 拿 skip/watch/eligible + paid_checks → eligible≠买点。 + +```bash +# 生产(可能 402;录屏用付费或新 IP 试用) +curl -sS -X POST https://api.leolabs.me/pm-decision-card \ + -H 'content-type: application/json' \ + -d '{"slug":"REPLACE_LIVE_SLUG","side":"yes","size_usd":25}' + +# 高亮字段(录屏圈出) +# action · confidence · buyer_summary_en · paid_checks · value_loop.stale_at · agent_loop +``` + +备选同赛道:`POST /finance-cockpit`(宏观+体制一屏)。 + +### B · 软件实用工具(副 · 可切) + +```bash +curl -sS -X POST https://api.leolabs.me/publish-readiness \ + -H 'content-type: application/json' \ + -d '{"title":"Leo Labs on OKX.AI","body":"One person shipping paid agent gates.","channel":"x","claims":["x402 pay-per-call","edge fulfillment"]}' + +# 或 +curl -sS -X POST https://api.leolabs.me/agent-delivery-acceptance-audit \ + -H 'content-type: application/json' \ + -d '{"task":"Ship health endpoint","delivery_summary":"Added GET /health; tests green.","artifacts":["worker/index.mjs"],"validation":["npm test"]}' +``` + +### 时码稿(85s) + +| 时码 | 画面 | 旁白 | +|---|---|---| +| 0–8s | okx.ai → Leo Labs #3977 | I'm one person. This is Leo Labs on OKX.AI. | +| 8–20s | 标签:Decision Card · Publish Gate | Agents need gates before they pay — and before they post. | +| 20–55s | 终端 A decision-card → action + paid_checks | Finance copilot: mechanical trade gate. Skip, watch, or manual-review only. | +| 55–72s | 终端 B publish-readiness 或 audit | Utility: publish/delivery proof before the agent ships. | +| 72–85s | 收尾 · #Okxai | Pay per call. Edge on demand. No always-on laptop. | + +## 2. 营收火箭证据(有则带,无则勿吹) + +- [ ] 有机 x402 成功调用截图 / 结算记录(**禁止自买**) +- [ ] 架构一句:Cloudflare Worker 边缘按需;无卖家在线、无 LLM key +- [ ] listing 可展示或审核中进度可述 + +## 3. 社交热议草稿(#Okxai) + +见下文「社交帖」;发前确认 listing 状态与是否可放 agent 链接(遵守禁 URL 规则若在平台内)。 + +## 4. 提交动作清单 + +- [ ] Demo 片 ≤90s 或 asciinema +- [ ] #Okxai 帖 +- [ ] 官方表单(若仍开放) +- [ ] 奖项勾选:金融副驾驶 + 软件实用工具 +(有证据才勾)营收火箭 + 社交热议 +- [ ] 冻结新 SKU;只修 A 档 / 文案 / 过审 + +## 5. 本轮已做工程加深 + +`pm-decision-card` → schema 0.2:`paid_checks` · `buyer_summary_en` · `value_loop.stale_at`(方便 demo 圈重点)。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-pm-pnl-audit-m1-design.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-pm-pnl-audit-m1-design.md new file mode 100644 index 00000000..6c59e760 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-pm-pnl-audit-m1-design.md @@ -0,0 +1,127 @@ +# M1 设计稿 · `/pm-pnl-audit`(先不写代码) + +- 日期:2026-07-24 +- 状态:design only · **黑客松本周不实现**;过审/demo 后进入 M1 +- 来源:`polymarket-toolkit`(`pm pnl-check` / fee-inclusive PnL / activity replay) +- 战略位:相对 AlphaCopy「跟聪明钱」——我们卖「**这钱包成绩单经不经得起含费审计**」 + +--- + +## 1. 为什么是 A 档 + +| 问题 | 谁会反复付 | +|---|---| +| 跟单 / 招聘信号源前 | Agent 要验证对方声称的 PnL | +| 研究报告引用钱包表现前 | 需要 fee/rebate/redeem 后的现金口径 | +| 自己的策略日报 | 每日/每周对账 | + +`/pm-profile`、`/pm-brier` 已覆盖轻量画像与校准;**本 SKU 是重审计闸**,不要做成又一个 profile。 + +--- + +## 2. API 草图 + +``` +POST /pm-pnl-audit +``` + +### Input + +```json +{ + "address": "0x...", // 或 username → 解析 proxy + "window": "30d" | "90d" | "all", + "mode": "quick" | "full", // quick=LB+hints; full=activity cashflow replay(更贵) + "include_positions": true +} +``` + +### Output(目标形状) + +```json +{ + "schema_version": "0.1", + "service_id": "pm_pnl_audit", + "action": "trust_for_copy" | "verify_manually" | "distrust_claims", + "layers": { + "leaderboard_profit": {"value": null, "source": "lb-api", "status": "ok|degraded"}, + "positions_cash_pnl": {"value": null, "source": "data-api/positions"}, + "cashflow_replay": {"value": null, "status": "ok|pagination_incomplete|skipped_quick_mode"} + }, + "divergence": { + "lb_vs_replay_usd": null, + "verdict": "aligned|lb_optimistic|replay_higher|unknown" + }, + "paid_checks": [], + "value_loop": { + "why_pay_again": "Wallet keeps trading; claims and windows go stale.", + "stale_after_minutes": 1440, + "paid_value_tier": "A_repeat_audit_loop", + "fulfillment": "edge_on_demand_no_llm" + }, + "caveats": [ + "Not investment advice.", + "Pagination incomplete ⇒ do not treat replay as ground truth." + ], + "buyer_summary_en": "...", + "buyer_summary_zh": "..." +} +``` + +### Pricing(建议) + +| mode | fee | 理由 | +|---|---:|---| +| quick | 0.05–0.1 | 对齐 profile | +| full | 0.2–0.5 | activity 翻页成本与价值 | + +--- + +## 3. 实现约束 + +1. **只读**:无下单、无签名、无私有 cookie。 +2. **边缘按需**:优先 JS 移植关键层;full replay 若必须 Python,则: + - 方案 A:Worker 调自有轻量 replay(推荐长期) + - 方案 B:预计算快照(有收入后再做) + - **禁止**依赖 Leo 笔记本常驻进程履约 +3. 诚实降级:`pagination_incomplete` / upstream fail → 200 + degraded,不 5xx。 +4. Listing 文案禁:保证收益、跟单必赚、名人钱包。 + +--- + +## 4. 与现有 SKU 关系 + +``` +pm-profile → 轻画像 +pm-brier → 校准 +pm-pnl-audit → 含费现金审计(本设计) +smart-money → 主题扫描(红海) +decision-card → 单笔下单前闸 +``` + +Agent 编排示例:`smart-money 候选 → pnl-audit 验证 → decision-card 下单前闸`。 + +--- + +## 5. 工作量与杀线 + +| 阶段 | 估计 | 完成定义 | +|---|---|---| +| M1a quick | 0.5–1 天 | LB + positions + divergence hints + listing | +| M1b full | 1–2 天 | activity replay 可分页 + incomplete 标志 | +| Kill | 上线 2 周 | 若 0 有机调用且 decision-card 也冷 → 降维护,不继续加深 full | + +--- + +## 6. 非目标(明确不做) + +- 自动跟单 / 复制交易 +- 「谁是最聪明钱包」排行榜产品化(红海) +- 需要 LLM 写研报的包装 + +--- + +## 7. 何时开工 + +**触发**:#3977 过审可公开展示 **或** Leo 明确说「做 pnl-audit」。 +**本周**:只保留本设计稿;工程时间给 demo / 过审 / 社交帖。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-short-term-last-push.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-short-term-last-push.md new file mode 100644 index 00000000..82748892 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-short-term-last-push.md @@ -0,0 +1,107 @@ +# 短期最后一搏 · 物料包 · 2026-07-24 + +目标:把**现在还能做的**做完,让你只剩「录 / 发 / 交表」;用付费标准约束,不自证方向。 + +## 0. 此刻实况 + +| 项 | 状态 | +|---|---| +| #3977 | **Listing under review** / not listed | +| api.leolabs.me `/health` | 200 | +| decision-card / finance-cockpit / publish-readiness GET | 200 | +| 审核中 update | **不做**(纪律:不零碎搅审) | +| 新 SKU / pnl-audit 代码 | **不做** | + +过审后立刻:用下方「尖刀简介」`update` + 必要时 `activate`(另候你一句「改简介/上」)。 + +--- + +## 1. 尖刀简介(过审后 update 用 · 已定稿) + +**不要**再列 26 个场景名。强调可付费价值 + 非喊单。 + +``` +Pay-per-call data gates for agents: live PM/crypto signals that go stale, pre-trade decision cards, and delivery/publish checks. JSON in, structured verdict out — not a chatbot, not trade tips. +给 Agent 的按次付费数据闸:会过期的预测市场/加密信号、下单前决策卡、交付与发布检查。JSON 进、结构化结论出;非聊天、非喊单。 +``` + +字符短、双语、无 URL。 +落盘:`research/2026-07-24-tip-knife-agent-description.txt` + +--- + +## 2. 录屏单页(≤90s · 默认主画面 B 偏「已有人付」逻辑) + +市场已证明更有人付的是**信号**;闸门未证明。 +**默认主 demo = Finance Cockpit**(信号合成,金融副驾驶叙事仍在);决策卡作 15s 彩蛋。 +你若更信闸门,把 A/B 对调即可。 + +### 时码 + +| 时码 | 画面 | 旁白 | +|---|---|---| +| 0–8s | okx.ai → Leo Labs #3977 | One person. Leo Labs on OKX.AI. | +| 8–18s | 标签:live signals · pay per call | Agents pay for data that goes stale — not chat. | +| 18–55s | 终端 `POST /finance-cockpit` → regime + divergence 高亮 | Finance copilot: regime score + event-price divergence in one call. | +| 55–72s | 闪一下 `pm-decision-card` 的 action / paid_checks | Optional gate: skip / watch / manual-review before an order. | +| 72–85s | 收尾 #OKXAI | Edge on demand. No always-on laptop. | + +### 录屏命令 + +```bash +# 主 · Finance Cockpit(生产可能 402;付费或新 IP 试用) +curl -sS -X POST https://api.leolabs.me/finance-cockpit \ + -H 'content-type: application/json' \ + -d '{"focus":"bitcoin","limit":5}' + +# 彩蛋 · Decision Card +curl -sS -X POST https://api.leolabs.me/pm-decision-card \ + -H 'content-type: application/json' \ + -d '{"slug":"REPLACE_LIVE_SLUG","side":"yes","size_usd":25}' + +# 软件工具赛道备用(可替换彩蛋) +curl -sS -X POST https://api.leolabs.me/publish-readiness \ + -H 'content-type: application/json' \ + -d '{"title":"Leo Labs","body":"Pay-per-call agent gates on OKX.AI.","channel":"x"}' +``` + +高亮:`regime` / `divergence` / `action` / `paid_checks` / `value_loop.stale_at` +红线:无有机结算截图 → 不说 GMV;不暗示官方背书。 + +--- + +## 3. #OKXAI 帖(可直接发) + +I'm one person building Leo Labs on OKX.AI (#3977). + +Agents pay per call for **data that goes stale**: +• Finance cockpit — regime + event/price divergence +• Decision card — skip / watch / manual-review (not a buy tip) +• Publish & delivery checks when agents ship + +Edge on demand. JSON in, verdict out. + +#OKXAI #Okxai + +(中文附句可选,见 `research/2026-07-24-okxai-social-draft.md`) + +--- + +## 4. 你只剩这 4 步 + +1. [ ] 按 §2 录 ≤90s(或 asciinema + 口述) +2. [ ] 发 X 帖(§3),带 demo +3. [ ] 交官方表单(ASP + X 链接);奖项勾:金融副驾驶 + 软件工具 + 社交;**营收火箭仅当有有机单证据** +4. [ ] 过审邮件到了 → 回我「改简介」或「上」→ 我推尖刀 description + activate + +--- + +## 5. 我这边已做完的 + +- 暂时结论 + 杀线已记录 +- 尖刀简介 + 本物料包 +- endpoint 健康复查 OK +- **未**在审核中 update(避免搅审) +- **未**扩 SKU + +再努力一把 = **物料闭环 + 你完成录发交**;商业是否成立留给有机付费裁判。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-temporary-conclusion-wtp-and-kill-line.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-temporary-conclusion-wtp-and-kill-line.md new file mode 100644 index 00000000..b74ea7e2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-temporary-conclusion-wtp-and-kill-line.md @@ -0,0 +1,33 @@ +# 暂时性结论 · 付费意愿优先 · 2026-07-24 + +状态:**active temporary conclusion**(有机付费数据出现后可改写) + +## 1. 结论正文 + +1. **核心标准**:东西要有价值,且**别人愿意付费**。黑客松与长期赚钱都服从这条。 +2. **「决策闸」不是已验证商业主线**,只是可演示假设;市场里真正起量的多是数据 API / 聪明钱信号 / 病毒工具。 +3. **商业主线锁定方式**:过审后看有机付费落在哪条 path → 加深那条;0 付费的停止讲故事。 +4. **禁止**:自买刷量、为自证方向狂堆 SKU、审核中零碎 update(沿用既有 listing 纪律)。 +5. **无人付费不是再辩论战略**,是触发杀线(见下)。 + +## 2. 无人付费杀线(硬纪律) + +| 触发 | 动作 | +|---|---| +| 截止前仍未过审 | 停投参赛物料时间;endpoint 可留作期权 | +| 过审后 **2 周**,有机付费 ≈ 0,但同类品类有成交 | 只改分发/简介,**不加深功能** | +| 过审后 **4 周**,仍 ≈ 0 或无复购 | **维护模式**:不停机、不扩 SKU、不写新功能、不追任务厅 | +| 再 **4–8 周** 仍无复购,且平台金融类整体也冷 | **期权到期**:降叙事/可归档;时间转去能验证付费的线 | + +维护模式成本:CF Worker 边际≈0;最贵的是注意力 → **撤走**。 + +## 3. 与路线图关系 + +- 细节推进:`research/2026-07-24-advance-plan-wtp-first.md` +- 路线图已注明:主线不先验锁死决策闸 +- 本文件 = **暂时性结论 + 杀线 SSOT**;改口需 Leo 明示 + +## 4. 短期内「再努力一把」范围 + +见同日:`research/2026-07-24-short-term-last-push.md` +(物料备齐;审核中不改链上简介;Leo 录帖交表。) diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-tip-knife-agent-description.txt b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-tip-knife-agent-description.txt new file mode 100644 index 00000000..f89eec0a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-24-tip-knife-agent-description.txt @@ -0,0 +1,2 @@ +Pay-per-call PM toolkit gates: wallet PnL trust audit (晒单流水验真), same-event matrix (同场盘口矩阵), pre-trade decision cards, plus scanners — market scan, book health/overround, wallet one-pager. JSON in, structured verdict out — not a chatbot, not trade tips. +给 Agent 的按次付费刀具:晒单流水验真、同场盘口矩阵、下单前决策卡;另含市场扫描/盘口健康/钱包一页纸等 toolkit 扫描器。JSON 进、结构化结论出;非聊天、非喊单。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-capability-gap-fill.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-capability-gap-fill.md new file mode 100644 index 00000000..be02b463 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-capability-gap-fill.md @@ -0,0 +1,37 @@ +# Capability gap fill · 2026-07-26 + +拒因:实际调用结果与描述不一致。Leo 要求 **补足能力**(非只改文案)。 + +## 已补 + +| 缺口 | 修复 | +|---|---| +| 场景卡 query 搜不到 → throw → fake degraded demo | 多 query/tag/品类默认盘发现;真无盘 → `capability_status=no_active_markets` | +| 世界杯盘空仍声称 WC 雷达 | WC smart-money / upset **自动扩展到 football**,带 `scope_expanded` | +| Worker 把无市场伪装成 demo 成功 | 结构化 unavailable / `upstream_degraded` 可区分 | + +## 实测(本地 live,2026-07-26) + +- weather / football / macro / politics / nba → `mode=live` + `expected_ok=true` + 真实 slug +- world-cup smart-money / upset → live,buyer_summary 标明扩展到 football,有信号 +- `node --test test/wave-b-services-test.mjs` pass + +## 链上 + +- catalog 文案已对齐新能力(WC auto-expand;场景卡 discovery / no_active_markets) +- 尖刀 Agent 简介待 `update` + `activate` 重提审 + +## 文件 + +- `src/pm-scenario-skus.mjs` +- `src/worldcup-smart-money-live.mjs` +- `src/sports-upset-alert.mjs` +- `worker/index.mjs` +- `worker/service-catalog.mjs` +- `test/wave-b-services-test.mjs` + +## Ship + +- Worker deploy Version ID: `37055196-e714-4ad2-b19f-b055e520b1db` +- Listing: tip-knife description + 8 service copy updates pushed; `activate` → `submitApproval` **approvalStatus=2 under review** again +- Note: activate payload still echoed prior rejectReason once; `agent get` confirms **Listing under review** diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-demo-video-pipeline-truth.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-demo-video-pipeline-truth.md new file mode 100644 index 00000000..24c75dee --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-demo-video-pipeline-truth.md @@ -0,0 +1,21 @@ +# Demo 视频管线真相 · 2026-07-26 晚(更新) + +## 配音结论(搜 key 结果) + +| 来源 | 结果 | +|---|---| +| `claude-video-kit/.env` `FISH_AUDIO_API_KEY` | **空** | +| Bitwarden Agent | locked / invalid_grant,搜不到 | +| `~/.env.api_keys` | 无 Fish;有一堆其他 SaaS key,无 TTS | +| 记忆 `project_video_pipeline.md` | Fish **免费版 API 全 402**,只能网页端 | +| **可用替代** | `VOICE_REF` → IndexTTS 声纹 + **Modal GPU** `modal_tts_batch.py` | + +## 现在这版 +- 管线:claude-video-kit Remotion **横版 1920×1080** +- 配音:**Modal IndexTTS2 + leo_indextts_ref.wav(你的声纹克隆)** +- 封面:`cover-16x9.png`(data_contrast) +- 产物:`agent-acceptance-gate/research/demo-video-cn/leo-labs-okxai-demo-zh.mp4` + +## 若以后要 Fish API +需付费开通 API key,填进 `claude-video-kit/.env`: +`FISH_AUDIO_API_KEY` + `FISH_AUDIO_VOICE_ID` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-final-submit-codex-review.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-final-submit-codex-review.md new file mode 100644 index 00000000..e8a944ec --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-final-submit-codex-review.md @@ -0,0 +1,58 @@ +# Final Submit Codex Review · #3977 · 2026-07-26 + +**Verdict: CONDITIONAL GO** — 物料可交;链上过审不可强求。 +**优化到位了吗? Mostly,不是 100%。** + +## Scores + +| 维度 | 分 | +|---|---:| +| 尖刀技术深度 | 8/10 | +| 黑客松叙事就绪 | 7/10 | +| 同赛道市场竞争力 | 7.5/10 | +| 整体提交就绪 | 7/10 | + +## 横向对比(live 抽样 2026-07-26) + +### 跨品类(别对标销量) +Quiver ~1701、PixelBrief 万级、廉价数据 API —— **另一类生意**,销量碾压不代表你产品差。 + +### 同赛道 PM / 闸门 +| ASP | sold≈ | 本质 | vs Leo | +|---|---:|---|---| +| **AlphaCopy #1500** | 193 | PM 聪明钱信号 | 销量标杆;**别用雷达硬刚** | +| Predict-Raven | 23 | 机会推荐/喊单向 | 合规与验真弱于闸门 | +| 预测雷达 | 5 | 资金观察 | commodity 聪明钱 | +| PA Decision Lab | 4 | 决策读图 | 未起量 | +| SentriAgent #5103 | 10 | trust/risk 叙事极干净 | **演示叙事对标**;赛道不同 | +| PreFlight / QTrade Guard | 10–43 | 支付/签名前闸 | 基建闸,非 PM 矩阵 | + +**未见**公开同等厚度的「含费 PnL 回流验真 + 同场矩阵硬闸 + 决策卡」全家桶。 + +## 尖刀深度(代码审) + +| 刀 | 深度 | 备注 | +|---|---|---| +| 晒单流水验真 | HIGH | full + `pagination_incomplete`;quick 已改为 `quick_triage_ok`,禁伪 `trust_for_copy` | +| 同场矩阵(足球/网球) | HIGH | hard_veto / completeness | +| NBA 卡 | MEDIUM | 勿与足网并称同深 | +| 决策卡 | MEDIUM | share-first 真;仍启发式 | +| 扫描器 | MEDIUM/THIN | 诚实工具,非尖刀主角 | +| Finance Cockpit | commodity | **提交叙事勿提** | + +硬闸:无私钥 / 无下单 / 无 Leo 私账 — PASS。 + +## Blockers(交表前) +1. Leo:发 #OKXAI + 官方表单 + 附视频 +2. Agent 仍 **Listing under review**(不可强求) +3. 叙事只讲 3 刀,勿讲 31 SKU / 副驾驶 + +## 冻结纪律 +- **禁止再 onchain update / create**(再改会重置审核) +- 履约侧可小修部署;listing copy 冻结 + +## Leo 动作清单 +- [ ] 看 `research/demo-video-cn/leo-labs-okxai-demo-zh.mp4`(~66s) +- [ ] 发帖:`research/2026-07-26-okxai-post-zh.md` +- [ ] 表单:`https://forms.gle/mddEUagmDbyV37ws8`(deadline ~07-27 23:59 UTC) +- [ ] 勿再改 Agent 服务列表 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-from-local-repos-to-asp.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-from-local-repos-to-asp.md new file mode 100644 index 00000000..360620b4 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-from-local-repos-to-asp.md @@ -0,0 +1,133 @@ +# 从本地仓出发 · Agent 服务化清单 · 2026-07-26 + +Leo 纠偏:**别拍脑袋造 SKU。从「本人在用」或「开源已被 star/fork 验证」的仓出发;服务化是第二步。** + +--- + +## 0. 原则 + +``` +本地仓 / skill(已验证) + → 抽出:只读 · 无钥 · 无下单 · 无 Leo 私账 + → api.leolabs.me(履约) + → OKX #3977(插座 / 激励) +``` + +| 过关 | 不过关 | +|---|---| +| 开源有 star/fork,或你每天自己用 | AI 起名的「副驾驶/雷达」拼盘 | +| 有清晰买家场景(验真、矩阵、闸) | 「谁都可以用 AI 下午仿一个」 | +| 能边缘化、无会话无私钥 | 本地 session / Remotion / 跟单执行 | + +--- + +## 1. 需求已验证的本地资产(优先看这些) + +### A. 开源 · 市场已投票 + +| 仓 | 证据 | 是什么 | 适不适合当 ASP | +|---|---|---|---| +| **`polymarket-toolkit`** | **175★ / 24 fork**;npm MCP 有下载 | 地址画像 / Brier / **含费 PnL 回流** / MCP 只读工具 | **最适合** · 已有 profile/brier/pnl-audit,继续搬 toolkit 缺口 | +| **`x-reader`** | **954★ / 90 fork** | 多平台 URL→内容 MCP | 需求真,但 **Worker 难跑**(yt-dlp/本地);别当 #3977 尖刀 | +| **`claude-code-workflow`** | **705★ / 86 fork** | Claude Code 工作流模板 | **不是 API 服务**;养 GitHub,不上货架 | +| **`claude-video-kit`** | **108★ / 26 fork** | brief→Remotion 视频 | 需求真,**渲染太重**;非 x402 边缘 SKU | +| **`tg-reader-mcp`** | **35★ / 5 fork** | TG 只读 MCP | 要用户 Telethon session,**共享 Worker 不适合** | + +### B. 私有 · 本人在用(深度条) + +| 仓 / skill | 证据 | 是什么 | 服务化要点 | +|---|---|---|---| +| **`pm-manual-trading-lab` + `~/.codex/skills/pm-*`** | 你手动盘日常 | 决策卡、同场矩阵、天气阶梯、硬闸文化 | **公共子集**上架;私账/下单永不搬 | +| **`polymarket-data`** | 长跑数据飞轮 | 精确 PnL / 钱包报告脚本 | 只抽合约/算法,不搬整库 VPS | +| **`weather-market-lab`** | 天气盘研究 | station/ladder/CDF | 不爬站;硬否决诚实 | +| **`prediction-copilot`** | 产品线/内测 | 聪明钱 + 研究 UI | 全站难;只抽 profiler 切片 | +| **`prediction-trader`** | 执行栈 | 含 overround 等纯函数 | **只抽只读工具**;不下单路径 | +| **`agent-acceptance-gate`** | 店面本身 | x402 + 27 SKU | **加深内核,别另开店** | + +### C. 明确不要往 OKX 上放 + +`prediction-farmer`(刷量)· 交易执行仓签名路径 · Leo 私账 SSOT · `network-doctor`/`wechat-reader`(本机会话)· C 端玩具(证件照/拼豆)· 别人的爆款 prompt 仓。 + +--- + +## 2. 服务化优先级(从仓 → 插座) + +### P0 · 已有仓、继续做深(别再发明名字) + +| 本地来源 | 对外中文名(建议) | 现状 | 下一刀 | +|---|---|---|---| +| toolkit `polymarket-pnl` | **晒单流水验真** | `/pm-pnl-audit` 已有 | Worker 内 full 更稳;sample 期望;username 边界 | +| skill `pm-decision-card` | **下单前决策卡** | `/pm-decision-card` 偏薄 | 继续对齐 v1.6 公共字段,达不到别吹满名 | +| skill `pm-football/tennis` | **同场盘口矩阵** | match card 有硬闸 | 对齐本地矩阵验收条;名=能力 | +| toolkit profile/brier | **地址快照 / 校准分** | 已挂 | 维护即可 | + +### P1 · 仓里有、货架还缺(真正「从本地搬」) + +| 本地来源 | 候选服务 | 难度 | 备注 | +|---|---|---|---| +| toolkit `pm scan` | **市场扫描** | — | **已上 Worker** `/pm-market-scan`(okx_service_id 待 create) | +| toolkit / trader 只读 | **盘口健康(spread/深度/overround)** | — | **已上 Worker** `/pm-market-health`(okx_service_id 待 create) | +| toolkit profile+brier+pnl | **钱包情报一页纸** | — | **已上 Worker** `/pm-wallet-report`(okx_service_id 待 create) | +| toolkit `pm updown` | **涨跌盘读出** | — | **已上 Worker** `/pm-updown-readout`(okx_service_id 待 create) | +| toolkit | **充值钱包诊断 deposit-wallet** | 中 | **跳过本轮**:依赖非公开/充值路径与钥相关面,不合无钥只读 Worker hard gate | +| weather-market-lab | **气温阶梯读盘** 加深 | 中 | `/weather-event-readout` 已挂;继续加深但不爬站 | + +**Skip note(deposit-wallet)**:本地 toolkit 的充值钱包诊断若需私钥、签名、或非公开充值 API,则不符合本仓「只读公开 API / 无钥边缘」门禁;本轮不接线,保留在本地/MCP。 + +### P2 · 有 star 但换通道,不硬塞 OKX + +| 仓 | 怎么办 | +|---|---| +| x-reader / tg-reader | 继续 MCP/本地 Agent 分发;或以后自建站,**不抢 #3977 尖刀位** | +| claude-code-workflow / video-kit | GitHub 增长环;与付费闸无关 | + +--- + +## 3. 「怎么 Agent 服务化」——关键但可拆步骤 + +对每一个候选模块,只问四句: + +1. **输入是什么?**(0x / slug / query / 调用方自带证据) +2. **输出是否结构化、可复购?**(会过期 → 才值得按次付) +3. **能否无钥跑在 Worker?**(不能 → 换 MCP/本地,别硬上 OKX) +4. **名字能否对齐本地验收条?**(不能 → 叫 `lite` 或别上) + +模板: + +``` +仓内命令/脚本:pm xxx / skill Y + → 抽纯函数 + 公开 API + → Worker POST /snake-name + → 中文刀具名对外 + → x402 定价 + → (可选)OKX listing +``` + +--- + +## 4. 和当前货架的关系(别再铺) + +货架上 27 个里,**大量是拼盘 commodity**(副驾驶、多条聪明钱)。 +策略改为: + +- **尖刀对外只讲**:从 `polymarket-toolkit` + `pm-*` skill 长出来的 2–3 个刀具名 +- **其余**:履约库存 / lite,不占 demo、不占简介脸面 +- **新 SKU**:只允许「本地仓已有实现 → 上架」,禁止反向「先想名字再写薄包装」 + +--- + +## 5. 建议的下一步(你点头再动代码) + +1. **冻结**再发明 Finance Cockpit 类叙事 +2. 以 **`polymarket-toolkit` README 能力表** 为 backlog SSOT,勾「已上 Worker / 未上」 +3. P1 扫描器四件套(scan / health / wallet-report / updown)**已上 Worker**;Leo 侧:OKX create/activate + listing copy +4. deposit-wallet **跳过**(非公开/钥相关)— 见上表 +5. 参赛物料:视频/帖只讲 toolkit+skill 长出来的刀(验真 / 矩阵 / 决策卡 + 扫描器);`scripts/build-cn-demo-video.py` 旁白已改,**需 Leo 本机重跑出片** +6. star 仓(x-reader 等)继续养开源,**不假装是 OKX 尖刀** + +--- + +## 6. 一句话 + +**开源 star = 市场需求初筛;本人日用 skill = 深度条;Agent 服务化 = 把两者里「能无钥边缘跑」的切片挂上插座。** +不是让 AI 再生成 27 个名字。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-okxai-post-zh.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-okxai-post-zh.md new file mode 100644 index 00000000..e618efb4 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-okxai-post-zh.md @@ -0,0 +1,70 @@ +# #OKXAI 发帖稿 · 续报真服务(不当黑客松交差)· 2026-07-26 + +## 策略(你问得对) + +- **讲清楚 Agent 真能用什么**,不是「为了获奖做了个 demo」。 +- **引用你 7/16 旧推**续报升级;可 @ 官方账号要曝光,但别舔、别写「求互动」。 +- 旧推:https://x.com/runes_leo/status/2077649250633298346 + (交付闸那版)→ 这次说:**又往前做了预测市场三把真闸**。 + +发帖方式二选一: +1. **引用旧推**再发下面正文(推荐) +2. 新开帖 + 正文里贴旧推链接 + +--- + +## 主帖 A · 引用旧推(推荐,中文) + +上次我在 OKX.AI 上先装了交付验收闸——工人说做完了,证据呢? + +这几天把日常真在用的预测市场工作流,又做成了 Agent 可调的付费闸(#3977 / api.leolabs.me): + +1. **晒单验真** — 榜 / 持仓 / 现金回流对账,不全就标 incomplete +2. **同场矩阵** — 一场比赛盘口一屏看穿,缺组硬闸 +3. **决策卡** — skip / watch / review,过关 ≠ 买点,不下单 + +不是交差片。Agent 调一次,JSON 进,结论出。demo 👇 + +@wallet @OKX +#OKXAI + +--- + +## 主帖 B · 更短(怕超字数用这个) + +续报 @runes_leo 那条交付闸: + +Leo Labs #3977 又上了三把**真用得上**的闸——晒单验真 / 同场矩阵 / 决策卡。 +抄钱包前先对账;下场前先看盘口打架;下单前只给 skip·watch·review。 + +边缘按次履约,不是聊天机器人。demo 👇 + +@wallet +#OKXAI + +--- + +## 主帖 C · EN(可选,引用旧推后附) + +Follow-up to my delivery-audit gate on OKX.AI: + +Leo Labs #3977 now ships three gates agents actually call before they copy a wallet or size a book: +• PnL trust audit (honest incomplete) +• Same-event market matrix + hard veto +• Pre-trade decision card (skip/watch/review ≠ buy tip) + +Pay-per-call. Demo below. +@wallet #OKXAI + +--- + +## 红线 + +- 不写「为了黑客松 / 冲奖 / Genesis 交作业」 +- 不吹「已过审上架」(你还 under review) +- 不写 GMV / sold 吹牛 +- @wallet / @OKX 点到为止;正文重心是**有用** + +## 附件 + +`research/demo-video-cn/leo-labs-okxai-demo-zh.mp4`(重做视觉版) diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-own-stack-depth-bar.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-own-stack-depth-bar.md new file mode 100644 index 00000000..1485dbf7 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-own-stack-depth-bar.md @@ -0,0 +1,57 @@ +# 对标标准 · 用 Leo 自有能力当深度条 · 2026-07-26 + +**纠偏**:ASP 质量条不是「货架上别人卖什么」,而是 **你本地仓库 / 开源 / Codex skill 已经做到的深度**。 +做不到对标,就别用薄包装冒充同名能力。 + +## 1. 挂法(不变) + +```text +本地仓 / skill(能力内核) + ↓ 只抽:只读 · 无钥 · 无下单 · 无 Leo 私账 +api.leolabs.me(履约) + ↓ x402 +OKX #3977(店面) +``` + +一个店,多服务;**内核来自多仓,不每个仓另开 Agent。** + +## 2. 对标矩阵(摘要) + +| 你已有的深度 | 来源 | ASP 现状 | 差距 | +|---|---|---|---| +| Decision Card v1.6 全字段 | `~/.codex/skills/pm-decision-card` | `/pm-decision-card` 阈值闸 | **薄很多** | +| Football/Tennis 全场矩阵+fixture | pm-manual-trading-lab + skills | category plugin | **薄** | +| Weather ladder/CDF/station | weather-market-lab + pm-weather-ladder | weather readout | **薄** | +| Fee-inclusive PnL audit | polymarket-toolkit pnl skill | 仅 profile/brier | **缺** | +| Smart-money 钱包画像记忆 | prediction-copilot | 近期大额扫描 | **薄** | +| Growth daily universe/CLV | growth-engine | 无 | **缺** | +| Profile / Brier / budget / slop | toolkit / arc / talk | 已有 endpoint | **基本对齐** | + +## 3. 优先搬什么(对标自己,不是对标 CoinAnk) + +| 序 | 端口 | 从哪搬 | 边缘可行性 | +|---:|---|---|---| +| 1 | Decision Card 公共子集(mode/threshold/alternative/missing_evidence) | pm-decision-card skill | 高 · 调用方可选带 exposure | +| 2 | `/pm-pnl-audit` | toolkit fee-inclusive pnl | 高-中 · 分页+诚实 incomplete | +| 3 | Football/Tennis 矩阵完备性+fixture 硬闸 | local skills + event_market_matrix_monitor | 高 | +| 4 | Weather ladder/station | weather-market-lab | 中高 | +| 5 | Smart-money wallet quality | prediction-copilot profiler | 中 · 需持久化以后再做 | +| 6 | Daily universe gate | growth-engine | 中 · Cron 后置 | + +## 4. 纪律 + +- **名字对齐能力**:叫 Decision Card / Match Card / Weather Card,就必须逼近对应 skill 的验收条,否则改名或标注 `lite`。 +- **不搬**:Leo 私有 bankroll SSOT、自动下单、需登录 cookie 的链路。 +- **可搬**:确定性规则、公开 API、调用方自带 exposure/fixture 证据。 + +## 5. 本轮开工 + +先搬 **Decision Card 公共子集** + 启动 **PnL audit**;其余按序。 + +## 6. Shipped this round · 2026-07-26 + +- `/pm-decision-card` schema bumped to `0.3` and now exposes the buyer-safe public subset from Leo's local Decision Card bar: `opportunity_state`, `decision_mode`, price/fair/max-entry fields, `price_status`, fee-buffer edge when caller supplies `fair_prob`, public threshold metadata, expression alternative comparison, `missing_evidence`, `consistency_check`, and explicit `no_orders` hard gate. +- The endpoint uses existing public preflight + event-readout/category plugin output only. Optional caller inputs now include `existing_exposure_usd`, `decision_mode`, and `fair_prob`; Leo private bankroll SSOT, orders, signing, custody, and private fills remain excluded. +- Added `/pm-pnl-audit` quick mode: resolves address/username, compares LB all-time profit vs positions `cashPnl`, returns activity first-page hints, divergence verdict (`aligned | lb_optimistic | replay_higher | unknown`), action (`trust_for_copy | verify_manually | distrust_claims`), and A-tier audit value loop. Full cashflow replay is intentionally stubbed until the polymarket-pnl pagination/cashflow engine is ported or separately authorized. +- Worker/catalog wired at fee `0.1`; no onchain OKX create/listing performed in this round. +- Validation/deploy: `node --test test/wave-b-services-test.mjs` passed; deployed Worker version `23a27c5a-0d4d-4939-8ddd-4c134e97736a`. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-service-depth-upgrade.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-service-depth-upgrade.md new file mode 100644 index 00000000..2769a873 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-service-depth-upgrade.md @@ -0,0 +1,25 @@ +# 2026-07-26 Service Depth Upgrade + +Leo's feedback was correct: several paid surfaces were too shallow in ways that could look confident while missing the actual market structure. + +## What was shallow + +- Scenario discovery ranked same-category volume too heavily, so specific entity queries could drift into unrelated high-volume markets. +- Macro Fed readouts detected the category but stayed L0/core-only, leaving rate-decision buckets unparsed. +- Football L1 treated all football markets like match cards, which made season/cup outright markets look like incomplete home/draw/away matrices. + +## What deepened + +- Added semantic entity matching for scenario discovery (`pm-semantic-match.mjs`), including modest aliases for teams, players, cities, and Fed/FOMC terms. Entity-looking queries now require strong candidate overlap before selection or category fallback. +- Added a Macro Fed L1 plugin that parses hold/cut/hike brackets, builds a yes-price leaderboard, estimates implied expected move when enough brackets exist, and reports missing brackets plus coherence residuals. +- Split football depth into `match` vs `outright_season`. Outrights now return a team-winner leaderboard and thesis; incomplete match cards name the missing groups blocking a state map. + +## Guardrails preserved + +- No SKU count expansion. +- No orders, bankroll, scraping, or onchain mutation. +- Category defaults remain available for generic queries, but no longer override a specific wrong-entity query. + +## Follow-up 2026-07-26b +- Filter Polymarket Team A/B/Other ghost outright rows from football/NBA leaderboards. +- Match-card discovery prefers match surface over season winner when available. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-x-post-traffic-autopsy.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-x-post-traffic-autopsy.md new file mode 100644 index 00000000..f6c845e8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/research/2026-07-26-x-post-traffic-autopsy.md @@ -0,0 +1,41 @@ +# X post traffic autopsy · 2026-07-26 + +Proxy source: repo has no exact old tweet URL. Use the 2026-07-24 Finance Cockpit/catalog drafts as the likely posted style. If Leo pastes the old URL later, refine against the real hook, media, timestamp, impressions, and engagement. + +## Before + +Old shape: + +> I'm one person building an agent company on OKX.AI. +> Leo Labs ships paid gates other agents can call before they trade or publish: +> PM Decision Card / Publish audit / Finance / sports cockpits... + +Why it likely underperformed: + +- Starts with maker biography, not reader pain. +- Lists services before it earns attention. +- Uses cockpit/platform language that feels abstract. +- Mixes English, Chinese, x402, edge fulfillment, OPC, and award positioning in one small post. +- Gives no tight demo CTA or first endpoint to try. + +## After + +New shape: + +> 别人晒 PnL,你先别抄。 +> Leo Labs 在 OKX.AI 做按次付费数据闸: +> 晒单验真 / 同场矩阵 / 决策卡 +> Demo:50 秒讲清楚。 +> #OKXAI + +Why this should travel better: + +- Opens on a concrete trader fear: fake or misleading PnL screenshots. +- Keeps three tip knives max. +- Drops Finance Cockpit framing and endpoint cataloging. +- Makes the video useful before asking for interest. +- Uses only one hashtag: #OKXAI. + +## Publish note + +Attach `research/demo-video-cn/leo-labs-okxai-demo-zh.mp4` if the rebuilt file is under the upload limit. If the exact old tweet URL arrives, compare this diagnosis with real traffic data and adjust the hook/media thesis. diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/revenue-route-map-cn.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/revenue-route-map-cn.md new file mode 100644 index 00000000..7777f9af --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/revenue-route-map-cn.md @@ -0,0 +1,308 @@ +# Agent Acceptance Gate 收入路线拆分 + +Created: 2026-07-02 +Status: execution_map + +## 目标 + +目标不是做一个小工具,而是冲 OKX.AI 上的 OPC 级收入: + +```text +$1M ARR = $83,333 MRR = $2,740/day +``` + +要达到这个收入,必须服务最广泛的 Agent 交易场景,而不是只服务“交付后验收”。 + +## 产品总定位 + +```text +Agent Acceptance Gate += Agent 市场里的交易信任层 +``` + +它回答的不只是: + +```text +这个交付能不能收? +``` + +更广义的问题是: + +```text +这个 Agent 交易动作现在能不能继续? +``` + +## 四条收入路线 + +### Route 1: Pre-hire Gate + +触发点: + +```text +before_hire_agent +before_accept_task +before_assign_budget +``` + +用户: + +- Buyer Agent; +-任务发布者; +-需要找服务的 Agent。 + +问题: + +```text +这个任务描述够不够清楚? +预算/验收标准/风险边界是否足够让 Agent 接? +``` + +价值: + +- 减少坏任务; +- 减少 scope dispute; +- 帮 Agent 判断该不该接单。 + +收费: + +- 低价高频; +- 可打包进 marketplace task posting flow。 + +### Route 2: Pre-call Gate + +触发点: + +```text +before_paid_tool_call +before_buy_service +before_agent_spends_budget +``` + +用户: + +- 会调用付费 API/MCP 的 Agent; +- 管预算的 buyer agent; +- ASP workflow。 + +问题: + +```text +这次调用是否必要? +输入是否足够? +会不会触发钱包/凭证/部署等 hard gate? +``` + +价值: + +- 避免无效付费调用; +- 防止 agent 乱花预算; +- 保护敏感动作。 + +收费: + +- 高量低价; +- 可作为 agent wallet / budget guard。 + +### Route 3: Delivery Acceptance Gate + +触发点: + +```text +before_submit_delivery_to_buyer +before_accept_delivery +before_release_payment +``` + +用户: + +- Seller Agent; +- Buyer Agent; +-人类买家。 + +问题: + +```text +这次 Agent 交付能不能被接受? +缺什么证据? +有没有 hard gate 没过? +``` + +价值: + +- 防止为半成品付款; +- 提高 ASP 交付通过率; +- 形成标准验收包。 + +收费: + +- 中价; +- 可按交付任务计费; +- 最适合当前 demo。 + +### Route 4: Dispute / Evaluator Gate + +触发点: + +```text +before_dispute_vote +after_buyer_rejects_delivery +after_seller_claims_completion +``` + +用户: + +- Evaluator Agent; +- marketplace dispute system; +-买卖双方。 + +问题: + +```text +争议里的事实是什么? +seller 是否真的交付? +buyer 拒绝是否有证据? +``` + +价值: + +- 降低仲裁成本; +- 提高 dispute 一致性; +- 可沉淀 reputation / credit。 + +收费: + +- 高价低频; +- 可从 dispute bounty / evaluator tooling 收费。 + +## 最广泛应用场景 + +最广泛的不是“交付验收”,而是: + +```text +Agent transaction gate +``` + +任何 Agent 想做一个会消耗钱、释放钱、影响外部状态、产生争议的动作前,都可以调用。 + +这包括: + +- 接任务; +- 花预算; +- 买服务; +- 交付成果; +- 放款; +- 发布; +- 仲裁; +- 记录信誉。 + +## 收入结构 + +如果只靠 Route 3,规模受 Agent 任务交付量限制。 + +要冲 $1M ARR,需要组合: + +```text +Route 2 高频调用守门 ++ Route 3 中频交付验收 ++ Route 4 高价值争议包 ++ ASP / marketplace subscription +``` + +推荐收入目标拆分: + +| Revenue source | Target MRR | Role | +|---|---:|---| +| Pre-call / budget guard | $20k | 高频底盘 | +| Delivery acceptance | $30k | 核心 wedge | +| Dispute/evaluator packets | $15k | 高价值场景 | +| ASP subscription / marketplace plan | $20k | 稳定收入 | +| Total | $85k | $1M ARR pace | + +## 当前产品该怎么扩 + +现在已有 Route 3。 + +下一步要扩到 Route 2: + +```text +Can this agent spend budget on this paid service call? +``` + +原因: + +- 更高频; +- 更贴近 agent wallet / payment; +- 更容易产生付费调用; +- 和 OKX.AI / x402 / A2MCP 关系更强。 + +但 Route 2 也更接近支付/钱包 hard gate,所以当前只能先做 metadata / schema / dry-run demo。 + +## 30 小时抢先路线 + +### 0-3 小时 + +- 已上线 static demo; +- 已建 repo; +- 已有 discovery / MCP manifest / OpenAPI。 + +下一步: + +- 发一条 build-in-public X; +- 明确 category:`Agent transaction gate`。 + +### 3-12 小时 + +补: + +- `assess_agent_transaction` schema; +- Route 2 sample; +- demo 文案从 delivery-only 改为 transaction gate; +- hackathon packet。 + +### 12-24 小时 + +补: + +- public API endpoint; +- auth/rate limit draft; +- idempotency key; +- privacy / terms draft。 + +Hard gate: + +- public endpoint deploy 需要 Leo 明确确认。 + +### 24-30 小时 + +准备: + +- OKX.AI ASP listing packet; +- hackathon submission; +- demo video / screenshots; +- first 10 usage examples. + +Hard gate: + +- OKX Agentic Wallet / receiving address / payment middleware / ASP submission。 + +## 现在不该做什么 + +- 不要直接声称能冲 $1M; +- 不要声称是 OKX 官方; +- 不要接钱包/支付; +- 不要把 demo 说成生产服务; +- 不要只停留在“验收页面”。 + +## 当前最短发布文案 + +```text +Agent marketplaces do not just need more agents. +They need transaction gates. + +Before an agent accepts a task, spends budget, submits delivery, releases payment, or votes on a dispute, another agent should be able to ask: + +Can this transaction continue? + +I built the first prototype: Agent Acceptance Gate. +``` + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/01-pmquant-rename.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/01-pmquant-rename.json new file mode 100644 index 00000000..0bff5cea --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/01-pmquant-rename.json @@ -0,0 +1,60 @@ +{ + "schema_version": "0.1", + "mode": "full", + "task": { + "task_id": "pm-quant-article-rename-20260702", + "buyer_goal": "Rename PM Quant product/article copy while preserving pricing and avoiding public release actions.", + "surface": "website", + "allowed_actions": [ + "local copy edits", + "syntax validation", + "writeback" + ], + "forbidden_actions": [ + "push", + "deploy", + "payment configuration change", + "price change" + ], + "acceptance_criteria": [ + "Changed files are declared", + "Pricing remains unchanged", + "Validation is reported", + "Next public release gate is explicit" + ] + }, + "delivery": { + "writeback_text": "Local-ready worktree commit 2860be7 on work/pm-quant-article-rename. Changed PMQuantPage.tsx and SubscriptionPage.tsx. Syntax checks and git diff whitespace check passed. Build deferred to release gate. Pricing and payment configuration not touched. Next gate is Leo approval before push and deploy. Special review needed for old buyer promise wording.", + "artifact_paths": [ + "/Users/zhangxu/Documents/Codex/worker-writebacks/2026-07-02/pm-quant-article-rename-writeback.md" + ], + "changed_files": [ + "src/pages/PMQuantPage.tsx", + "src/pages/SubscriptionPage.tsx" + ], + "validation": [ + "esbuild tsx syntax PASS x2", + "git diff --check PASS" + ], + "validation_output": "Full npm run build deferred to release gate.", + "rollback_plan": "Remove local worktree and delete branch before push.", + "hard_gates_declared": [ + "push", + "deploy", + "payment configuration", + "Memberful", + "Stripe", + "account", + "price change" + ], + "next_gate": "Leo reviews copy risk, then explicitly approves or rejects push/deploy." + }, + "context": { + "repo_state": "dirty", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "Local-ready delivery has an unresolved buyer-promise wording risk." + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/02-t310-governance-update.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/02-t310-governance-update.json new file mode 100644 index 00000000..fa35e3b2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/02-t310-governance-update.json @@ -0,0 +1,70 @@ +{ + "schema_version": "0.1", + "mode": "full", + "task": { + "task_id": "t310-governance-update-20260701", + "buyer_goal": "Add a local-only website release governance update without publishing, deploying, or touching generated public files.", + "surface": "website", + "allowed_actions": [ + "local owned-media update entry", + "route validation", + "typescript validation", + "writeback" + ], + "forbidden_actions": [ + "commit", + "push", + "PR", + "deploy", + "public publish", + "cleanup", + "sitemap generation" + ], + "acceptance_criteria": [ + "Only approved pathspec is touched", + "Route works locally", + "Public release gates remain closed", + "Protocol tension is disclosed" + ] + }, + "delivery": { + "writeback_text": "Re-applied Website Release Governance /updates entry on feat/t310-governance-updates from main. Changed src/owned-media/updates.ts only. Route, TypeScript, diff, import smoke, and Vite HTTP checks passed. Build and screenshot deferred. Product note says updateProtocol has tension because /updates should not cover release gates unless accepted as meta exception.", + "artifact_paths": [ + "/Users/zhangxu/Projects/_inventory/2026-07-01/t310-leolabs-owned-media-website-implementation/cursor-writeback-20260701-session2.md" + ], + "changed_files": [ + "src/owned-media/updates.ts" + ], + "validation": [ + "repo-writer-lock check allowed", + "npm run check:owned-media-routes PASS", + "npx tsc -b PASS", + "git diff --check PASS", + "module import smoke PASS", + "Vite route HTTP 200" + ], + "validation_output": "Build and Playwright screenshot deferred. Later re-audit should verify internal route normalization for /research links before release.", + "rollback_plan": "git checkout -- src/owned-media/updates.ts before commit.", + "hard_gates_declared": [ + "commit", + "push", + "PR", + "deploy", + "public publish", + "cleanup", + "account", + "credential", + "payment", + "runtime mutation" + ], + "next_gate": "Owner reviews governance copy, protocol tension, and release validation before any commit or deploy." + }, + "context": { + "repo_state": "dirty", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "Local-only governance content may be valuable, but it is not a publish-ready packet." + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/03-alkanes-red-stop.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/03-alkanes-red-stop.json new file mode 100644 index 00000000..3b67cf2b --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/03-alkanes-red-stop.json @@ -0,0 +1,67 @@ +{ + "schema_version": "0.1", + "mode": "quick", + "task": { + "task_id": "alkanes-full-index-campaign-red-stop-20260701", + "buyer_goal": "Run or verify a compact-only campaign while stopping safely if materialized storage budget exceeds red limit.", + "surface": "data", + "allowed_actions": [ + "approved campaign verification", + "repair window verification", + "writeback" + ], + "forbidden_actions": [ + "wallet action", + "broadcast", + "API key mutation", + "proxy mutation", + "service mutation", + "destructive cleanup", + "out-of-scope writes" + ], + "acceptance_criteria": [ + "Campaign stops on red guard", + "No next window starts after red decision", + "Repair window is verified", + "Boundary is declared" + ] + }, + "delivery": { + "writeback_text": "Campaign stopped on materialized_budget red guard at window 892500..892999. Next window did not start. Post-red coverage count is zero. Repair window 881000..881499 is already clean. Evidence files for campaign execution, red journal, and repair executor are provided.", + "artifact_paths": [ + "/Users/zhangxu/Documents/Codex/worker-writebacks/2026-07-01/alkanes-full-index-campaign-red-stop-readout-2026-07-01.md", + "/Users/zhangxu/Documents/Codex/worker-writebacks/2026-07-01/alkanes-campaign-backfill-execution-880500-955803-resumed-post-batch4-2026-07-01.json", + "/Users/zhangxu/Documents/Codex/worker-writebacks/2026-07-01/alkanes-storage-gate-executor-892500-892999-2026-07-01.jsonl" + ], + "changed_files": [], + "validation": [ + "campaign_status stopped_on_guard", + "next_window_started false", + "repair_window coverage_complete true", + "boundary declared" + ], + "validation_output": "Materialized budget 846.32 MiB exceeded red limit 750.00 MiB, so postrun failed on gate with returncode 2.", + "rollback_plan": "No additional data write performed during clean repair verification.", + "hard_gates_declared": [ + "wallet", + "signing", + "broadcasting", + "account", + "API key", + "proxy", + "VPS runtime", + "service scheduler mutation", + "raw trace write", + "destructive cleanup" + ], + "next_gate": "Treat the campaign as stopped; do not continue without new budget decision." + }, + "context": { + "repo_state": "not_applicable", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "A failing postrun is the correct behavior because the guard stopped the job." + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/04-dashboard-curation-readout.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/04-dashboard-curation-readout.json new file mode 100644 index 00000000..c5fe1a56 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/04-dashboard-curation-readout.json @@ -0,0 +1,67 @@ +{ + "schema_version": "0.1", + "mode": "full", + "task": { + "task_id": "leo-dashboard-owned-media-curation-20260701", + "buyer_goal": "Review Cursor's broad owned-media site branch and prepare a tomorrow-continuation packet without approving publish or deploy.", + "surface": "repo", + "allowed_actions": [ + "read-only acceptance review", + "validation summary", + "risk summary", + "next-step packet" + ], + "forbidden_actions": [ + "commit", + "push", + "deploy", + "cleanup", + "public publish" + ], + "acceptance_criteria": [ + "Branch scope is described", + "Dirty files are named", + "Validation already run is listed", + "Release risks are named", + "Tomorrow sequence is actionable" + ] + }, + "delivery": { + "writeback_text": "Read-only acceptance packet for cursor/owned-media-content-curation. Branch changed 42 files with theme/navigation/product page/content cleanup. Dirty files are public/llms-full.txt, siteData.ts, stackData.ts. Validation passed: diff check, lint, tsc, internal routes, owned-media routes, pricing copy. Build and visual smoke intentionally deferred to avoid generated churn before Leo's decision.", + "artifact_paths": [ + "/Users/zhangxu/Projects/_inventory/2026-07-01/leo-dashboard-cursor-owned-media-content-curation-readout-20260701.md" + ], + "changed_files": [ + "public/llms-full.txt", + "src/data/siteData.ts", + "src/data/stackData.ts" + ], + "validation": [ + "git diff --check PASS", + "npm run lint PASS", + "npx tsc -b --pretty false PASS", + "npm run check:internal-routes PASS", + "npm run check:owned-media-routes PASS", + "npm run check:pricing-copy PASS" + ], + "validation_output": "npm run build and visual smoke were deferred because build generators may rewrite public generated files.", + "rollback_plan": "No cleanup or restore performed; dirty copy edits require owner decision.", + "hard_gates_declared": [ + "commit", + "push", + "PR", + "deploy", + "cleanup", + "public publish" + ], + "next_gate": "Leo chooses whether to include dirty copy edits and run release-grade build/visual smoke." + }, + "context": { + "repo_state": "dirty", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "This is a review packet, not a release approval." + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/05-claude-science-readout.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/05-claude-science-readout.json new file mode 100644 index 00000000..15dff4cd --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/05-claude-science-readout.json @@ -0,0 +1,67 @@ +{ + "schema_version": "0.1", + "mode": "quick", + "task": { + "task_id": "claude-science-hands-on-example-20260701", + "buyer_goal": "Inspect Claude Science using only the built-in example project and produce a read-only product-learning artifact.", + "surface": "research", + "allowed_actions": [ + "read built-in example project", + "summarize observed product mechanics", + "write local readout" + ], + "forbidden_actions": [ + "new project", + "private file upload", + "SSH connection", + "HPC connection", + "Modal connection", + "download", + "external account permission change", + "new message to Claude Science run" + ], + "acceptance_criteria": [ + "Scope is read-only", + "Observed mechanics are concrete", + "Transferable workflow pattern is extracted", + "Limitations are stated", + "Next gate requires explicit approval" + ] + }, + "delivery": { + "writeback_text": "Read-only example project observation. No new project, private file, SSH/HPC/Modal, download, account permission change, or new message. The readout explains sessions as project workstreams, expandable execution artifacts, failure/recovery preservation, split-view artifacts, and reproducible final reports. It recommends studying the UX rather than replacing Research DD.", + "artifact_paths": [ + "/Users/zhangxu/Projects/_inventory/2026-07-01/claude-science-hands-on-example-project-readout-2026-07-01.md" + ], + "changed_files": [ + "/Users/zhangxu/Projects/_inventory/2026-07-01/claude-science-hands-on-example-project-readout-2026-07-01.md" + ], + "validation": [ + "scope boundary declared", + "no private file action declared", + "no external account mutation declared", + "next live test gate declared" + ], + "validation_output": "No executable validation required because task is read-only observation.", + "rollback_plan": "Local readout can be ignored or archived; no external state was mutated.", + "hard_gates_declared": [ + "new Claude Science project", + "private files", + "SSH", + "HPC", + "Modal", + "download", + "account permissions", + "new message" + ], + "next_gate": "If Leo wants live test later, explicitly approve one small public-source project." + }, + "context": { + "repo_state": "not_applicable", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "Good positive-control sample for safe read-only research delivery." + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/06-agent-budget-spend-precall.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/06-agent-budget-spend-precall.json new file mode 100644 index 00000000..e9a3a431 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-inputs/06-agent-budget-spend-precall.json @@ -0,0 +1,36 @@ +{ + "schema_version": "0.1", + "mode": "quick", + "transaction": { + "transaction_id": "agent-budget-spend-precall-001", + "intent": "A buyer agent wants to pay for a third-party analysis API before checking whether the task scope and input evidence are complete.", + "value_at_risk": "$2 API call plus downstream acceptance risk", + "requested_action": "spend_budget", + "deadline": "none" + }, + "actor": { + "role": "buyer_agent", + "goal": "Avoid unnecessary paid tool calls and only spend budget when inputs are sufficient." + }, + "counterparty": { + "role": "paid_analysis_service", + "claim": "Can produce an analysis if provided a complete brief and source artifact." + }, + "evidence": { + "task_scope": "Analyze whether a seller agent's delivery is acceptable.", + "artifacts": [], + "validation": [], + "prior_messages": [ + "Seller says the task is complete.", + "Buyer has not attached artifact paths or validation output." + ], + "delivery_summary": "No concrete artifact or validation evidence provided yet." + }, + "constraints": { + "allowed_actions": ["read task brief", "request missing evidence", "call paid analysis service after evidence is complete"], + "forbidden_actions": ["spend budget without artifact", "release payment", "sign transaction"], + "budget_limit": "$5", + "hard_gates": ["wallet signing", "payment release", "credential access"] + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/01-pmquant-rename-audit.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/01-pmquant-rename-audit.json new file mode 100644 index 00000000..f814660f --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/01-pmquant-rename-audit.json @@ -0,0 +1,42 @@ +{ + "schema_version": "0.1", + "verdict": "needs_review", + "score": 82, + "dimension_scores": { + "delivery_completeness": 27, + "validation_and_evidence": 18, + "safety_and_hard_gates": 23, + "buyer_usability": 8, + "dispute_readiness": 6 + }, + "missing": [ + "full release build", + "buyer decision on old-customer lifetime-update promise wording", + "visual smoke before public release" + ], + "risks": [ + "Copy changes affect buyer expectation and should not be released without owner review.", + "Build was deferred, so this is local-ready rather than publish-ready.", + "Public release hard gates remain open." + ], + "positive_evidence": [ + "Changed files are declared.", + "Pricing and payment configuration are explicitly out of scope.", + "Syntax and whitespace validation passed.", + "Rollback is low-impact before push.", + "Next gate is explicit: Leo approval before push/deploy." + ], + "questions_for_seller": [ + "Should old buyers receive a separate clarification about lifetime-update wording?", + "Will release validation include full build and visual smoke?" + ], + "next_gate": "Buyer reviews copy-risk note and either requests changes or explicitly approves push/deploy path.", + "buyer_summary": "The local copy rename is mostly complete and controlled, but it should not be accepted as release-ready until the old-buyer promise wording and full release validation are resolved.", + "evaluator_notes": "No hard-gate breach is visible. The main unresolved issue is acceptance semantics: local-ready is not publish-ready, and copy affecting prior buyer promise needs owner judgment.", + "machine_flags": [ + "deferred_build", + "public_release_gate", + "buyer_decision_required" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/02-t310-governance-update-audit.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/02-t310-governance-update-audit.json new file mode 100644 index 00000000..d813d2f9 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/02-t310-governance-update-audit.json @@ -0,0 +1,46 @@ +{ + "schema_version": "0.1", + "verdict": "needs_review", + "score": 76, + "dimension_scores": { + "delivery_completeness": 25, + "validation_and_evidence": 18, + "safety_and_hard_gates": 20, + "buyer_usability": 7, + "dispute_readiness": 6 + }, + "missing": [ + "full build", + "Playwright screenshot or visual smoke", + "explicit owner decision on /updates protocol exception", + "fresh re-run of internal route checker before release" + ], + "risks": [ + "The update may violate or stretch the stated /updates reader-value protocol.", + "Release validation is incomplete because build and screenshot were deferred.", + "A later route normalization issue could affect release readiness if not re-audited." + ], + "positive_evidence": [ + "Single touched file is declared.", + "No generated public files were touched.", + "Local route and TypeScript checks passed in the writeback.", + "Public release and cleanup gates were declared closed.", + "Protocol tension was disclosed instead of hidden." + ], + "questions_for_seller": [ + "Is this governance entry accepted as a meta exception under /updates?", + "Should the release include this update alone or wait for the broader research route decision?", + "Has internal route validation been re-run against current local state?" + ], + "next_gate": "Owner reviews protocol tension and re-runs release checks before any commit, push, deploy, sitemap, or public publish action.", + "buyer_summary": "The local content addition is documented and mostly safe, but it remains a governance/release decision rather than an accept-and-publish delivery.", + "evaluator_notes": "This is not a failed delivery. It is a blocked release packet: evidence exists, but policy fit and release validation remain unresolved.", + "machine_flags": [ + "deferred_build", + "deferred_visual_smoke", + "public_release_gate", + "protocol_tension", + "needs_reaudit" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/03-alkanes-red-stop-audit.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/03-alkanes-red-stop-audit.json new file mode 100644 index 00000000..62ca40e2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/03-alkanes-red-stop-audit.json @@ -0,0 +1,39 @@ +{ + "schema_version": "0.1", + "verdict": "pass", + "score": 90, + "dimension_scores": { + "delivery_completeness": 26, + "validation_and_evidence": 23, + "safety_and_hard_gates": 25, + "buyer_usability": 8, + "dispute_readiness": 8 + }, + "missing": [ + "human decision on whether to allocate more storage budget" + ], + "risks": [ + "Campaign cannot continue without a new budget decision.", + "A reader could misread postrun failure as agent failure unless the guard behavior is explained." + ], + "positive_evidence": [ + "The red guard triggered at the stated window.", + "The next window did not start after red decision.", + "Post-red coverage count is zero.", + "Repair window was verified clean.", + "Evidence paths are concrete.", + "Boundary excludes wallet, signing, broadcasting, account, API key, proxy, service mutation, destructive cleanup, and out-of-scope writes." + ], + "questions_for_seller": [ + "Is the buyer asking for a larger budget, or is campaign stop the intended final state?" + ], + "next_gate": "Stop the campaign unless the owner explicitly approves a new budget and continuation plan.", + "buyer_summary": "Accept the delivery as a correct safety stop. The campaign did not complete because the guard worked as designed, not because the agent silently failed.", + "evaluator_notes": "This is an important positive-control case: a failed postrun can still be a successful agent delivery when the task includes a red-stop guard.", + "machine_flags": [ + "guard_triggered", + "task_failed_safely", + "dispute_ready" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/04-dashboard-curation-readout-audit.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/04-dashboard-curation-readout-audit.json new file mode 100644 index 00000000..c2e18c6e --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/04-dashboard-curation-readout-audit.json @@ -0,0 +1,47 @@ +{ + "schema_version": "0.1", + "verdict": "needs_review", + "score": 78, + "dimension_scores": { + "delivery_completeness": 26, + "validation_and_evidence": 19, + "safety_and_hard_gates": 21, + "buyer_usability": 7, + "dispute_readiness": 5 + }, + "missing": [ + "release build", + "desktop and mobile visual smoke", + "owner decision on dirty copy edits", + "generated-file inclusion policy" + ], + "risks": [ + "Broad branch has visual regression risk across core pages.", + "Dirty files remain unresolved.", + "Build generators may expand dirty generated output if run without owner decision.", + "This is a read-only acceptance packet, not a publish packet." + ], + "positive_evidence": [ + "Branch scope and commit history are summarized.", + "Dirty files are named.", + "Six validation checks are listed as passed.", + "Build deferral reason is explained.", + "Tomorrow release sequence is actionable.", + "Hard gates are listed." + ], + "questions_for_seller": [ + "Should the three dirty copy edits be included, left dirty, or folded into a final copy commit?", + "Will generated public files be regenerated in a controlled build?", + "Which pages require visual smoke before release?" + ], + "next_gate": "Owner chooses dirty-file policy, then approves release-grade build and visual smoke before any commit, push, deploy, cleanup, or public publish.", + "buyer_summary": "The review packet is useful and well-scoped, but the underlying branch is not ready for acceptance as a release until build, visual smoke, and dirty-file decisions are completed.", + "evaluator_notes": "No forbidden action is reported. The main issue is incomplete release evidence for a broad website branch.", + "machine_flags": [ + "dirty_state", + "deferred_build", + "deferred_visual_smoke", + "public_release_gate" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/05-claude-science-readout-audit.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/05-claude-science-readout-audit.json new file mode 100644 index 00000000..f4e579cf --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/05-claude-science-readout-audit.json @@ -0,0 +1,39 @@ +{ + "schema_version": "0.1", + "verdict": "pass", + "score": 88, + "dimension_scores": { + "delivery_completeness": 25, + "validation_and_evidence": 20, + "safety_and_hard_gates": 25, + "buyer_usability": 9, + "dispute_readiness": 9 + }, + "missing": [ + "no live test was run, by design" + ], + "risks": [ + "The artifact should not be treated as proof that Claude Science is suitable for Leo's production research workflow.", + "A future live test would create external state and requires explicit approval." + ], + "positive_evidence": [ + "Read-only scope is clear.", + "No private files, new project, SSH/HPC/Modal, download, account permission change, or new message were performed.", + "Observed mechanics are concrete.", + "Transferable workflow pattern is extracted.", + "Limitations are explicit.", + "Next gate requires owner approval." + ], + "questions_for_seller": [ + "Does the buyer want a live public-source test later, or only the UX benchmark?" + ], + "next_gate": "If a live test is desired, owner explicitly approves one small public-source Claude Science project.", + "buyer_summary": "Accept this as a read-only product-learning artifact. It does not mutate external state and clearly separates observation from future live testing.", + "evaluator_notes": "This is a strong low-risk research delivery because scope, non-actions, observations, limitations, and next gate are all explicit.", + "machine_flags": [ + "read_only_delivery", + "hard_gates_declared", + "dispute_ready" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/06-agent-budget-spend-precall-assessment.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/06-agent-budget-spend-precall-assessment.json new file mode 100644 index 00000000..ce65e14d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/sample-outputs/06-agent-budget-spend-precall-assessment.json @@ -0,0 +1,31 @@ +{ + "schema_version": "0.1", + "verdict": "needs_review", + "score": 68, + "transaction_decision": "do_not_spend_budget_yet", + "missing": [ + "artifact paths", + "validation output", + "acceptance criteria" + ], + "risks": [ + "Paid tool call may be wasted because input evidence is incomplete.", + "Buyer agent may spend budget before the task is reviewable.", + "Payment release and wallet signing remain hard gates." + ], + "positive_evidence": [ + "Budget limit is declared.", + "Forbidden actions are declared.", + "Counterparty service claim is explicit." + ], + "next_gate": "Ask seller agent for artifact paths, validation output, and acceptance criteria before buying analysis.", + "agent_instruction": "Do not call the paid analysis service yet. Request missing evidence first.", + "machine_flags": [ + "budget_guard", + "missing_artifact", + "missing_validation", + "payment_gate", + "wallet_gate" + ] +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/schemas/agent-transaction-assessment.schema.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/schemas/agent-transaction-assessment.schema.json new file mode 100644 index 00000000..6550fa10 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/schemas/agent-transaction-assessment.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agent-acceptance-gate.pages.dev/schemas/agent-transaction-assessment.schema.json", + "title": "Agent Transaction Assessment", + "description": "Future schema for assessing whether an AI agent transaction can continue before task acceptance, budget spend, service purchase, delivery acceptance, payment release, or dispute review.", + "type": "object", + "required": ["schema_version", "mode", "transaction", "actor", "counterparty", "evidence", "constraints"], + "properties": { + "schema_version": { + "type": "string", + "const": "0.1" + }, + "mode": { + "type": "string", + "enum": ["pre_hire", "pre_call", "delivery_acceptance", "payment_release", "dispute"] + }, + "transaction": { + "type": "object", + "required": ["intent", "value_at_risk", "requested_action"], + "properties": { + "transaction_id": { "type": "string" }, + "intent": { "type": "string" }, + "value_at_risk": { "type": "string" }, + "requested_action": { + "type": "string", + "enum": ["accept_task", "spend_budget", "buy_service", "submit_delivery", "release_payment", "escalate_dispute", "public_release", "other"] + }, + "deadline": { "type": "string" } + } + }, + "actor": { + "type": "object", + "properties": { + "role": { "type": "string", "enum": ["buyer_agent", "seller_agent", "evaluator_agent", "human_buyer", "human_seller", "marketplace"] }, + "goal": { "type": "string" } + } + }, + "counterparty": { + "type": "object", + "properties": { + "role": { "type": "string" }, + "claim": { "type": "string" } + } + }, + "evidence": { + "type": "object", + "properties": { + "task_scope": { "type": "string" }, + "artifacts": { "type": "array", "items": { "type": "string" } }, + "validation": { "type": "array", "items": { "type": "string" } }, + "prior_messages": { "type": "array", "items": { "type": "string" } }, + "delivery_summary": { "type": "string" } + } + }, + "constraints": { + "type": "object", + "properties": { + "allowed_actions": { "type": "array", "items": { "type": "string" } }, + "forbidden_actions": { "type": "array", "items": { "type": "string" } }, + "budget_limit": { "type": "string" }, + "hard_gates": { "type": "array", "items": { "type": "string" } } + } + } + } +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/build-cn-demo-video.py b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/build-cn-demo-video.py new file mode 100644 index 00000000..b94de773 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/build-cn-demo-video.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +"""Build ≤60s Chinese tip-knife demo — visual cards, not PPT bullets.""" + +from __future__ import annotations + +import math +import os +import subprocess +import wave +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "research" / "demo-video-cn" +SLIDES_DIR = OUT_DIR / "slides" +AUDIO_DIR = OUT_DIR / "audio" +FINAL = OUT_DIR / "leo-labs-okxai-demo-zh.mp4" + +DEFAULT_VOICE = "zh-CN-YunyangNeural" +FALLBACK_VOICE = "zh-CN-XiaoxiaoNeural" +DEFAULT_RATE = "+10%" +VOICE = os.environ.get("VOICE", DEFAULT_VOICE) +RATE = os.environ.get("RATE", DEFAULT_RATE) +EDGE_RETRIES = 2 + +W, H = 1280, 720 + +# scene: min_secs, voiceover, kind, payload +SCENES = [ + ( + 5, + "别人晒一张收益截图,先别抄。Agent 调一次,先过闸。", + "hook", + { + "kicker": "Leo Labs · OKX.AI #3977", + "headline": "先别抄那张晒单", + "sub": "按次付费数据闸 · 真用,不是交差", + }, + ), + ( + 9, + "第一刀,晒单验真。排行榜、持仓、现金回流对账。对不上就标不完整,别拿截图当真理。", + "pnl", + {}, + ), + ( + 8, + "第二刀,同场矩阵。同一场比赛的盘口放一屏:价差、流动性、互相打架的价格,一眼看穿。", + "matrix", + {}, + ), + ( + 8, + "第三刀,决策卡。下单前只给三个结果:跳过、观望、人工复核。过关不等于买点,也不代下单。", + "decision", + {}, + ), + ( + 7, + "JSON 进,结构化结论出。边缘立刻履约,笔记本不必一直开着。给 Agent 用的真闸门。", + "close", + { + "kicker": "api.leolabs.me", + "headline": "三刀就够用", + "lines": ["晒单验真", "同场矩阵", "决策卡"], + "tag": "#OKXAI", + }, + ), +] + + +def find_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: + candidates = [ + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/STHeiti Medium.ttc", + "/System/Library/Fonts/Hiragino Sans GB.ttc", + "/Library/Fonts/Arial Unicode.ttf", + ] + for path in candidates: + p = Path(path) + if not p.exists(): + continue + for index in ((0, 1, 2) if bold else (0, 1)): + try: + return ImageFont.truetype(str(p), size=size, index=index) + except Exception: + continue + return ImageFont.load_default() + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +def gradient_bg(c1=(6, 8, 18), c2=(18, 12, 48), c3=(8, 40, 52)) -> Image.Image: + img = Image.new("RGB", (W, H)) + px = img.load() + for y in range(H): + ty = y / (H - 1) + for x in range(W): + tx = x / (W - 1) + r = int(lerp(lerp(c1[0], c2[0], tx), c3[0], ty)) + g = int(lerp(lerp(c1[1], c2[1], tx), c3[1], ty)) + b = int(lerp(lerp(c1[2], c2[2], tx), c3[2], ty)) + # soft vignette + dx = (tx - 0.5) * 2 + dy = (ty - 0.45) * 2 + vig = max(0.55, 1 - 0.35 * (dx * dx + dy * dy)) + px[x, y] = (min(255, int(r * vig)), min(255, int(g * vig)), min(255, int(b * vig))) + # glow orbs + overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + od = ImageDraw.Draw(overlay) + od.ellipse([820, -80, 1280, 380], fill=(0, 200, 255, 38)) + od.ellipse([-120, 420, 420, 900], fill=(120, 60, 255, 40)) + od.ellipse([500, 500, 900, 820], fill=(0, 255, 160, 22)) + overlay = overlay.filter(ImageFilter.GaussianBlur(48)) + return Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB") + + +def rounded_rect(draw: ImageDraw.ImageDraw, box, radius, fill, outline=None, width=2): + draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) + + +def draw_badge(draw, xy, text, fill, font): + x, y = xy + pad_x, pad_y = 14, 8 + bbox = draw.textbbox((0, 0), text, font=font) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + rounded_rect(draw, [x, y, x + tw + pad_x * 2, y + th + pad_y * 2], 18, fill) + draw.text((x + pad_x, y + pad_y - 1), text, font=font, fill=(8, 12, 20)) + + +def draw_hook(payload: dict) -> Image.Image: + img = gradient_bg() + draw = ImageDraw.Draw(img) + font_k = find_font(22) + font_h = find_font(64, bold=True) + font_s = find_font(28) + font_card = find_font(26) + font_small = find_font(20) + + draw.text((56, 40), payload["kicker"], font=font_k, fill=(140, 160, 190)) + draw.text((56, 90), payload["headline"], font=font_h, fill=(245, 248, 255)) + draw.text((56, 175), payload["sub"], font=font_s, fill=(120, 220, 255)) + + # left fake screenshot card + rounded_rect(draw, [56, 260, 600, 620], 24, (20, 24, 36), (80, 90, 120), 2) + draw.text((88, 290), "晒单截图(假)", font=font_card, fill=(180, 190, 210)) + draw.text((88, 350), "+128.4% 7D", font=find_font(48, True), fill=(90, 255, 170)) + draw.text((88, 420), "看起来很猛", font=font_s, fill=(160, 170, 190)) + draw.text((88, 470), "抄之前先过闸 →", font=font_small, fill=(255, 180, 80)) + + # right audit card + rounded_rect(draw, [660, 260, 1224, 620], 24, (18, 28, 40), (0, 220, 180), 3) + draw_badge(draw, (692, 290), "验真结果", (0, 230, 180), font_small) + draw.text((692, 360), "LB vs 回流", font=font_card, fill=(220, 230, 245)) + draw.text((692, 420), "pagination_incomplete", font=find_font(32, True), fill=(255, 120, 120)) + draw.text((692, 480), "先别 trust_for_copy", font=font_s, fill=(255, 200, 120)) + draw.text((692, 540), "有用不靠喊单", font=font_small, fill=(140, 200, 255)) + return img + + +def draw_pnl(_: dict) -> Image.Image: + img = gradient_bg((8, 10, 20), (10, 30, 40), (20, 10, 36)) + draw = ImageDraw.Draw(img) + font_h = find_font(52, bold=True) + font_b = find_font(26) + font_n = find_font(36, True) + font_s = find_font(20) + + draw_badge(draw, (56, 40), "01 晒单验真", (0, 230, 180), font_s) + draw.text((56, 100), "别人晒成绩单,你先对账", font=font_h, fill=(245, 248, 255)) + + cards = [ + ("Leaderboard", "+$12.0k", (90, 210, 255)), + ("Positions cashPnL", "+$9.4k", (255, 200, 90)), + ("Full cashflow", "incomplete", (255, 110, 120)), + ] + x0 = 56 + for title, value, color in cards: + rounded_rect(draw, [x0, 230, x0 + 370, 520], 22, (16, 22, 34), color, 3) + draw.text((x0 + 28, 270), title, font=font_b, fill=(170, 180, 200)) + draw.text((x0 + 28, 360), value, font=font_n, fill=color) + draw.text((x0 + 28, 440), "公开 API · 只读", font=font_s, fill=(120, 130, 150)) + x0 += 400 + + draw.text((56, 580), "Agent 调用 /pm-pnl-audit → 结构化 divergence + action", font=font_b, fill=(160, 220, 255)) + return img + + +def draw_matrix(_: dict) -> Image.Image: + img = gradient_bg((10, 8, 24), (30, 12, 50), (8, 28, 40)) + draw = ImageDraw.Draw(img) + font_h = find_font(52, bold=True) + font_b = find_font(24) + font_s = find_font(20) + font_cell = find_font(22) + + draw_badge(draw, (56, 40), "02 同场矩阵", (160, 120, 255), font_s) + draw.text((56, 100), "同一场盘口,一屏拆穿", font=font_h, fill=(245, 248, 255)) + + # matrix grid mock + rounded_rect(draw, [56, 210, 1224, 560], 22, (14, 18, 30), (120, 100, 255), 2) + headers = ["市场", "价", "价差", "流动性", "状态"] + rows = [ + ("Moneyline Home", "0.48", "2.1%", "$82k", "OK"), + ("Spread -0.5", "0.51", "6.8%", "$11k", "宽"), + ("Totals 2.5", "0.44", "1.4%", "$60k", "OK"), + ("BTTS Yes", "0.57", "9.2%", "$4k", "硬闸"), + ] + xs = [80, 420, 620, 820, 1040] + for i, hname in enumerate(headers): + draw.text((xs[i], 240), hname, font=font_s, fill=(140, 150, 180)) + for r, row in enumerate(rows): + y = 300 + r * 55 + color = (255, 120, 120) if row[4] in ("宽", "硬闸") else (120, 230, 170) + for i, cell in enumerate(row): + draw.text((xs[i], y), cell, font=font_cell, fill=color if i == 4 else (230, 235, 245)) + + draw.text((56, 600), "缺组 / 赛程未核 → hard_veto,不装懂", font=font_b, fill=(200, 180, 255)) + return img + + +def draw_decision(_: dict) -> Image.Image: + img = gradient_bg((8, 14, 28), (12, 40, 36), (28, 12, 40)) + draw = ImageDraw.Draw(img) + font_h = find_font(52, bold=True) + font_b = find_font(26) + font_s = find_font(20) + font_big = find_font(40, True) + + draw_badge(draw, (56, 40), "03 决策卡", (255, 200, 80), font_s) + draw.text((56, 100), "下单前只问三件事", font=font_h, fill=(245, 248, 255)) + + opts = [ + ("SKIP", "直接跳过", (255, 100, 110), "价差太宽 / 硬闸"), + ("WATCH", "先观望", (255, 200, 80), "可看不可冲"), + ("REVIEW", "人工复核", (80, 220, 160), "eligible ≠ 买点"), + ] + x0 = 56 + for title, sub, color, note in opts: + rounded_rect(draw, [x0, 230, x0 + 370, 520], 22, (16, 22, 34), color, 3) + draw.text((x0 + 28, 280), title, font=font_big, fill=color) + draw.text((x0 + 28, 360), sub, font=font_b, fill=(230, 235, 245)) + draw.text((x0 + 28, 430), note, font=font_s, fill=(150, 160, 180)) + x0 += 400 + + draw.text((56, 580), "可带 size → 算 shares · 无私钥 · 不下单", font=font_b, fill=(255, 220, 140)) + return img + + +def draw_close(payload: dict) -> Image.Image: + img = gradient_bg((6, 10, 22), (20, 16, 50), (6, 36, 40)) + draw = ImageDraw.Draw(img) + font_k = find_font(22) + font_h = find_font(58, bold=True) + font_b = find_font(32, True) + font_s = find_font(24) + + draw.text((56, 50), payload["kicker"], font=font_k, fill=(140, 160, 190)) + draw.text((56, 110), payload["headline"], font=font_h, fill=(245, 248, 255)) + + y = 230 + for i, line in enumerate(payload["lines"], 1): + rounded_rect(draw, [56, y, 700, y + 78], 18, (18, 24, 38), (0, 210, 180), 2) + draw.text((84, y + 20), f"{i}. {line}", font=font_b, fill=(230, 240, 255)) + y += 100 + + rounded_rect(draw, [760, 230, 1224, 520], 24, (12, 28, 36), (0, 200, 255), 3) + draw.text((800, 280), "Agent 真能用", font=font_b, fill=(120, 230, 255)) + draw.text((800, 350), "会过期的信息", font=font_s, fill=(200, 210, 220)) + draw.text((800, 400), "才值得按次付费", font=font_s, fill=(200, 210, 220)) + draw.text((800, 470), payload["tag"], font=find_font(36, True), fill=(255, 220, 100)) + + draw.text((56, 620), "不是为了黑客松交差 · 是日常 Agent 工作流里的闸", font=font_s, fill=(160, 180, 200)) + return img + + +DRAWERS = { + "hook": draw_hook, + "pnl": draw_pnl, + "matrix": draw_matrix, + "decision": draw_decision, + "close": draw_close, +} + + +def wav_duration(path: Path) -> float: + with wave.open(str(path), "rb") as wf: + return wf.getnframes() / float(wf.getframerate()) + + +def edge_tts_to_mp3(text: str, mp3_path: Path, voice: str) -> None: + subprocess.run( + [ + "edge-tts", + "--voice", voice, + "--rate", RATE, + "--text", text, + "--write-media", str(mp3_path), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + + +def audio_to_wav(audio_path: Path, wav_path: Path) -> None: + subprocess.run( + [ + "ffmpeg", "-y", "-i", str(audio_path), + "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "1", + str(wav_path), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + +def say_tingting_to_wav(text: str, wav_path: Path) -> str: + aiff_path = wav_path.with_suffix(".aiff") + subprocess.run( + ["say", "-v", "Tingting", "-r", "185", "-o", str(aiff_path), text], + check=True, + ) + audio_to_wav(aiff_path, wav_path) + return "Tingting" + + +def narration_to_wav(text: str, mp3_path: Path, wav_path: Path) -> str: + voices = [VOICE] + if VOICE != FALLBACK_VOICE: + voices.append(FALLBACK_VOICE) + last_error: Exception | None = None + for voice in voices: + for attempt in range(1, EDGE_RETRIES + 1): + try: + edge_tts_to_mp3(text, mp3_path, voice) + audio_to_wav(mp3_path, wav_path) + return voice + except (OSError, subprocess.CalledProcessError) as exc: + last_error = exc + detail = getattr(exc, "stderr", "") or str(exc) + detail = detail.strip().splitlines()[-1] if detail.strip() else str(exc) + print(f"WARN: edge-tts failed with {voice} attempt {attempt}/{EDGE_RETRIES}: {detail}") + print(f"WARN: falling back to Tingting: {last_error}") + return say_tingting_to_wav(text, wav_path) + + +def main() -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + SLIDES_DIR.mkdir(parents=True, exist_ok=True) + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + + concat_parts: list[Path] = [] + total = len(SCENES) + voices_used: set[str] = set() + print(f"TTS default: voice={VOICE} rate={RATE}") + + for i, (min_secs, line, kind, payload) in enumerate(SCENES, start=1): + slide_path = SLIDES_DIR / f"slide-{i:02d}.png" + DRAWERS[kind](payload).save(slide_path) + + mp3 = AUDIO_DIR / f"line-{i:02d}.mp3" + wav = AUDIO_DIR / f"line-{i:02d}.wav" + voice_used = narration_to_wav(line, mp3, wav) + voices_used.add(voice_used) + audio_secs = wav_duration(wav) + duration = max(float(min_secs), audio_secs + 0.35) + + part = OUT_DIR / f"part-{i:02d}.mp4" + # slight zoom ken-burns for less static feel + subprocess.run( + [ + "ffmpeg", "-y", + "-loop", "1", "-i", str(slide_path), + "-i", str(wav), + "-filter_complex", + f"[0:v]scale=1350:760,zoompan=z='min(1.08,1+0.0008*on)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s={W}x{H}:fps=30,format=yuv420p[v]", + "-map", "[v]", "-map", "1:a", + "-c:v", "libx264", "-preset", "veryfast", "-crf", "18", + "-c:a", "aac", "-b:a", "160k", + "-t", f"{duration:.2f}", + "-shortest", + str(part), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + concat_parts.append(part) + print(f"scene {i}/{total}: {duration:.1f}s — {kind} — {voice_used}") + + list_file = OUT_DIR / "concat.txt" + list_file.write_text("".join(f"file '{p.name}'\n" for p in concat_parts), encoding="utf-8") + + subprocess.run( + [ + "ffmpeg", "-y", "-f", "concat", "-safe", "0", + "-i", str(list_file), + "-c", "copy", + str(FINAL), + ], + check=True, + cwd=str(OUT_DIR), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + probe = subprocess.run( + [ + "ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", str(FINAL), + ], + check=True, + capture_output=True, + text=True, + ) + dur = float(probe.stdout.strip()) + print(f"\nDONE: {FINAL}") + print(f"duration: {dur:.1f}s") + print(f"voices: {', '.join(sorted(voices_used))}") + if dur > 90: + print("WARN: over 90s") + + +if __name__ == "__main__": + main() diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/check-upstream-contracts.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/check-upstream-contracts.mjs new file mode 100644 index 00000000..42fbe3cb --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/check-upstream-contracts.mjs @@ -0,0 +1,210 @@ +#!/usr/bin/env node +/** + * Upstream contract smoke check. + * + * Why this exists: on 2026-07-30 an audit of the listed services found that only 3 of + * 38 modules carried any note about the upstream response shape they depend on. Every + * other service reads fields like `redeemable`, `oneDayPriceChange` or `outcomePrices` + * on faith. When an upstream renames or drops one of those, nothing throws — the field + * reads as undefined, flows through a `?? 0` or a comparison that silently goes false, + * and the service keeps answering with a confident but wrong verdict. Two of the bugs + * fixed that day were exactly this failure mode wearing different clothes. + * + * So this is not a unit test. It is a live probe that asks each upstream for a real + * response and asserts that the fields the code actually reads are still there. It is + * deliberately kept out of `npm test`, which must stay offline and deterministic; run + * it on demand, before a release, or when a service starts returning something odd. + * + * node scripts/check-upstream-contracts.mjs # all sources + * node scripts/check-upstream-contracts.mjs --json # machine-readable + * node scripts/check-upstream-contracts.mjs --only polymarket + * + * Exit code is 1 if any required field is missing, so it can gate a release. + * A network failure is reported as `unreachable`, not as a contract break — those are + * different problems and conflating them would make the check untrustworthy. + */ + +const TIMEOUT_MS = 20000; + +/** Fields each source must still provide, taken from what src/*.mjs actually reads. */ +const CONTRACTS = [ + { + id: 'gamma_markets', + label: 'Polymarket Gamma /markets', + url: 'https://gamma-api.polymarket.com/markets?closed=false&active=true&limit=3&order=volume24hr&ascending=false', + consumers: ['worldcup-smart-money-live', 'crypto-market-regime', 'event-price-divergence'], + pick: (body) => (Array.isArray(body) ? body[0] : null), + required: ['conditionId', 'question', 'slug', 'outcomes', 'outcomePrices', 'volume24hr', 'active', 'closed'], + optional: ['enableOrderBook', 'oneDayPriceChange', 'liquidity', 'bestBid', 'bestAsk', 'endDate'] + }, + { + id: 'gamma_events', + label: 'Polymarket Gamma /events', + url: 'https://gamma-api.polymarket.com/events?closed=false&active=true&limit=3&order=volume24hr&ascending=false', + consumers: ['worldcup-smart-money-live', 'pm-event-readout'], + pick: (body) => (Array.isArray(body) ? body[0] : null), + required: ['slug', 'markets'], + optional: ['closed', 'title', 'id'] + }, + { + id: 'gamma_public_search', + label: 'Polymarket Gamma /public-search', + url: 'https://gamma-api.polymarket.com/public-search?q=bitcoin&events_status=active&limit_per_type=3', + consumers: ['crypto-market-regime', 'event-price-divergence', 'worldcup-smart-money-live'], + pick: (body) => (Array.isArray(body?.events) ? body.events[0] : null), + required: ['markets'], + optional: ['closed', 'slug', 'title'] + }, + { + id: 'data_api_positions', + label: 'Polymarket Data API /positions', + // A wallet with settled rows, so `redeemable` is actually exercised. Public data. + url: 'https://data-api.polymarket.com/positions?user=0x63ce342161250d705dc0b16df89036c8e5f9ba9a&limit=5&sizeThreshold=0', + consumers: ['pm-brier', 'pm-profile', 'pm-pnl-audit', 'pm-wallet-report'], + pick: (body) => (Array.isArray(body) ? body[0] : null), + // redeemable drives the whole pm-brier sample; losing it silently would make every + // wallet look like it has no settled history. + required: ['redeemable', 'avgPrice', 'curPrice', 'currentValue', 'size', 'conditionId'], + optional: ['title', 'slug', 'outcome', 'cashPnl', 'realizedPnl'] + }, + { + id: 'lb_api_profit', + label: 'Polymarket Leaderboard /profit', + url: 'https://lb-api.polymarket.com/profit?window=7d&limit=3', + consumers: ['pm-brier', 'pm-profile', 'worldcup-smart-money-live'], + pick: (body) => (Array.isArray(body) ? body[0] : null), + required: ['proxyWallet', 'amount'], + optional: ['name', 'pseudonym'] + }, + { + id: 'okx_ticker', + label: 'OKX /market/ticker', + url: 'https://www.okx.com/api/v5/market/ticker?instId=BTC-USDT', + consumers: ['crypto-market-regime'], + pick: (body) => (body?.code === '0' ? body?.data?.[0] : null), + required: ['last', 'open24h'], + optional: ['instId', 'vol24h'] + }, + { + id: 'okx_funding', + label: 'OKX /public/funding-rate', + url: 'https://www.okx.com/api/v5/public/funding-rate?instId=BTC-USDT-SWAP', + consumers: ['crypto-market-regime'], + pick: (body) => (body?.code === '0' ? body?.data?.[0] : null), + required: ['fundingRate'], + optional: ['premium', 'nextFundingRate'] + }, + { + id: 'okx_open_interest', + label: 'OKX /public/open-interest', + // Noted in crypto-market-regime: /api/v5/market/open-interest does NOT exist (404). + url: 'https://www.okx.com/api/v5/public/open-interest?instId=BTC-USDT-SWAP', + consumers: ['crypto-market-regime'], + pick: (body) => (body?.code === '0' ? body?.data?.[0] : null), + required: ['oiUsd'], + optional: ['oi', 'instId'] + } +]; + +const args = process.argv.slice(2); +const asJson = args.includes('--json'); +const onlyIdx = args.indexOf('--only'); +const only = onlyIdx >= 0 ? args[onlyIdx + 1] : null; + +async function fetchJson(url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return await res.json(); + } finally { + clearTimeout(timer); + } +} + +async function checkOne(contract) { + const row = { + id: contract.id, + label: contract.label, + consumers: contract.consumers, + status: 'ok', + missing_required: [], + missing_optional: [], + note: null + }; + let body; + try { + body = await fetchJson(contract.url); + } catch (err) { + row.status = 'unreachable'; + row.note = err.message; + return row; + } + + const sample = contract.pick(body); + if (!sample || typeof sample !== 'object') { + // An empty list is not a contract break — the query may simply match nothing now. + row.status = 'no_sample'; + row.note = 'upstream reachable but returned no row to inspect'; + return row; + } + + row.missing_required = contract.required.filter((f) => !(f in sample)); + row.missing_optional = (contract.optional || []).filter((f) => !(f in sample)); + if (row.missing_required.length) row.status = 'contract_break'; + else if (row.missing_optional.length) row.status = 'optional_drift'; + return row; +} + +const targets = only + ? CONTRACTS.filter((c) => c.id.includes(only) || c.label.toLowerCase().includes(only.toLowerCase())) + : CONTRACTS; + +if (!targets.length) { + console.error(`no contract matches --only ${only}`); + process.exit(2); +} + +const results = []; +for (const contract of targets) { + results.push(await checkOne(contract)); +} + +const broken = results.filter((r) => r.status === 'contract_break'); +const drifted = results.filter((r) => r.status === 'optional_drift'); +const unreachable = results.filter((r) => r.status === 'unreachable' || r.status === 'no_sample'); + +if (asJson) { + console.log(JSON.stringify({ + checked_at: new Date().toISOString(), + results, + summary: { + ok: results.filter((r) => r.status === 'ok').length, + contract_break: broken.length, + optional_drift: drifted.length, + unreachable: unreachable.length + } + }, null, 2)); +} else { + for (const r of results) { + const mark = { ok: 'PASS', optional_drift: 'DRIFT', no_sample: 'SKIP', unreachable: 'SKIP', contract_break: 'FAIL' }[r.status]; + console.log(`${mark.padEnd(5)} ${r.label}`); + if (r.missing_required.length) { + console.log(` missing required: ${r.missing_required.join(', ')}`); + console.log(` consumers at risk: ${r.consumers.join(', ')}`); + } + if (r.missing_optional.length) { + console.log(` missing optional: ${r.missing_optional.join(', ')}`); + } + if (r.note) console.log(` ${r.note}`); + } + console.log( + `[upstream-contracts] ok=${results.length - broken.length - drifted.length - unreachable.length} ` + + `drift=${drifted.length} break=${broken.length} skipped=${unreachable.length}` + ); +} + +// Only a missing REQUIRED field fails the run. Unreachable upstreams are a network +// problem, not a contract change, and must not be able to mask or mimic one. +process.exit(broken.length ? 1 : 0); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-asp-self-call.sh b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-asp-self-call.sh new file mode 100755 index 00000000..1173e7b2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-asp-self-call.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# OKX ASP self-call harness — free-trial POST per service (usage / demo evidence). +# Safe: read-only research endpoints, no OKX listing mutation. +set -euo pipefail + +BASE="${OKX_ASP_BASE:-https://api.leolabs.me}" +SLUG="${PM_DEMO_SLUG:-will-egypt-win-the-2026-fifa-world-cup}" +OUT_DIR="${OKX_SELF_CALL_OUT:-/tmp/okx-asp-self-call}" +mkdir -p "$OUT_DIR" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +LOG="$OUT_DIR/self-call-$STAMP.jsonl" + +post() { + local path="$1" + local body="$2" + local outfile="$OUT_DIR/$(echo "$path" | tr '/' '_')-$STAMP.json" + echo "==> POST $path" + http_code=$(curl -sS -o "$outfile" -w '%{http_code}' \ + -X POST "$BASE$path" \ + -H 'content-type: application/json' \ + -d "$body") + echo "{\"path\":\"$path\",\"http_code\":$http_code,\"file\":\"$outfile\"}" >> "$LOG" + if [[ "$http_code" != "200" ]]; then + echo "WARN: $path returned $http_code" >&2 + else + python3 -c "import json; d=json.load(open('$outfile')); print(' ', d.get('service_id'), d.get('mode'), d.get('billing',{}).get('mode','paid'))" 2>/dev/null || true + fi +} + +post '/agent-delivery-acceptance-audit' '{"task":"Self-call smoke","delivery_summary":"npm test passes on worker deploy.","artifacts":["worker/index.mjs"],"validation":["npm test"]}' +post '/token-dd-verdict' '{"asset":"ETH"}' +post '/event-price-divergence-radar' '{"asset":"bitcoin","limit":2}' +post '/pm-event-readout' "{\"slug\":\"$SLUG\"}" +post '/content-verify-claims' '{"claims":["Demo claim with 358 ASPs."],"sources":[{"text":"Scan shows 358 ASPs on marketplace."}]}' +post '/pm-trade-preflight' "{\"slug\":\"$SLUG\",\"side\":\"yes\",\"size_usd\":50}" +post '/crypto-market-regime-radar' '{"focus":"bitcoin","limit":2}' + +echo "Done. Log: $LOG" diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-batch-listing-draft.sh b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-batch-listing-draft.sh new file mode 100755 index 00000000..9471608d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-batch-listing-draft.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Batch OKX listing helper — prints onchainos commands for unlisted SKUs. +# Hard gate: run only after Agent #3977 is listed and Leo approves. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +node --input-type=module -e " +import { SERVICE_CATALOG, PENDING_OKX_LISTING_COPY, LISTED_SERVICE_PATHS } from './worker/service-catalog.mjs'; + +const base = 'https://api.leolabs.me'; +const pending = Object.entries(SERVICE_CATALOG) + .filter(([path]) => !LISTED_SERVICE_PATHS.has(path)) + .filter(([, meta]) => meta.mode === 'live_unlisted'); + +console.log('# OKX batch create draft — review before running'); +console.log('# Agent ID: 3977 · ASP: Leo Labs'); +console.log(''); + +for (const [path, meta] of pending) { + const copy = PENDING_OKX_LISTING_COPY[meta.service_id]; + if (!copy) { + console.log('# SKIP no copy:', path); + continue; + } + console.log('# ---', copy.serviceName, '---'); + console.log('onchainos agent validate-listing \\\\'); + console.log(' --service-name', JSON.stringify(copy.serviceName), '\\\\'); + console.log(' --service-description', JSON.stringify(copy.serviceDescription), '\\\\'); + console.log(' --fee', meta.fee_usdt, '\\\\'); + console.log(' --endpoint', base + path); + console.log(''); + console.log('# onchainos agent create ... && onchainos agent activate ...'); + console.log(''); +} +" diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-print-tip-knife-update.sh b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-print-tip-knife-update.sh new file mode 100755 index 00000000..f7e1b2d7 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/scripts/okx-print-tip-knife-update.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Print-only: tip-knife agent description update for #3977. +# Do NOT run while "Listing under review" unless Leo says 改简介/上. +set -euo pipefail +DESC=$(cat "$(dirname "$0")/../research/2026-07-24-tip-knife-agent-description.txt") +echo "# Proposed command (print-only):" +echo "onchainos agent update --agent-id 3977 --description $(printf %q "$DESC")" +echo +echo "# After update, typically:" +echo "# onchainos agent activate --agent-id 3977" +echo "# Then: onchainos agent get --agent-ids 3977" diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/service-spec.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/service-spec.md new file mode 100644 index 00000000..3f8a0943 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/service-spec.md @@ -0,0 +1,136 @@ +# Service Spec + +Status: draft_local_only +Service: Agent Deliverable Auditor + +## Proposed endpoint + +```text +POST /audit-agent-deliverable +``` + +## Request schema + +```json +{ + "schema_version": "0.1", + "mode": "quick | full | evaluator", + "task": { + "task_id": "string", + "buyer_goal": "string", + "surface": "repo | website | research | data | ops | other", + "allowed_actions": ["string"], + "forbidden_actions": ["string"], + "acceptance_criteria": ["string"] + }, + "delivery": { + "writeback_text": "string", + "artifact_paths": ["string"], + "changed_files": ["string"], + "validation": ["string"], + "validation_output": "string", + "rollback_plan": "string", + "hard_gates_declared": ["string"], + "next_gate": "string" + }, + "context": { + "repo_state": "clean | dirty | unknown | not_applicable", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "string" + } +} +``` + +## Response schema + +```json +{ + "schema_version": "0.1", + "verdict": "pass | needs_review | fail", + "score": 0, + "dimension_scores": { + "delivery_completeness": 0, + "validation_and_evidence": 0, + "safety_and_hard_gates": 0, + "buyer_usability": 0, + "dispute_readiness": 0 + }, + "missing": ["string"], + "risks": ["string"], + "positive_evidence": ["string"], + "questions_for_seller": ["string"], + "next_gate": "string", + "buyer_summary": "string", + "evaluator_notes": "string", + "machine_flags": ["string"] +} +``` + +## Scoring rubric + +Total: 100 points. + +### Delivery completeness - 30 + +- 8 artifact or output exists and is named. +- 6 changed files or touched surfaces are declared. +- 6 task goal and scope are understandable. +- 5 rollback or non-impact path is declared. +- 5 next gate is explicit. + +### Validation and evidence - 25 + +- 8 validation commands or checks are listed. +- 6 actual result is included, not just claimed. +- 5 deferred validation is named with reason. +- 3 evidence paths or URLs are concrete. +- 3 source / generated / state files are separated. + +### Safety and hard gates - 25 + +- 7 no unauthorized push / deploy / public publish. +- 5 no unauthorized credentials / API key / OAuth / proxy action. +- 5 no unauthorized wallet / funding / signing / transaction / staking. +- 4 no destructive cleanup or broad repo mutation. +- 4 active writer / ownership / pathspec risk is addressed when relevant. + +### Buyer usability - 10 + +- 4 verdict is clear enough for acceptance decision. +- 3 buyer-facing summary is concise. +- 3 next action is concrete. + +### Dispute readiness - 10 + +- 4 evidence can be independently inspected. +- 3 open questions are listed. +- 3 evaluator notes separate facts from judgment. + +## Verdict thresholds + +- `pass`: score >= 85 and no critical hard-gate breach. +- `needs_review`: score 65-84, or any unresolved release / buyer-decision / validation gap. +- `fail`: score < 65, missing core artifact, no validation, unclear scope, or hard-gate breach. + +Critical hard-gate breach always caps verdict at `fail`. + +## Machine flags + +Suggested flags: + +- `missing_artifact` +- `missing_validation` +- `deferred_build` +- `deferred_visual_smoke` +- `public_release_gate` +- `credentials_gate` +- `wallet_gate` +- `payment_gate` +- `writer_lock_risk` +- `dirty_state` +- `destructive_cleanup_risk` +- `scope_mismatch` +- `dispute_ready` + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/agent-budget-preflight.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/agent-budget-preflight.mjs new file mode 100644 index 00000000..18ecfd29 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/agent-budget-preflight.mjs @@ -0,0 +1,220 @@ +// Agent Budget Preflight — deterministic spend gate before an agent pays for a call. +// Productized from arc-budget-agent policy (read-only decision; no wallet / settle). + +const SERVICE_ID = 'agent_budget_preflight'; + +const STANDARD_CAVEATS = [ + 'Decision-only preflight. Does not hold funds, sign, settle, or call facilitators.', + 'Caller supplies budget state + offer; ASP returns buy / skip / reject with reasons.', + 'Not investment advice. For agent commerce safety before x402 / paid API calls.' +]; + +/** + * @param {object} input + * @param {number|string} input.budget_cap_usdt + * @param {number|string} [input.spent_usdt] + * @param {number|string} [input.held_usdt] + * @param {number|string} [input.max_per_call_usdt] + * @param {string[]} [input.allowlisted_providers] + * @param {boolean} [input.evidence_sufficient] + * @param {object} input.offer + * { provider, price_usdt, resource_url?, network?, asset?, id? } + */ +export function assessAgentBudgetPreflight(input = {}) { + const budgetCap = toUsdt(input.budget_cap_usdt ?? input.budget_cap); + const spent = toUsdt(input.spent_usdt ?? input.spent ?? 0); + const held = toUsdt(input.held_usdt ?? input.held ?? 0); + const maxPerCall = toUsdt(input.max_per_call_usdt ?? input.max_per_call ?? budgetCap); + const evidenceSufficient = Boolean(input.evidence_sufficient); + const allowlisted = normalizeProviders(input.allowlisted_providers ?? input.allowlist); + const offer = normalizeOffer(input.offer ?? input.purchase ?? input.call); + + if (budgetCap === null || budgetCap <= 0) { + throw new Error('budget_cap_usdt is required (positive number)'); + } + if (!offer) { + throw new Error('offer is required ({ provider, price_usdt, ... })'); + } + // 2026-07-30: remaining = cap - spent - held, and nothing checked the sign of the + // caller-supplied ledger. budget_cap_usdt=1 with spent_usdt=-100 reported + // remaining=101 and returned action=buy for a 50 USDT offer — a spend gate + // approving 50x its own cap. Negative ledger values are always a caller bug or an + // attempt to widen the cap; refuse them instead of arithmetically absorbing them. + if (spent === null || spent < 0) { + throw new Error('spent_usdt must be zero or positive'); + } + if (held === null || held < 0) { + throw new Error('held_usdt must be zero or positive'); + } + if (maxPerCall === null || maxPerCall <= 0) { + throw new Error('max_per_call_usdt must be a positive number when provided'); + } + + const remaining = round4(Math.max(0, budgetCap - spent - held)); + const checks = []; + + if (allowlisted.length && !allowlisted.includes(offer.provider)) { + return result({ + input: echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient), + action: 'reject_policy', + reason: 'provider_not_allowed', + remaining_usdt: remaining, + checks: [...checks, failCheck('provider_allowlist', `provider ${offer.provider} not in allowlist`)] + }); + } + checks.push(passCheck('provider_allowlist', allowlisted.length ? 'provider allowed' : 'allowlist open')); + + if (offer.price_usdt > maxPerCall + 1e-9) { + return result({ + input: echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient), + action: 'reject_budget', + reason: 'per_call_cap_exceeded', + remaining_usdt: remaining, + checks: [...checks, failCheck('per_call_cap', `price ${offer.price_usdt} > max_per_call ${maxPerCall}`)] + }); + } + checks.push(passCheck('per_call_cap', 'within max_per_call_usdt')); + + if (offer.price_usdt > remaining + 1e-9) { + return result({ + input: echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient), + action: 'reject_budget', + reason: 'total_cap_exceeded', + remaining_usdt: remaining, + checks: [...checks, failCheck('total_cap', `price ${offer.price_usdt} > remaining ${remaining}`)] + }); + } + checks.push(passCheck('total_cap', 'within remaining budget')); + + if (evidenceSufficient) { + return result({ + input: echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient), + action: 'skip_sufficient', + reason: 'evidence_already_sufficient', + remaining_usdt: remaining, + checks: [...checks, failCheck('evidence_gate', 'caller marked evidence_sufficient=true — skip paid call')] + }); + } + checks.push(passCheck('evidence_gate', 'evidence not marked sufficient')); + + return result({ + input: echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient), + action: 'buy', + reason: 'within_policy_and_budget', + remaining_usdt: remaining, + amount_usdt: offer.price_usdt, + checks: [...checks, passCheck('decision', 'buy allowed (decision-only; caller settles)')] + }); +} + +export function buildAgentBudgetPreflightFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + action: 'reject_policy', + reason: 'demo_fallback', + amount_usdt: 0, + remaining_usdt: null, + checks: [], + input: { note: 'Provide budget_cap_usdt + offer.price_usdt' }, + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Caller_executes_or_skips_payment', + source: { + method: 'rule_based_budget_preflight', + oss_lineage: 'arc-budget-agent evaluateSpendDecision (generalized, no settle)' + } + }; +} + +function result({ input, action, reason, remaining_usdt, checks, amount_usdt = 0 }) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + action, + reason, + amount_usdt, + remaining_usdt, + checks, + input, + buyer_summary_zh: buildBudgetBuyerSummaryZh(action, reason, amount_usdt, remaining_usdt), + value_loop: { + why_pay_again: 'Budget state and offer change every call; re-run before each paid x402/API spend.', + stale_after_minutes: null, + best_used_in: 'agent_spend_gate_before_payment', + paid_value_tier: 'A_repeat_workflow', + fulfillment: 'edge_on_demand_no_llm' + }, + caveats: [...STANDARD_CAVEATS], + next_gate: action === 'buy' + ? 'Caller_may_proceed_to_x402_or_paid_API' + : 'Caller_must_skip_or_revise_offer', + source: { + method: 'rule_based_budget_preflight', + oss_lineage: 'arc-budget-agent evaluateSpendDecision (generalized, no settle)' + } + }; +} + +function buildBudgetBuyerSummaryZh(action, reason, amount, remaining) { + const actionZh = { + buy: '可买(仅决策,不代付)', + skip_sufficient: '证据已够,建议跳过付费', + reject_budget: '预算不足/超单笔上限', + reject_policy: '策略拒绝' + }[action] || action; + return `预算闸门:${actionZh}(${reason})。本次报价 ${amount} USDT,剩余额度约 ${remaining} USDT。不签名、不结算。`; +} + +function echo(input, budgetCap, spent, held, maxPerCall, offer, allowlisted, evidenceSufficient) { + return { + budget_cap_usdt: budgetCap, + spent_usdt: spent, + held_usdt: held, + max_per_call_usdt: maxPerCall, + allowlisted_providers: allowlisted, + evidence_sufficient: evidenceSufficient, + offer + }; +} + +function normalizeOffer(raw) { + if (!raw || typeof raw !== 'object') return null; + const price = toUsdt(raw.price_usdt ?? raw.price ?? raw.fee_usdt); + const provider = String(raw.provider ?? raw.vendor ?? raw.asp ?? '').trim().toLowerCase(); + if (price === null || price <= 0 || !provider) return null; + return { + id: raw.id ? String(raw.id) : null, + provider, + price_usdt: price, + resource_url: raw.resource_url ?? raw.endpoint ?? raw.url ?? null, + network: raw.network ?? null, + asset: raw.asset ?? 'USDT' + }; +} + +function normalizeProviders(value) { + if (!Array.isArray(value)) return []; + return value.map((p) => String(p).trim().toLowerCase()).filter(Boolean); +} + +function toUsdt(value) { + if (value === null || value === undefined || value === '') return null; + const n = Number(value); + return Number.isFinite(n) ? round4(n) : null; +} + +function round4(n) { + return Math.round(n * 10000) / 10000; +} + +function passCheck(id, note) { + return { id, status: 'pass', note }; +} + +function failCheck(id, note) { + return { id, status: 'fail', note }; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/auditor.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/auditor.mjs new file mode 100644 index 00000000..00ac81b9 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/auditor.mjs @@ -0,0 +1,393 @@ +const SCHEMA_VERSION = '0.1'; + +const CRITICAL_GATE_PATTERNS = [ + ['credentials_gate', /\b(api key|credential|oauth|proxy|secret|token)\b/i], + ['wallet_gate', /\b(wallet|funding|signing|transaction|staking|stake|broadcast)\b/i], + ['payment_gate', /\b(payment|stripe|memberful|price|pricing|receiving address)\b/i], + ['public_release_gate', /\b(push|deploy|public publish|publish|pr)\b/i], + ['destructive_cleanup_risk', /\b(cleanup|restore|delete|remove|rm -rf|destructive)\b/i] +]; + +const RISK_PATTERNS = [ + ['deferred_build', /\b(build|npm run build)\b[\s\S]{0,40}\bdeferred\b|\bdeferred\b[\s\S]{0,40}\b(build|npm run build)\b/i], + ['deferred_visual_smoke', /\b(visual smoke|screenshot|playwright)\b[\s\S]{0,50}\bdeferred\b|\bdeferred\b[\s\S]{0,50}\b(visual smoke|screenshot|playwright)\b/i], + ['public_release_gate', /\b(push|deploy|public publish|publish|pr)\b[\s\S]{0,70}\b(approval|gate|before|forbidden|not touched|not crossed|deferred)\b|\b(approval|gate|before|forbidden|not touched|not crossed|deferred)\b[\s\S]{0,70}\b(push|deploy|public publish|publish|pr)\b/i], + ['dirty_state', /\bdirty\b/i], + ['writer_lock_risk', /\b(writer lock|active writer|repo lock|pathspec)\b/i], + ['protocol_tension', /\bprotocol tension|meta exception\b/i], + ['needs_reaudit', /\bre-?audit|re-run|rerun|route normalization\b/i], + ['buyer_decision_required', /\b(buyer decision|owner decision|leo reviews|leo approval|leo confirms|old buyer|lifetime|promise)\b/i], + ['guard_triggered', /\b(stopped_on_guard|red guard|guard triggered|red limit|returncode 2)\b/i], + ['task_failed_safely', /\b(stopped_on_guard|red guard|correct behavior|safe stop|failed safely)\b/i], + ['read_only_delivery', /\b(read-only|read only|no new project|no private file|no external state)\b/i] +]; + +export function auditDelivery(input) { + validateInput(input); + + const text = flattenText(input); + const flags = new Set(); + const missing = []; + const risks = []; + const positive = []; + const questions = []; + + for (const [flag, pattern] of RISK_PATTERNS) { + if (pattern.test(text)) flags.add(flag); + } + + if (input.context?.repo_state === 'dirty') flags.add('dirty_state'); + if (input.delivery.hard_gates_declared?.length) flags.add('hard_gates_declared'); + + const criticalBreaches = detectCriticalBreaches(input, text); + for (const breach of criticalBreaches) flags.add(breach.flag); + + const dimension_scores = { + delivery_completeness: scoreDeliveryCompleteness(input, missing, positive), + validation_and_evidence: scoreValidation(input, flags, missing, positive), + safety_and_hard_gates: scoreSafety(input, criticalBreaches, flags, risks, positive), + buyer_usability: scoreBuyerUsability(input, flags, positive), + dispute_readiness: scoreDisputeReadiness(input, questions, positive) + }; + + addDerivedFindings(input, flags, missing, risks, questions, positive); + + let score = Object.values(dimension_scores).reduce((sum, value) => sum + value, 0); + score = applyReviewCaps(score, flags); + + const hasCriticalBreach = criticalBreaches.length > 0; + const needsReviewFlags = [ + 'deferred_build', + 'deferred_visual_smoke', + 'dirty_state', + 'writer_lock_risk', + 'protocol_tension', + 'needs_reaudit', + 'buyer_decision_required', + 'public_release_gate' + ]; + const needsReview = needsReviewFlags.some((flag) => flags.has(flag)); + + let verdict = 'pass'; + if (hasCriticalBreach || score < 65) { + verdict = 'fail'; + } else if (score < 85 || needsReview) { + verdict = 'needs_review'; + } + + if ((flags.has('guard_triggered') || flags.has('task_failed_safely')) && !hasCriticalBreach && score >= 80) { + verdict = 'pass'; + flags.add('task_failed_safely'); + } + + if (input.mode === 'quick' && flags.has('read_only_delivery') && !hasCriticalBreach && score >= 80) { + verdict = 'pass'; + } + + return { + schema_version: SCHEMA_VERSION, + service_id: 'agent_delivery_acceptance_audit', + verdict, + score, + dimension_scores, + missing: unique(missing), + risks: unique(risks), + positive_evidence: unique(positive), + questions_for_seller: unique(questions), + next_gate: normalizeNextGate(input, verdict, flags), + buyer_summary: buildBuyerSummary(input, verdict, flags, missing, risks), + buyer_summary_zh: buildBuyerSummaryZh(input, verdict, flags, missing, risks), + evaluator_notes: buildEvaluatorNotes(verdict, flags, criticalBreaches), + machine_flags: Array.from(flags).sort(), + value_loop: { + why_pay_again: 'Each delivery is a new artifact set; re-run on every submit before accept/pay.', + stale_after_minutes: null, + best_used_in: 'buyer_acceptance_gate_per_task', + paid_value_tier: 'A_repeat_workflow', + fulfillment: 'edge_on_demand_no_llm' + } + }; +} + +function validateInput(input) { + const required = [ + ['task', input.task], + ['delivery', input.delivery], + ['context', input.context], + ['task.buyer_goal', input.task?.buyer_goal], + ['delivery.writeback_text', input.delivery?.writeback_text], + ['delivery.next_gate', input.delivery?.next_gate] + ]; + + const missing = required.filter(([, value]) => !value).map(([field]) => field); + if (missing.length) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +} + +function flattenText(input) { + return [ + input.task?.buyer_goal, + input.task?.surface, + ...(input.task?.allowed_actions ?? []), + ...(input.task?.forbidden_actions ?? []), + ...(input.task?.acceptance_criteria ?? []), + input.delivery?.writeback_text, + ...(input.delivery?.artifact_paths ?? []), + ...(input.delivery?.changed_files ?? []), + ...(input.delivery?.validation ?? []), + input.delivery?.validation_output, + input.delivery?.rollback_plan, + ...(input.delivery?.hard_gates_declared ?? []), + input.delivery?.next_gate, + input.context?.repo_state, + input.context?.notes + ].filter(Boolean).join('\n'); +} + +function scoreDeliveryCompleteness(input, missing, positive) { + let score = 0; + if (input.delivery.artifact_paths?.length) { + score += 8; + positive.push('Artifact or evidence path is named.'); + } else { + missing.push('artifact or evidence path'); + } + + if (Array.isArray(input.delivery.changed_files) && (input.delivery.changed_files.length > 0 || ['data', 'research', 'ops'].includes(input.task.surface))) { + score += 6; + positive.push('Changed files or touched surface are declared.'); + } else { + missing.push('changed files or touched surface declaration'); + } + + if (input.task.buyer_goal && input.delivery.writeback_text) { + score += 6; + positive.push('Task goal and writeback are understandable.'); + } + + if (input.delivery.rollback_plan) { + score += 5; + positive.push('Rollback or non-impact path is declared.'); + } else { + missing.push('rollback or non-impact plan'); + } + + if (input.delivery.next_gate) { + score += 5; + positive.push('Next gate is explicit.'); + } else { + missing.push('next gate'); + } + + return score; +} + +function scoreValidation(input, flags, missing, positive) { + let score = 0; + const validationText = `${input.delivery.validation?.join('\n') ?? ''}\n${input.delivery.validation_output ?? ''}`; + + if (input.delivery.validation?.length) { + score += 8; + positive.push('Validation checks are listed.'); + } else { + missing.push('validation checks'); + flags.add('missing_validation'); + } + + if (/\b(pass|fail|false|true|returncode|http 200|complete|deferred|stopped|clean)\b/i.test(validationText)) { + score += 6; + positive.push('Validation result is stated.'); + } else { + missing.push('actual validation result'); + } + + if (/deferred/i.test(validationText)) { + score += /because|reason|gate|avoid|before|until/i.test(validationText) ? 4 : 2; + } else { + score += 5; + } + + if (input.delivery.artifact_paths?.some((path) => path.startsWith('/') || /^https?:\/\//.test(path))) { + score += 3; + positive.push('Evidence paths are concrete.'); + } + + if (/(source|generated|state|dirty|public|artifact|changed_files|changed files)/i.test(flattenText(input))) { + score += 3; + } + + return Math.min(score, 25); +} + +function scoreSafety(input, criticalBreaches, flags, risks, positive) { + let score = 25; + + if (criticalBreaches.length) { + for (const breach of criticalBreaches) risks.push(`Possible hard-gate breach: ${breach.reason}.`); + return 0; + } + + if (input.delivery.hard_gates_declared?.length) { + positive.push('Hard gates are explicitly declared.'); + } else { + score -= 6; + risks.push('Hard gates are not explicitly declared.'); + } + + if (flags.has('dirty_state')) score -= 2; + if (flags.has('writer_lock_risk')) score -= 2; + if (flags.has('public_release_gate')) score -= 1; + + if (input.context?.payments_or_wallets_in_scope || input.context?.credentials_in_scope) { + score -= 8; + risks.push('Sensitive payment, wallet, credential, or account context is in scope.'); + } + + positive.push('No critical hard-gate breach is detected from the supplied delivery text.'); + return Math.max(0, score); +} + +function scoreBuyerUsability(input, flags, positive) { + let score = 4; + if (input.delivery.next_gate) score += 3; + if (input.task.acceptance_criteria?.length) score += 2; + if (flags.has('buyer_decision_required')) score -= 1; + if (score >= 8) positive.push('Buyer-facing decision path is mostly clear.'); + return Math.max(0, Math.min(score, 10)); +} + +function scoreDisputeReadiness(input, questions, positive) { + let score = 0; + if (input.delivery.artifact_paths?.length) score += 4; + if (input.delivery.validation?.length) score += 2; + if (input.delivery.hard_gates_declared?.length) score += 2; + if (input.delivery.next_gate) score += 1; + if (input.task.acceptance_criteria?.length) score += 1; + + if (score >= 8) { + positive.push('Evidence is reasonably dispute-ready.'); + } else { + questions.push('What extra evidence would let an evaluator independently verify this delivery?'); + } + return Math.min(score, 10); +} + +function detectCriticalBreaches(input, text) { + const declaredHardGates = (input.delivery.hard_gates_declared ?? []).join('\n'); + const declaredText = `${declaredHardGates}\n${input.task?.forbidden_actions?.join('\n') ?? ''}`; + const breaches = []; + + if (/\b(done|completed|executed|submitted|published|deployed|pushed|signed|funded|staked|deleted)\b/i.test(text)) { + for (const [flag, pattern] of CRITICAL_GATE_PATTERNS) { + if (pattern.test(text) && pattern.test(declaredText) && !/\bnot touched|not crossed|no |without|before|deferred|requires approval|approval before\b/i.test(text)) { + breaches.push({ flag, reason: flag.replaceAll('_', ' ') }); + } + } + } + + return breaches; +} + +function addDerivedFindings(input, flags, missing, risks, questions, positive) { + if (flags.has('deferred_build')) { + missing.push('release build'); + risks.push('Build is deferred, so the delivery is not release-ready.'); + } + if (flags.has('deferred_visual_smoke')) { + missing.push('visual smoke or screenshot validation'); + risks.push('Visual regression risk remains unresolved.'); + } + if (flags.has('dirty_state')) { + risks.push('Repo or delivery state is dirty and needs owner decision before release.'); + } + if (flags.has('public_release_gate')) { + risks.push('Public release gate remains open.'); + } + if (flags.has('protocol_tension')) { + risks.push('Protocol fit is unresolved and needs owner review.'); + } + if (flags.has('buyer_decision_required')) { + questions.push('What exact owner or buyer decision is needed before acceptance?'); + } + if (flags.has('guard_triggered')) { + positive.push('Guard-triggered stop is explicitly documented.'); + } + if (flags.has('read_only_delivery')) { + positive.push('Read-only boundary is explicitly documented.'); + } + if (!input.delivery.validation?.length) { + risks.push('No validation checks are supplied.'); + } +} + +function normalizeNextGate(input, verdict, flags) { + if (verdict === 'fail') { + return 'Do not accept. Request corrected delivery and hard-gate evidence before payment or release.'; + } + if (flags.has('guard_triggered')) { + return 'Accept the safety stop, then require owner approval before any continuation.'; + } + return input.delivery.next_gate; +} + +function buildBuyerSummary(input, verdict, flags, missing, risks) { + const task = input.task.task_id ?? 'this delivery'; + if (verdict === 'pass') { + if (flags.has('guard_triggered')) { + return `${task}: acceptable as a safe-stop delivery. The task did not continue because the guard worked.`; + } + return `${task}: acceptable within the declared scope. Remaining notes are not acceptance blockers.`; + } + if (verdict === 'fail') { + return `${task}: do not accept yet. ${risks[0] ?? 'A critical delivery or hard-gate issue is unresolved.'}`; + } + return `${task}: useful delivery, but needs review before acceptance. Main gap: ${missing[0] ?? risks[0] ?? 'owner decision required'}.`; +} + +function buildBuyerSummaryZh(input, verdict, flags, missing, risks) { + const task = input.task.task_id ?? '本次交付'; + if (verdict === 'pass') { + if (flags.has('guard_triggered')) { + return `${task}:可接受(安全停机)。任务因护栏正确触发而停止,不算乱交付。`; + } + return `${task}:在声明范围内可接受。剩余备注不是验收阻塞项。`; + } + if (verdict === 'fail') { + return `${task}:暂勿验收。${risks[0] ?? '存在未解的关键交付或硬闸问题。'}`; + } + return `${task}:有用但需复核后再验收。主要缺口:${missing[0] ?? risks[0] ?? '需买方决策'}。`; +} + +function buildEvaluatorNotes(verdict, flags, criticalBreaches) { + if (criticalBreaches.length) { + return `Critical hard-gate concern detected: ${criticalBreaches.map((b) => b.reason).join(', ')}. Treat as fail unless contradicted by evidence.`; + } + if (flags.has('guard_triggered')) { + return 'Separate task outcome from agent behavior: a red-stop or guard-triggered failure can be the correct delivery.'; + } + if (flags.has('read_only_delivery')) { + return 'Delivery is read-only and should be evaluated mainly on boundary clarity, observations, limitations, and next gate.'; + } + if (verdict === 'needs_review') { + return 'No critical breach is proven, but acceptance depends on missing validation, release gate, dirty state, or owner decision.'; + } + return 'No critical breach is detected from supplied evidence. Verify artifacts independently if value at risk is high.'; +} + +function applyReviewCaps(score, flags) { + if (flags.has('guard_triggered') || flags.has('task_failed_safely')) return score; + if (flags.has('needs_reaudit')) score = Math.min(score, 80); + if (flags.has('protocol_tension')) score = Math.min(score, 80); + if (flags.has('writer_lock_risk')) score = Math.min(score, 82); + if (flags.has('deferred_build')) score = Math.min(score, 84); + if (flags.has('deferred_visual_smoke')) score = Math.min(score, 84); + if (flags.has('public_release_gate')) score = Math.min(score, 84); + if (flags.has('dirty_state')) score = Math.min(score, 84); + if (flags.has('buyer_decision_required')) score = Math.min(score, 84); + return score; +} + +function unique(items) { + return Array.from(new Set(items.filter(Boolean))); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-slop-check.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-slop-check.mjs new file mode 100644 index 00000000..ecc5e8db --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-slop-check.mjs @@ -0,0 +1,127 @@ +// Content Slop Check — rule-based AI-filler / spam-pattern detector for draft text. +// No rewrite, no publish, no LLM. Public ASP surface only. + +const SERVICE_ID = 'content_slop_check'; + +const STANDARD_CAVEATS = [ + 'Rule-based slop heuristics only — not a style rewrite and not multi-model review.', + 'Does not publish, schedule, or mutate any account.', + 'High score means more filler risk; human judgment still required.' +]; + +const SLOP_PATTERNS = [ + { id: 'delve', re: /\bdelve\b/i, weight: 8, note: 'Classic LLM filler verb.' }, + { id: 'landscape', re: /\b(?:in today'?s|the) (?:digital |ever-evolving )?landscape\b/i, weight: 10, note: 'Generic landscape opener.' }, + { id: 'tapestry', re: /\btapestry\b/i, weight: 8, note: 'Ornate filler noun.' }, + { id: 'crucial', re: /\bit(?:'s| is) crucial (?:to|that)\b/i, weight: 6, note: 'Empty emphasis.' }, + { id: 'underscore', re: /\bunderscores? the (?:importance|need|fact)\b/i, weight: 7, note: 'LLM emphasis cliché.' }, + { id: 'not_only', re: /\bnot only\b[\s\S]{0,40}\bbut also\b/i, weight: 5, note: 'Boilerplate contrast.' }, + { id: 'in_conclusion', re: /\b(?:in conclusion|to summarize|in summary)\b/i, weight: 5, note: 'Essay-closing filler.' }, + { id: 'as_an_ai', re: /\bas an ai\b/i, weight: 20, note: 'Model self-reference leak.' }, + { id: 'excited_to', re: /\bi(?:'m| am) (?:excited|thrilled|delighted) to\b/i, weight: 6, note: 'Corporate enthusiasm spam.' }, + { id: 'game_changer', re: /\bgame[- ]changer\b/i, weight: 6, note: 'Hype filler.' }, + { id: 'leverage_synergy', re: /\b(?:leverage|synergy|holistic|robust)\b/i, weight: 4, note: 'Biz jargon density.' }, + { id: 'emoji_spam', re: /(?:[\u{1F300}-\u{1FAFF}].*){4,}/u, weight: 8, note: 'Heavy emoji spam.' }, + { id: 'cn_ai_filler', re: /综上所述|赋能|闭环|抓手|打通|底层逻辑|认知升级/u, weight: 7, note: '中文空话/黑话。' } +]; + +/** + * @param {object} input + * @param {string} [input.text] + * @param {string} [input.content] + * @param {string} [input.draft] + */ +export function assessContentSlopCheck(input = {}) { + const text = String(input.text ?? input.content ?? input.draft ?? '').trim(); + if (!text) { + throw new Error('text (draft content) is required'); + } + + const flags = []; + let rawScore = 0; + for (const pattern of SLOP_PATTERNS) { + if (pattern.re.test(text)) { + rawScore += pattern.weight; + flags.push({ id: pattern.id, weight: pattern.weight, note: pattern.note }); + } + } + + const words = text.split(/\s+/).filter(Boolean).length; + const sentences = text.split(/[.!?。!?]+/).filter((s) => s.trim().length > 0).length || 1; + const avgSentenceLen = words / sentences; + if (avgSentenceLen > 38) { + rawScore += 6; + flags.push({ id: 'long_sentences', weight: 6, note: `Avg sentence ~${Math.round(avgSentenceLen)} words — dense/LLM-ish.` }); + } + if (words > 0) { + const unique = new Set(text.toLowerCase().split(/\s+/).filter(Boolean)); + const diversity = unique.size / words; + if (diversity < 0.45 && words >= 40) { + rawScore += 8; + flags.push({ id: 'low_lexical_diversity', weight: 8, note: `Lexical diversity ${diversity.toFixed(2)} looks repetitive.` }); + } + } + + const slop_score_0_100 = clamp(Math.round(rawScore * 1.4), 0, 100); + const verdict = slop_score_0_100 >= 55 + ? 'sloppy' + : slop_score_0_100 >= 30 + ? 'needs_edit' + : 'clean_enough'; + + const suggested_actions = []; + if (verdict !== 'clean_enough') { + suggested_actions.push('Cut cliché openers and empty emphasis.'); + suggested_actions.push('Replace abstract claims with concrete numbers or named sources.'); + } + if (flags.some((f) => f.id === 'as_an_ai')) { + suggested_actions.push('Remove model self-reference before any public post.'); + } + if (flags.some((f) => f.id === 'cn_ai_filler')) { + suggested_actions.push('删掉空话黑话,改成可核验事实句。'); + } + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + char_count: text.length, + word_count: words, + sentence_count: sentences + }, + verdict, + slop_score_0_100, + slop_flags: flags, + readability: { + avg_sentence_words: Math.round(avgSentenceLen * 10) / 10 + }, + suggested_actions, + caveats: [...STANDARD_CAVEATS], + next_gate: 'Human_edit_then_content_verify_claims_before_publish', + source: { method: 'rule_based_slop_heuristics' } + }; +} + +export function buildContentSlopCheckFallback() { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { char_count: 0, word_count: 0, sentence_count: 0 }, + verdict: 'needs_edit', + slop_score_0_100: 40, + slop_flags: [{ id: 'demo', weight: 0, note: 'Provide text to score.' }], + readability: { avg_sentence_words: 0 }, + suggested_actions: ['Pass draft text in JSON field text.'], + caveats: [...STANDARD_CAVEATS], + next_gate: 'Human_edit_then_content_verify_claims_before_publish', + source: { method: 'static_fallback' } + }; +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-verify-claims.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-verify-claims.mjs new file mode 100644 index 00000000..89325176 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/content-verify-claims.mjs @@ -0,0 +1,200 @@ +// Content Verify Claims (content_verify_claims) — rule-based pre-publish claim check. +// Compares caller-supplied claims against caller-supplied source excerpts. +// Not LLM cross-model review; use leo-route review for full content-verify skill. + +const SERVICE_ID = 'content_verify_claims'; + +const STANDARD_CAVEATS = [ + 'Rule-based overlap check only — not multi-model content-verify or web fetch.', + 'Caller must supply source excerpts; URLs alone are not fetched in this endpoint.', + 'Not a substitute for human review before publish.' +]; + +export function assessContentVerifyClaims(input = {}) { + const claims = normalizeStringArray(input.claims ?? input.claim); + const sources = normalizeSources(input.sources ?? input.source); + + if (!claims.length) { + throw new Error('claims[] is required (one or more strings to verify).'); + } + if (!sources.length) { + throw new Error('sources[] is required (text excerpts and/or url labels; text is used for matching).'); + } + + const corpus = buildCorpus(sources); + const supported = []; + const unsupported = []; + const needs_review = []; + const conflicts = []; + + for (const claim of claims) { + const result = evaluateClaim(claim, corpus, sources); + if (result.status === 'supported') supported.push(result); + else if (result.status === 'unsupported') unsupported.push(result); + else needs_review.push(result); + } + + detectNumericConflicts(claims, corpus, conflicts); + + const verdict = verdictFrom(supported, unsupported, needs_review, conflicts); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + claim_count: claims.length, + source_count: sources.length + }, + verdict, + consensus: buildConsensus(verdict, supported, unsupported, needs_review), + supported, + unsupported, + needs_review, + conflicts, + caveats: [...STANDARD_CAVEATS], + next_gate: 'Run_leo-route_review_or_human_check_before_publish', + source: { method: 'rule_based_text_overlap_and_numeric_match' } + }; +} + +export function buildContentVerifyClaimsFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { claim_count: 0, source_count: 0 }, + verdict: 'needs_review', + consensus: 'Demo fallback — provide claims[] and sources[] with text excerpts.', + supported: [], + unsupported: [], + needs_review: [], + conflicts: [], + caveats: [...STANDARD_CAVEATS], + next_gate: 'Run_leo-route_review_or_human_check_before_publish', + source: { method: 'static_fallback' } + }; +} + +function normalizeStringArray(value) { + const items = Array.isArray(value) ? value : [value]; + return items.map((item) => String(item).trim()).filter((item) => item.length > 0); +} + +function normalizeSources(value) { + const items = Array.isArray(value) ? value : [value]; + return items.map((item, index) => { + if (typeof item === 'string') { + return { id: `source_${index + 1}`, url: null, text: item.trim() }; + } + if (item && typeof item === 'object') { + return { + id: String(item.id ?? `source_${index + 1}`), + url: item.url ? String(item.url) : null, + text: String(item.text ?? item.excerpt ?? item.content ?? '').trim() + }; + } + return { id: `source_${index + 1}`, url: null, text: '' }; + }).filter((s) => s.text.length > 0 || s.url); +} + +function buildCorpus(sources) { + return sources.map((s) => ({ + id: s.id, + text: s.text.toLowerCase(), + numbers: extractNumbers(s.text) + })); +} + +function evaluateClaim(claim, corpus, sources) { + const claimLower = claim.toLowerCase(); + const claimNumbers = extractNumbers(claim); + const claimTokens = significantTokens(claimLower); + + let best = { score: 0, source_id: null }; + for (const entry of corpus) { + if (!entry.text && !entry.numbers.length) continue; + let score = 0; + if (claimNumbers.length) { + const matched = claimNumbers.filter((n) => entry.numbers.includes(n)); + score += matched.length / claimNumbers.length * 0.6; + } + const tokenHits = claimTokens.filter((t) => entry.text.includes(t)).length; + if (claimTokens.length) score += tokenHits / claimTokens.length * 0.4; + if (score > best.score) best = { score, source_id: entry.id }; + } + + const entry = { + claim, + best_source_id: best.source_id, + match_score: round2(best.score) + }; + + if (claimNumbers.length) { + const anyNumberMatch = corpus.some((c) => + claimNumbers.every((n) => c.numbers.includes(n))); + if (!anyNumberMatch) { + return { ...entry, status: 'unsupported', reason: 'Numeric tokens in claim not found in any source excerpt.' }; + } + } + + if (best.score >= 0.55) { + return { ...entry, status: 'supported', reason: 'Claim overlaps source text/numbers above threshold.' }; + } + if (best.score >= 0.25) { + return { ...entry, status: 'needs_review', reason: 'Partial overlap — manual review recommended.' }; + } + return { ...entry, status: 'unsupported', reason: 'Insufficient overlap with supplied source excerpts.' }; +} + +function detectNumericConflicts(claims, corpus, conflicts) { + const allSourceNumbers = new Set(corpus.flatMap((c) => c.numbers)); + for (const claim of claims) { + const nums = extractNumbers(claim); + for (const n of nums) { + if (allSourceNumbers.size > 0 && !allSourceNumbers.has(n)) { + conflicts.push({ + type: 'claim_number_not_in_sources', + value: n, + claim + }); + } + } + } +} + +function verdictFrom(supported, unsupported, needs_review, conflicts) { + if (conflicts.length && unsupported.length) return 'fail'; + if (unsupported.length && !supported.length) return 'fail'; + if (unsupported.length || needs_review.length || conflicts.length) return 'needs_review'; + return 'pass'; +} + +function buildConsensus(verdict, supported, unsupported, needs_review = []) { + if (verdict === 'pass') { + return `All ${supported.length} claim(s) overlap supplied source excerpts above threshold.`; + } + if (verdict === 'fail') { + return `${unsupported.length} unsupported claim(s); review before publish.`; + } + return `Mixed support: ${supported.length} ok, ${unsupported.length} unsupported, ${needs_review.length} need review.`; +} + +function extractNumbers(text) { + const matches = String(text).match(/\d+(?:\.\d+)?%?/g) ?? []; + return [...new Set(matches.map((m) => m.replace(/%$/, '')))]; +} + +function significantTokens(text) { + return [...new Set( + text.split(/[^a-z0-9\u4e00-\u9fff]+/i) + .map((t) => t.trim()) + .filter((t) => t.length >= 4) + )].slice(0, 12); +} + +function round2(value) { + return Math.round(value * 100) / 100; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/crypto-market-regime.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/crypto-market-regime.mjs new file mode 100644 index 00000000..559309b4 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/crypto-market-regime.mjs @@ -0,0 +1,568 @@ +// Crypto Market Regime Radar (crypto_market_regime_radar). +// Decision-layer service: blends OKX spot momentum, perp funding/premium and +// Polymarket event-probability drift into one explainable regime call +// (risk_on / risk_off / neutral / mixed) with a 0-100 score. Every dimension's +// weight and contribution is spelled out in `dimensions` + `rationale` — no +// black box. +// +// Public endpoints used (no API key required), response shapes verified 2026-07-05: +// - OKX: https://www.okx.com/api/v5/market/ticker?instId=BTC-USDT +// -> { code: '0', data: [{ last: '63105.3', open24h: '62565.9', ... }] } +// - OKX: https://www.okx.com/api/v5/public/funding-rate?instId=BTC-USDT-SWAP +// -> { code: '0', data: [{ fundingRate: '0.0001', premium: '-0.00045', ... }] } +// - OKX: https://www.okx.com/api/v5/public/open-interest?instId=BTC-USDT-SWAP +// -> { code: '0', data: [{ oiUsd: '1922638943.46', ... }] } +// (NOTE: /api/v5/market/open-interest does NOT exist — returns 404, verified 2026-07-05) +// - Gamma: https://gamma-api.polymarket.com/public-search?q=&events_status=active&limit_per_type=10 +// -> { events: [{ closed, markets: [{ conditionId, question, outcomes, +// outcomePrices, oneDayPriceChange, volume24hr, active, closed }] }] } +// (oneDayPriceChange missing on quiet markets — those are skipped, never estimated) + +import { classifyAssetStance } from './market-stance.mjs'; + +const SERVICE_ID = 'crypto_market_regime_radar'; +const OKX_BASE = 'https://www.okx.com'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; + +const FETCH_TIMEOUT_MS = 8000; +const PM_MARKETS_PER_ASSET = 8; +const MIN_PM_VOLUME_24H = 500; + +const ASSETS = { + bitcoin: { search: 'bitcoin', spot: 'BTC-USDT', swap: 'BTC-USDT-SWAP', aliases: ['btc', 'xbt'] }, + ethereum: { search: 'ethereum', spot: 'ETH-USDT', swap: 'ETH-USDT-SWAP', aliases: ['eth', 'ether'] }, + solana: { search: 'solana', spot: 'SOL-USDT', swap: 'SOL-USDT-SWAP', aliases: ['sol'] } +}; +const DEFAULT_SCAN = ['bitcoin', 'ethereum', 'solana']; + +// Explainable weighting rules. Each dimension maps a raw reading to a +// normalized [-1, +1] direction score; contribution = 50 * weight% * normalized. +// Normalization scales (the "full conviction" magnitudes) are declared here so +// they show up in `source.method` and can be audited by the buyer. +const DIMENSIONS = { + spot_momentum_24h: { weight_pct: 40, full_scale: '±3% avg 24h spot move' }, + perp_funding_rate: { weight_pct: 20, full_scale: '±0.05% deviation from 0.01% baseline funding' }, + perp_premium: { weight_pct: 15, full_scale: '±0.10% perp premium vs index' }, + polymarket_event_sentiment: { weight_pct: 25, full_scale: '±5 pts volume-weighted 24h bullish-probability drift' } +}; + +const STANDARD_CAVEATS = [ + 'Not financial advice. Data-only research signal for downstream agents; final trading decisions and risk limits stay with the caller.', + 'No wallet custody, no user funds, no trade execution, no order routing.', + 'Regime call is a heuristic weighted blend of 24h snapshots (spot momentum, funding, perp premium, Polymarket probability drift); it can be wrong, stale, or dominated by asset-specific events.', + 'Open interest is reported as context only — a single OI snapshot has no direction and does not enter the score.' +]; + +/** + * Build the full live response payload. + * Throws when every upstream lookup fails so callers can decide how to degrade. + */ +export async function assessCryptoMarketRegimeLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInteger(input.limit, 1, 10, 5); + const assetKeys = resolveAssetKeys(input.focus ?? input.asset); + + const assets = await Promise.all(assetKeys.map(async (key) => { + const spec = ASSETS[key] ?? { + search: key, + spot: `${key.toUpperCase()}-USDT`, + swap: `${key.toUpperCase()}-USDT-SWAP` + }; + const [ticker, funding, openInterest, pmMarkets] = await Promise.all([ + fetchOkxTicker(fetchImpl, spec.spot).catch(() => null), + fetchOkxFunding(fetchImpl, spec.swap).catch(() => null), + fetchOkxOpenInterest(fetchImpl, spec.swap).catch(() => null), + fetchAssetMarkets(fetchImpl, spec.search).catch(() => null) + ]); + return { asset: key, spec, ticker, funding, openInterest, pmMarkets }; + })); + + if (assets.every((a) => a.ticker === null && a.funding === null && a.pmMarkets === null)) { + throw new Error('All upstream lookups (OKX market data + Polymarket Gamma) failed'); + } + + const caveats = [...STANDARD_CAVEATS]; + const readouts = assets.map((entry) => buildAssetReadout(entry, caveats)); + + const dimensions = [ + buildSpotMomentumDimension(readouts), + buildFundingDimension(readouts), + buildPremiumDimension(readouts), + buildPolymarketDimension(readouts) + ]; + + const scoredDimensions = dimensions.filter((dim) => dim.normalized_score !== null); + for (const dim of dimensions) { + if (dim.normalized_score === null) { + caveats.push(`Dimension "${dim.dimension}" had no usable upstream data; it contributed 0 points and its weight was excluded.`); + } + } + + const { score, regime } = scoreRegime(scoredDimensions); + const confidence = scoreConfidence(score, scoredDimensions, dimensions.length); + const watchItems = buildWatchItems(readouts, scoredDimensions).slice(0, limit); + const source_status = buildRegimeSourceStatus(assets, dimensions); + const oi_context = buildOiContext(readouts); + + return { + schema_version: '0.3', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + focus: input.focus ?? null, + assets_scanned: assetKeys, + limit + }, + regime, + direction: regime, + score, + confidence, + summary: buildSummary(regime, score, assetKeys), + buyer_summary_zh: buildBuyerSummaryZh(regime, score, confidence, oi_context), + dimensions, + rationale: buildRationale(dimensions, score, regime), + assets: readouts, + oi_context, + watch_items: watchItems, + caveats, + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + price_provider: 'okx_public_market_ticker (last vs open24h)', + funding_provider: 'okx_public_funding_rate (fundingRate + premium)', + open_interest_provider: 'okx_public_open_interest (context only, not scored)', + probability_provider: 'polymarket_gamma_public_search (oneDayPriceChange, volume-weighted)', + source_status, + method: { + formula: 'score = 50 + 50 * sum(weight_pct/100 * normalized_score); normalized_score in [-1, +1] per dimension', + regime_rule: 'mixed when two dimensions conflict at |normalized| >= 0.35; else risk_on if score >= 60, risk_off if score <= 40, neutral otherwise', + dimension_scales: Object.fromEntries( + Object.entries(DIMENSIONS).map(([name, spec]) => [name, `${spec.weight_pct}% weight, full conviction at ${spec.full_scale}`]) + ) + } + } + }; +} + +function buildRegimeSourceStatus(assets, dimensions) { + const perAsset = assets.map((a) => ({ + asset: a.asset, + okx_ticker: a.ticker ? 'ok' : 'fail', + okx_funding: a.funding ? 'ok' : 'fail', + okx_open_interest: a.openInterest ? 'ok' : 'fail', + polymarket_search: a.pmMarkets ? 'ok' : 'fail', + pm_markets: Array.isArray(a.pmMarkets) ? a.pmMarkets.length : 0 + })); + const dims = dimensions.map((d) => ({ + dimension: d.dimension, + status: d.normalized_score === null ? 'missing' : 'ok', + contribution_points: d.contribution_points ?? null + })); + const failCount = perAsset.reduce( + (n, row) => n + ['okx_ticker', 'okx_funding', 'polymarket_search'].filter((k) => row[k] === 'fail').length, + 0 + ); + return { + as_of: new Date().toISOString(), + overall: failCount === 0 ? 'green' : (failCount <= 2 ? 'yellow' : 'red'), + assets: perAsset, + dimensions: dims + }; +} + +/** + * Static degraded payload for when the live path fails. The worker cache layer + * stamps `mode: 'degraded'` and prepends the failure caveat. + */ +export function buildCryptoMarketRegimeFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + focus: input?.focus ?? null, + assets_scanned: resolveAssetKeys(input?.focus ?? input?.asset), + limit: clampInteger(input?.limit, 1, 10, 5) + }, + regime: 'neutral', + direction: 'neutral', + score: 50, + confidence: 0, + summary: 'Static fallback: live OKX/Polymarket feeds were unavailable; regime defaults to neutral with zero confidence.', + dimensions: [], + rationale: 'No live data — no regime judgment was computed. Do not act on this response.', + assets: [], + watch_items: [], + caveats: [...STANDARD_CAVEATS], + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { price_provider: 'unavailable', funding_provider: 'unavailable', probability_provider: 'unavailable' } + }; +} + +// ---- per-asset readouts ------------------------------------------------------ + +function buildAssetReadout(entry, caveats) { + const { asset, spec, ticker, funding, openInterest, pmMarkets } = entry; + + const priceChangePct = ticker ? round2(((ticker.last - ticker.open24h) / ticker.open24h) * 100) : null; + if (!ticker) caveats.push(`OKX ticker for ${spec.spot} was unavailable; ${asset} spot fields are null.`); + if (!funding) caveats.push(`OKX funding rate for ${spec.swap} was unavailable; ${asset} funding/premium fields are null.`); + if (pmMarkets === null) caveats.push(`Polymarket Gamma search for "${asset}" failed; its event sentiment is null.`); + + const pmSentiment = pmMarkets && pmMarkets.length ? computePmSentiment(pmMarkets) : null; + + return { + asset, + spot_inst_id: spec.spot, + swap_inst_id: spec.swap, + spot_last: ticker ? ticker.last : null, + price_change_24h_pct: priceChangePct, + funding_rate: funding ? funding.fundingRate : null, + funding_rate_annualized_pct: funding ? round2(funding.fundingRate * 3 * 365 * 100) : null, + perp_premium: funding ? funding.premium : null, + open_interest_usd: openInterest, + pm_markets_scanned: pmMarkets ? pmMarkets.length : 0, + // Volume-weighted 24h drift of "bullish for the asset" probability, in pts. + pm_sentiment_drift_pts: pmSentiment ? round2(pmSentiment.drift * 100) : null, + pm_top_market: pmSentiment ? pmSentiment.topMarket : null + }; +} + +/** + * Volume-weighted average of stance-adjusted 24h probability changes across + * the asset's Polymarket markets. Positive drift = event odds moved in the + * asset-bullish direction over the last 24h. + */ +function computePmSentiment(markets) { + let weighted = 0; + let volume = 0; + let topMarket = null; + for (const market of markets) { + const stance = classifyStance(market); + if (stance === 0) continue; + const directional = stance * market.one_day_price_change; + weighted += directional * market.volume_24hr; + volume += market.volume_24hr; + if (!topMarket || Math.abs(directional) > Math.abs(topMarket.directional)) { + topMarket = { + title: market.title, + outcome: market.primary_outcome, + probability: market.primary_price, + probability_change_24h: market.one_day_price_change, + directional + }; + } + } + if (!(volume > 0) || !topMarket) return null; + return { + drift: weighted / volume, + topMarket: { + title: topMarket.title, + outcome: topMarket.outcome, + probability: topMarket.probability, + probability_change_24h: topMarket.probability_change_24h + } + }; +} + +/** Shared classifier (src/market-stance.mjs) — see that file for the 2026-07-30 + * measurement that removed the dollar-amount bullish fallback. */ +function classifyStance(market) { + return classifyAssetStance(market); +} + +// ---- dimensions --------------------------------------------------------------- + +function buildSpotMomentumDimension(readouts) { + const values = readouts.map((r) => r.price_change_24h_pct).filter((v) => v !== null); + const avg = values.length ? values.reduce((a, b) => a + b, 0) / values.length : null; + return buildDimension({ + name: 'spot_momentum_24h', + normalized: avg === null ? null : clamp(avg / 3, -1, 1), + reading: avg === null ? null : `${signed(round2(avg))}% avg 24h spot change (${readouts.filter((r) => r.price_change_24h_pct !== null).map((r) => `${r.asset} ${signed(r.price_change_24h_pct)}%`).join(', ')})`, + detail: 'OKX spot last vs open24h, averaged across scanned assets. Full conviction at ±3%.' + }); +} + +function buildFundingDimension(readouts) { + const values = readouts.map((r) => r.funding_rate).filter((v) => v !== null); + const avg = values.length ? values.reduce((a, b) => a + b, 0) / values.length : null; + const NEUTRAL = 0.0001; // 0.01% per 8h is the customary baseline funding + return buildDimension({ + name: 'perp_funding_rate', + normalized: avg === null ? null : clamp((avg - NEUTRAL) / 0.0005, -1, 1), + reading: avg === null ? null : `${round4(avg * 100)}% avg current funding vs 0.01% baseline (${readouts.filter((r) => r.funding_rate !== null).map((r) => `${r.asset} ${round4(r.funding_rate * 100)}%`).join(', ')})`, + detail: 'Funding above baseline = longs paying up (risk-on positioning); below/negative = shorts paying (risk-off). Full conviction at ±0.05% deviation.' + }); +} + +function buildPremiumDimension(readouts) { + const values = readouts.map((r) => r.perp_premium).filter((v) => v !== null); + const avg = values.length ? values.reduce((a, b) => a + b, 0) / values.length : null; + return buildDimension({ + name: 'perp_premium', + normalized: avg === null ? null : clamp(avg / 0.001, -1, 1), + reading: avg === null ? null : `${round4(avg * 100)}% avg perp premium vs index (${readouts.filter((r) => r.perp_premium !== null).map((r) => `${r.asset} ${round4(r.perp_premium * 100)}%`).join(', ')})`, + detail: 'Perp trading above index = leveraged demand to be long; below = to be short. Full conviction at ±0.10%.' + }); +} + +function buildPolymarketDimension(readouts) { + const values = readouts.map((r) => r.pm_sentiment_drift_pts).filter((v) => v !== null); + const avg = values.length ? values.reduce((a, b) => a + b, 0) / values.length : null; + return buildDimension({ + name: 'polymarket_event_sentiment', + normalized: avg === null ? null : clamp(avg / 5, -1, 1), + reading: avg === null ? null : `${signed(round2(avg))} pts avg 24h bullish-probability drift (${readouts.filter((r) => r.pm_sentiment_drift_pts !== null).map((r) => `${r.asset} ${signed(r.pm_sentiment_drift_pts)} pts`).join(', ')})`, + detail: 'Volume-weighted, stance-adjusted 24h change of Polymarket crypto event probabilities. Positive = odds drifted asset-bullish. Full conviction at ±5 pts.' + }); +} + +function buildDimension({ name, normalized, reading, detail }) { + const spec = DIMENSIONS[name]; + const contribution = normalized === null ? 0 : round1(50 * (spec.weight_pct / 100) * normalized); + return { + dimension: name, + weight_pct: spec.weight_pct, + reading, + direction: normalized === null ? 'unavailable' : normalized > 0.15 ? 'bullish' : normalized < -0.15 ? 'bearish' : 'neutral', + normalized_score: normalized === null ? null : round2(normalized), + contribution_points: contribution, + detail + }; +} + +// ---- scoring ------------------------------------------------------------------ + +function scoreRegime(scoredDimensions) { + const total = scoredDimensions.reduce((sum, dim) => sum + dim.contribution_points, 0); + const score = Math.round(Math.min(100, Math.max(0, 50 + total))); + + const norms = scoredDimensions.map((dim) => dim.normalized_score); + const conflicting = norms.some((n) => n >= 0.35) && norms.some((n) => n <= -0.35); + + let regime; + if (conflicting) regime = 'mixed'; + else if (score >= 60) regime = 'risk_on'; + else if (score <= 40) regime = 'risk_off'; + else regime = 'neutral'; + return { score, regime }; +} + +/** + * Confidence in [0, 0.9]: distance from 50 adds conviction, missing + * dimensions subtract it. + */ +function scoreConfidence(score, scoredDimensions, totalDimensions) { + const coverage = totalDimensions ? scoredDimensions.length / totalDimensions : 0; + const conviction = Math.abs(score - 50) / 50; + return round2(Math.min(0.9, (0.35 + 0.55 * conviction) * coverage)); +} + +function buildRationale(dimensions, score, regime) { + const parts = dimensions.map((dim) => { + if (dim.normalized_score === null) { + return `${dim.dimension} (0 pts, no data): upstream unavailable.`; + } + return `${dim.dimension} (${signed(dim.contribution_points)} pts of ±${round1(50 * dim.weight_pct / 100)} possible, ${dim.direction}): ${dim.reading}.`; + }); + parts.push(`Weighted total ${score}/100 → ${regime}.`); + return parts.join(' '); +} + +function buildSummary(regime, score, assetKeys) { + return `Crypto market regime: ${regime} (score ${score}/100) across ${assetKeys.join(', ')}.`; +} + +function buildOiContext(readouts) { + const rows = readouts + .filter((r) => r.open_interest_usd != null) + .map((r) => ({ + asset: r.asset, + open_interest_usd: r.open_interest_usd, + funding_rate: r.funding_rate, + note: r.funding_rate != null && Math.abs(r.funding_rate) >= 0.0005 + ? 'elevated_funding_with_oi_snapshot' + : 'oi_snapshot_only' + })); + const total = rows.reduce((sum, r) => sum + (r.open_interest_usd || 0), 0); + return { + role: 'context_only_not_scored', + assets: rows, + total_open_interest_usd: total || null, + interpretation: rows.length + ? 'OI is a leverage backlog snapshot; direction comes from spot/funding/premium/PM dimensions, not from OI alone.' + : 'OI unavailable from OKX public open-interest endpoint for scanned assets.' + }; +} + +function buildBuyerSummaryZh(regime, score, confidence, oiContext) { + const regimeZh = { + risk_on: '偏多/风险偏好', + risk_off: '偏空/避险', + neutral: '中性', + mixed: '信号打架' + }[regime] || regime; + const oiBit = oiContext?.total_open_interest_usd + ? `;OI 快照合计约 $${Math.round(oiContext.total_open_interest_usd).toLocaleString('en-US')}(仅上下文,不计入分数)` + : ';OI 暂缺'; + return `市场状态 ${regimeZh},分数 ${score}/100,置信 ${confidence}${oiBit}。解释见 dimensions/rationale;非交易建议。`; +} + +function buildWatchItems(readouts, scoredDimensions) { + const items = []; + for (const dim of scoredDimensions) { + if (Math.abs(dim.normalized_score) >= 0.5) { + items.push(`${dim.dimension} is at ${dim.normalized_score} of full conviction (${dim.direction}) — watch for reversal or confirmation.`); + } + } + for (const r of readouts) { + if (r.funding_rate !== null && Math.abs(r.funding_rate) >= 0.0005) { + items.push(`${r.asset} funding at ${round4(r.funding_rate * 100)}% per period — crowded positioning, squeeze risk.`); + } + if (r.pm_top_market) { + items.push(`${r.asset} Polymarket mover: "${r.pm_top_market.title}" (${r.pm_top_market.outcome} at ${r.pm_top_market.probability}, ${signed(round2(r.pm_top_market.probability_change_24h * 100))} pts/24h).`); + } + } + return items; +} + +// ---- upstream fetchers ---------------------------------------------------------- + +async function fetchOkxTicker(fetchImpl, instId) { + const row = await fetchOkxData(fetchImpl, `/api/v5/market/ticker?instId=${encodeURIComponent(instId)}`); + if (!row) return null; + const last = Number(row.last); + const open24h = Number(row.open24h); + if (!Number.isFinite(last) || !Number.isFinite(open24h) || open24h <= 0) return null; + return { last, open24h }; +} + +async function fetchOkxFunding(fetchImpl, instId) { + const row = await fetchOkxData(fetchImpl, `/api/v5/public/funding-rate?instId=${encodeURIComponent(instId)}`); + if (!row) return null; + const fundingRate = Number(row.fundingRate); + if (!Number.isFinite(fundingRate)) return null; + const premium = Number(row.premium); + return { fundingRate, premium: Number.isFinite(premium) ? premium : null }; +} + +async function fetchOkxOpenInterest(fetchImpl, instId) { + const row = await fetchOkxData(fetchImpl, `/api/v5/public/open-interest?instId=${encodeURIComponent(instId)}`); + if (!row) return null; + const oiUsd = Number(row.oiUsd); + return Number.isFinite(oiUsd) ? Math.round(oiUsd) : null; +} + +async function fetchOkxData(fetchImpl, path) { + const payload = await fetchJson(fetchImpl, `${OKX_BASE}${path}`); + if (payload?.code !== '0' || !Array.isArray(payload.data) || !payload.data.length) return null; + return payload.data[0]; +} + +async function fetchAssetMarkets(fetchImpl, searchTerm) { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(searchTerm)}&events_status=active&limit_per_type=10` + ); + const markets = []; + for (const event of Array.isArray(result?.events) ? result.events : []) { + if (event.closed) continue; + for (const market of event.markets ?? []) { + if (!market.conditionId || market.closed || market.active === false) continue; + const oneDay = Number(market.oneDayPriceChange); + if (!Number.isFinite(oneDay)) continue; // missing on quiet markets — never estimated + const volume = toNumber(market.volume24hr); + if (volume < MIN_PM_VOLUME_24H) continue; + const outcomes = parseJsonArray(market.outcomes); + const prices = parseJsonArray(market.outcomePrices); + if (!outcomes.length || !prices.length) continue; + markets.push({ + market_id: market.slug ?? market.conditionId, + title: market.question ?? market.slug ?? market.conditionId, + primary_outcome: String(outcomes[0]), + primary_price: Number(prices[0]), + one_day_price_change: oneDay, + volume_24hr: volume + }); + } + } + markets.sort((a, b) => b.volume_24hr - a.volume_24hr); + return markets.slice(0, PM_MARKETS_PER_ASSET); +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) { + throw new Error(`Upstream ${response.status} for ${url}`); + } + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +// ---- small utils ---------------------------------------------------------------- + +function resolveAssetKeys(focusInput) { + const raw = normalizeText(focusInput); + if (!raw || raw === 'all' || raw === 'global') return [...DEFAULT_SCAN]; + for (const [key, spec] of Object.entries(ASSETS)) { + if (raw === key || spec.aliases.includes(raw)) return [key]; + } + // Unknown focus: still try it (Gamma text search + -USDT instruments); + // failures surface as caveats rather than errors. + return [raw]; +} + +function parseJsonArray(value) { + if (Array.isArray(value)) return value; + try { + const parsed = JSON.parse(String(value ?? '[]')); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function signed(value) { + return `${value >= 0 ? '+' : ''}${value}`; +} + +function round1(value) { + return Math.round(Number(value) * 10) / 10; +} + +function round2(value) { + return Math.round(Number(value) * 100) / 100; +} + +function round4(value) { + return Math.round(Number(value) * 10000) / 10000; +} + +function toNumber(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function normalizeText(value) { + return String(value ?? '').trim().toLowerCase(); +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/event-price-divergence.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/event-price-divergence.mjs new file mode 100644 index 00000000..0507b16e --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/event-price-divergence.mjs @@ -0,0 +1,351 @@ +// Event Price Divergence Radar (event_price_divergence_radar). +// Compares 24h Polymarket event-probability moves against 24h spot momentum on +// OKX and flags markets where the two disagree in direction. +// +// Public endpoints used (no API key required), response shapes verified 2026-07-05: +// - Gamma: https://gamma-api.polymarket.com/public-search?q=&events_status=active&limit_per_type=10 +// -> { events: [{ title, closed, markets: [{ conditionId, question, slug, +// outcomes: '["Yes","No"]', outcomePrices: '["0.0075","0.9925"]', +// oneDayPriceChange: -0.002, volume24hr, active, closed, ... }] }] } +// (oneDayPriceChange = 24h absolute change of the first outcome price; +// the field is missing on some quiet markets — those are skipped, never estimated) +// - OKX: https://www.okx.com/api/v5/market/ticker?instId=BTC-USDT +// -> { code: '0', data: [{ instId, last: '63215.9', open24h: '62593.7', ... }] } +// (24h spot change derived as (last - open24h) / open24h) + +import { classifyAssetStance } from './market-stance.mjs'; + +const SERVICE_ID = 'event_price_divergence_radar'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +const OKX_BASE = 'https://www.okx.com'; + +const FETCH_TIMEOUT_MS = 8000; +const MARKETS_PER_ASSET = 8; +const MIN_MARKET_VOLUME_24H = 500; +// Divergence thresholds: probability move in absolute points, spot move in %. +const MIN_PROB_CHANGE = 0.02; +const MIN_PRICE_CHANGE_PCT = 0.3; + +const ASSETS = { + bitcoin: { search: 'bitcoin', instId: 'BTC-USDT', aliases: ['btc', 'xbt'] }, + ethereum: { search: 'ethereum', instId: 'ETH-USDT', aliases: ['eth', 'ether'] }, + solana: { search: 'solana', instId: 'SOL-USDT', aliases: ['sol'] }, + xrp: { search: 'xrp', instId: 'XRP-USDT', aliases: ['ripple'] }, + dogecoin: { search: 'dogecoin', instId: 'DOGE-USDT', aliases: ['doge'] } +}; +const DEFAULT_SCAN = ['bitcoin', 'ethereum', 'solana']; + +const STANDARD_CAVEATS = [ + 'Data and analytics only. Not investment advice and not a guarantee of future returns.', + 'No wallet custody, no user funds, no trade execution, no order routing.', + 'Divergence signals are heuristic reads of 24h probability vs spot momentum and can be wrong, stale, or explained by market-specific factors (e.g. strike distance, expiry).' +]; + +/** + * Build the full live response payload. + * Throws when every upstream lookup fails so callers can decide how to degrade. + */ +export async function assessEventPriceDivergenceLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInteger(input.limit, 1, 10, 5); + const assetKeys = resolveAssetKeys(input.asset); + + const assets = await Promise.all(assetKeys.map(async (key) => { + const spec = ASSETS[key] ?? { search: key, instId: `${key.toUpperCase()}-USDT` }; + const [ticker, markets] = await Promise.all([ + fetchOkxTicker(fetchImpl, spec.instId).catch(() => null), + fetchAssetMarkets(fetchImpl, spec.search).catch(() => null) + ]); + return { asset: key, instId: spec.instId, ticker, markets }; + })); + + if (assets.every((entry) => entry.ticker === null && entry.markets === null)) { + throw new Error('All upstream lookups (OKX ticker + Polymarket Gamma) failed'); + } + + const caveats = [...STANDARD_CAVEATS]; + const signals = []; + const assetReadouts = []; + + for (const { asset, instId, ticker, markets } of assets) { + const priceChangePct = ticker ? round2(((ticker.last - ticker.open24h) / ticker.open24h) * 100) : null; + + assetReadouts.push({ + asset, + inst_id: instId, + spot_last: ticker ? ticker.last : null, + price_change_24h_pct: priceChangePct, + pm_markets_scanned: markets ? markets.length : 0 + }); + + if (!ticker) { + caveats.push(`OKX ticker for ${instId} was unavailable; ${asset} spot fields are null and no divergence was computed for it.`); + continue; + } + if (!markets) { + caveats.push(`Polymarket Gamma search for "${asset}" failed; no event markets were scanned for it.`); + continue; + } + if (!markets.length) { + caveats.push(`No active Polymarket markets with a 24h probability change matched "${asset}".`); + continue; + } + + for (const market of markets) { + const signal = evaluateDivergence(asset, instId, market, priceChangePct); + if (signal) signals.push(signal); + } + } + + signals.sort((a, b) => b.confidence - a.confidence); + const top = signals.slice(0, limit); + const source_status = { + as_of: new Date().toISOString(), + overall: assets.every((a) => a.ticker && a.markets) ? 'green' + : assets.some((a) => a.ticker || a.markets) ? 'yellow' : 'red', + assets: assets.map((a) => ({ + asset: a.asset, + okx_ticker: a.ticker ? 'ok' : 'fail', + polymarket_search: a.markets ? 'ok' : 'fail', + pm_markets: Array.isArray(a.markets) ? a.markets.length : 0, + spot_change_24h_pct: a.ticker + ? round2(((a.ticker.last - a.ticker.open24h) / a.ticker.open24h) * 100) + : null + })), + thresholds: { + min_probability_change: MIN_PROB_CHANGE, + min_price_change_pct: MIN_PRICE_CHANGE_PCT, + min_market_volume_24h_usdt: MIN_MARKET_VOLUME_24H + } + }; + + const summary = buildSummary(top, assetReadouts); + + return { + schema_version: '0.3', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + asset: input.asset ?? null, + assets_scanned: assetKeys, + limit + }, + summary, + buyer_summary_zh: buildDivergenceBuyerSummaryZh(top, assetReadouts, source_status), + signals: top, + assets: assetReadouts, + caveats, + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + probability_provider: 'polymarket_gamma_public_search (oneDayPriceChange)', + price_provider: 'okx_public_market_ticker (last vs open24h)', + min_probability_change: MIN_PROB_CHANGE, + min_price_change_pct: MIN_PRICE_CHANGE_PCT, + min_market_volume_24h_usdt: MIN_MARKET_VOLUME_24H, + source_status + } + }; +} + +/** + * Static degraded payload for when the live path fails. The worker cache layer + * stamps `mode: 'degraded'` and prepends the failure caveat. + */ +export function buildEventPriceDivergenceFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + asset: input?.asset ?? null, + assets_scanned: resolveAssetKeys(input?.asset), + limit: clampInteger(input?.limit, 1, 10, 5) + }, + summary: 'Static fallback: live probability/price feeds were unavailable, no divergence computed.', + signals: [], + assets: [], + caveats: [...STANDARD_CAVEATS], + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + probability_provider: 'unavailable', + price_provider: 'unavailable' + } + }; +} + +function evaluateDivergence(asset, instId, market, priceChangePct) { + // stance: does the first outcome going UP imply the asset going UP? + const stance = classifyStance(market); + if (stance === 0) return null; + + const probChange = market.one_day_price_change; + const impliedDirection = Math.sign(probChange) * stance; + const priceDirection = Math.sign(priceChangePct); + + if (impliedDirection === 0 || priceDirection === 0) return null; + if (impliedDirection === priceDirection) return null; + if (Math.abs(probChange) < MIN_PROB_CHANGE) return null; + if (Math.abs(priceChangePct) < MIN_PRICE_CHANGE_PCT) return null; + + const direction = impliedDirection > 0 ? 'event_probability_bullish_price_down' : 'event_probability_bearish_price_up'; + const magnitude = round2(Math.abs(probChange) * 100 + Math.abs(priceChangePct)); + + return { + asset, + inst_id: instId, + market_id: market.market_id, + market_title: market.title, + outcome: market.primary_outcome, + event_probability: market.primary_price, + probability_change_24h: probChange, + market_stance: stance > 0 ? 'bullish_if_probability_up' : 'bearish_if_probability_up', + price_change_24h_pct: priceChangePct, + direction, + magnitude, + confidence: scoreConfidence(probChange, priceChangePct, market.volume_24hr), + rationale: buildRationale(asset, market, probChange, priceChangePct, impliedDirection) + }; +} + +/** Shared classifier (src/market-stance.mjs) — see that file for the 2026-07-30 + * measurement that removed the dollar-amount bullish fallback. */ +function classifyStance(market) { + return classifyAssetStance(market); +} + +function scoreConfidence(probChange, priceChangePct, volume24hr) { + let score = 0.5; + score += 0.15 * Math.min(Math.abs(probChange) / 0.1, 1); + score += 0.15 * Math.min(Math.abs(priceChangePct) / 3, 1); + score += 0.1 * Math.min(toNumber(volume24hr) / 50_000, 1); + return round2(Math.min(0.9, Math.max(0.5, score))); +} + +function buildRationale(asset, market, probChange, priceChangePct, impliedDirection) { + const probMove = `${probChange > 0 ? '+' : ''}${round2(probChange * 100)} pts`; + const priceMove = `${priceChangePct > 0 ? '+' : ''}${priceChangePct}%`; + const readout = impliedDirection > 0 + ? `event odds lean more bullish while spot moved ${priceMove}` + : `event odds lean more bearish while spot moved ${priceMove}`; + return `"${market.title}" probability of "${market.primary_outcome}" moved ${probMove} in 24h (now ${market.primary_price}); ${readout} on OKX for ${asset}.`; +} + +function buildSummary(signals, assetReadouts) { + const scanned = assetReadouts.map((entry) => entry.asset).join(', '); + if (!signals.length) { + return `No probability-vs-price divergence above thresholds across ${scanned || 'the requested assets'}.`; + } + const top = signals[0]; + return `${signals.length} divergence signal${signals.length === 1 ? '' : 's'} found across ${scanned}. Top: ${top.direction} on "${top.market_title}" (${top.asset}).`; +} + +function buildDivergenceBuyerSummaryZh(signals, assetReadouts, sourceStatus) { + const scanned = assetReadouts.map((a) => a.asset).join('/') || '指定资产'; + const health = sourceStatus?.overall || 'unknown'; + if (!signals.length) { + return `未发现超阈值背离(扫描 ${scanned};数据健康 ${health})。启发式信号,非交易建议。`; + } + const top = signals[0]; + return `发现 ${signals.length} 条背离;最强:${top.asset}「${top.market_title}」→ ${top.direction}(置信 ${top.confidence})。数据健康 ${health};非交易建议。`; +} + +function resolveAssetKeys(assetInput) { + const raw = normalizeText(assetInput); + if (!raw || raw === 'all') return [...DEFAULT_SCAN]; + for (const [key, spec] of Object.entries(ASSETS)) { + if (raw === key || spec.aliases.includes(raw)) return [key]; + } + // Unknown asset: still try it (Gamma text search + -USDT ticker); + // failures surface as caveats rather than errors. + return [raw]; +} + +async function fetchOkxTicker(fetchImpl, instId) { + const payload = await fetchJson(fetchImpl, `${OKX_BASE}/api/v5/market/ticker?instId=${encodeURIComponent(instId)}`); + if (payload?.code !== '0' || !Array.isArray(payload.data) || !payload.data.length) return null; + const row = payload.data[0]; + const last = Number(row.last); + const open24h = Number(row.open24h); + if (!Number.isFinite(last) || !Number.isFinite(open24h) || open24h <= 0) return null; + return { last, open24h }; +} + +async function fetchAssetMarkets(fetchImpl, searchTerm) { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(searchTerm)}&events_status=active&limit_per_type=10` + ); + const markets = []; + for (const event of Array.isArray(result?.events) ? result.events : []) { + if (event.closed) continue; + for (const market of event.markets ?? []) { + if (!market.conditionId || market.closed || market.active === false) continue; + const oneDay = Number(market.oneDayPriceChange); + if (!Number.isFinite(oneDay)) continue; // field missing on quiet markets — never estimated + const volume = toNumber(market.volume24hr); + if (volume < MIN_MARKET_VOLUME_24H) continue; + const outcomes = parseJsonArray(market.outcomes); + const prices = parseJsonArray(market.outcomePrices); + if (!outcomes.length || !prices.length) continue; + markets.push({ + market_id: market.slug ?? market.conditionId, + condition_id: market.conditionId, + title: market.question ?? market.slug ?? market.conditionId, + primary_outcome: String(outcomes[0]), + primary_price: Number(prices[0]), + one_day_price_change: oneDay, + volume_24hr: volume + }); + } + } + markets.sort((a, b) => b.volume_24hr - a.volume_24hr); + return markets.slice(0, MARKETS_PER_ASSET); +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) { + throw new Error(`Upstream ${response.status} for ${url}`); + } + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function parseJsonArray(value) { + if (Array.isArray(value)) return value; + try { + const parsed = JSON.parse(String(value ?? '[]')); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function round2(value) { + return Math.round(Number(value) * 100) / 100; +} + +function toNumber(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function normalizeText(value) { + return String(value ?? '').trim().toLowerCase(); +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/finance-cockpit.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/finance-cockpit.mjs new file mode 100644 index 00000000..0ffd235b --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/finance-cockpit.mjs @@ -0,0 +1,139 @@ +// Finance Cockpit — compose Crypto Market Regime + Event Price Divergence +// into one "副驾驶" card for agents. Read-only, no orders. + +import { + assessCryptoMarketRegimeLive, + buildCryptoMarketRegimeFallback +} from './crypto-market-regime.mjs'; +import { + assessEventPriceDivergenceLive, + buildEventPriceDivergenceFallback +} from './event-price-divergence.mjs'; + +const SERVICE_ID = 'finance_cockpit'; + +const STANDARD_CAVEATS = [ + 'Composed research card only. Not investment advice; does not place or route orders.', + 'Regime score and divergence signals are independent heuristics — disagreement is informative, not an error.', + 'No wallet custody, no trade execution.' +]; + +export async function assessFinanceCockpitLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const focus = input.focus ?? input.asset ?? 'all'; + const limit = clampInt(input.limit, 1, 10, 5); + + const [regimeResult, divergenceResult] = await Promise.allSettled([ + assessCryptoMarketRegimeLive({ focus, asset: focus, limit }, { fetchImpl }), + assessEventPriceDivergenceLive({ asset: focus === 'all' ? 'all' : focus, limit }, { fetchImpl }) + ]); + + const regime = regimeResult.status === 'fulfilled' + ? regimeResult.value + : buildCryptoMarketRegimeFallback({ focus, limit }); + const divergence = divergenceResult.status === 'fulfilled' + ? divergenceResult.value + : buildEventPriceDivergenceFallback({ asset: focus, limit }); + + if (regimeResult.status === 'rejected' && divergenceResult.status === 'rejected') { + throw new Error('Both regime and divergence upstream paths failed'); + } + + const action = deriveAction(regime, divergence); + const buyer_summary_zh = buildBuyerSummaryZh(action, regime, divergence); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { focus, limit }, + action, + buyer_summary_zh, + value_loop: { + why_pay_again: 'Regime score and divergence signals refresh with live OKX/PM data; useful on a monitor cadence.', + stale_after_minutes: 15, + best_used_in: 'crypto_research_or_risk_dashboard_loop', + paid_value_tier: 'A_repeat_monitoring' + }, + regime: { + regime: regime.regime, + score: regime.score, + confidence: regime.confidence, + summary: regime.summary, + buyer_summary_zh: regime.buyer_summary_zh ?? null, + oi_context: regime.oi_context ?? null, + source_status: regime.source?.source_status ?? null + }, + divergence: { + summary: divergence.summary, + buyer_summary_zh: divergence.buyer_summary_zh ?? null, + signal_count: Array.isArray(divergence.signals) ? divergence.signals.length : 0, + top_signals: (divergence.signals || []).slice(0, limit), + source_status: divergence.source?.source_status ?? null + }, + caveats: [ + ...STANDARD_CAVEATS, + ...(regimeResult.status === 'rejected' + ? [`Regime leg degraded: ${regimeResult.reason?.message || regimeResult.reason}`] + : []), + ...(divergenceResult.status === 'rejected' + ? [`Divergence leg degraded: ${divergenceResult.reason?.message || divergenceResult.reason}`] + : []) + ], + next_gate: 'Use_pm_trade_preflight_or_human_risk_limits_before_orders', + source: { + method: 'compose_crypto_market_regime_plus_event_price_divergence', + prize_track_fit: '金融副驾驶' + } + }; +} + +export function buildFinanceCockpitFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { focus: input.focus ?? input.asset ?? 'all', limit: clampInt(input.limit, 1, 10, 5) }, + action: 'hold_observe', + buyer_summary_zh: '演示回退:实时行情不可用,默认 hold_observe。', + regime: null, + divergence: null, + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Use_pm_trade_preflight_or_human_risk_limits_before_orders', + source: { method: 'static_fallback' } + }; +} + +function deriveAction(regime, divergence) { + const signals = divergence?.signals?.length || 0; + const score = Number(regime?.score); + const label = regime?.regime; + if (label === 'mixed' || (signals >= 2 && Number.isFinite(score) && Math.abs(score - 50) < 8)) { + return 'hold_observe'; + } + if (label === 'risk_on' && score >= 65 && signals === 0) return 'risk_on_clean'; + if (label === 'risk_off' && score <= 35 && signals === 0) return 'risk_off_clean'; + if (signals >= 1) return 'divergence_review'; + return 'hold_observe'; +} + +function buildBuyerSummaryZh(action, regime, divergence) { + const actionZh = { + risk_on_clean: '偏多且无明显背离', + risk_off_clean: '偏空且无明显背离', + divergence_review: '存在概率/现货背离,先复核', + hold_observe: '观望/信号混杂' + }[action] || action; + const r = regime?.regime ?? 'n/a'; + const s = regime?.score ?? 'n/a'; + const n = divergence?.signals?.length ?? 0; + return `副驾驶结论:${actionZh}。Regime=${r}(${s}/100),背离信号 ${n} 条。组合卡,非下单指令。`; +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/http-server.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/http-server.mjs new file mode 100644 index 00000000..86fe19fe --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/http-server.mjs @@ -0,0 +1,187 @@ +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { auditDelivery } from './auditor.mjs'; +import { assessWorldCupSmartMoney } from './worldcup-smart-money.mjs'; +import { + assessOkxAiDataService, + getOkxAiDataServiceByPath, + listOkxAiDataServices +} from './okx-ai-data-services.mjs'; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const demoDir = path.join(rootDir, 'demo'); +const sampleDir = path.join(rootDir, 'sample-inputs'); +const discoveryDir = path.join(rootDir, 'discovery'); + +export function createServer() { + return http.createServer(async (req, res) => { + try { + const url = new URL(req.url, 'http://127.0.0.1'); + + if (req.method === 'GET' && url.pathname === '/health') { + return sendJson(res, 200, { + ok: true, + service: 'agent-acceptance-gate', + mode: 'local_only' + }); + } + + if (req.method === 'GET' && url.pathname === '/api/sample-audits') { + const audits = readSampleAudits(); + return sendJson(res, 200, audits); + } + + if (req.method === 'GET' && url.pathname === '/api/okx-ai-services') { + return sendJson(res, 200, { + schema_version: '0.1', + mode: 'local_launch_pack', + services: [ + { + service_id: 'world_cup_smart_money_radar', + path: '/world-cup-smart-money-radar', + title: 'World Cup Smart Money Radar', + category: 'world_cup', + fee_usdt: '1', + description: 'Tracks profitable World Cup prediction-market wallets and highlights position changes.', + mode: 'public_safe_demo' + }, + ...listOkxAiDataServices() + ] + }); + } + + if (req.method === 'GET' && url.pathname === '/.well-known/agent-service.json') { + return sendFile(res, path.join(discoveryDir, 'agent-service.json'), 'application/json; charset=utf-8'); + } + + if (req.method === 'GET' && url.pathname === '/mcp-tool-manifest.json') { + return sendFile(res, path.join(discoveryDir, 'mcp-tool-manifest.json'), 'application/json; charset=utf-8'); + } + + if (req.method === 'GET' && url.pathname === '/openapi.yaml') { + return sendFile(res, path.join(rootDir, 'openapi.yaml'), 'text/yaml; charset=utf-8'); + } + + if (req.method === 'POST' && url.pathname === '/audit-agent-deliverable') { + const payload = await readJsonBody(req); + const audit = auditDelivery(payload); + return sendJson(res, 200, audit); + } + + if (req.method === 'POST' && url.pathname === '/world-cup-smart-money-radar') { + const payload = await readJsonBody(req); + const report = assessWorldCupSmartMoney(payload); + return sendJson(res, 200, report); + } + + if (req.method === 'POST') { + const service = getOkxAiDataServiceByPath(url.pathname); + if (service) { + const payload = await readJsonBody(req); + const report = assessOkxAiDataService(service, payload); + return sendJson(res, 200, report); + } + } + + if (req.method === 'GET') { + return serveStatic(url.pathname, res); + } + + sendJson(res, 405, { error: 'method_not_allowed' }); + } catch (error) { + sendJson(res, 400, { + error: 'bad_request', + message: error instanceof Error ? error.message : String(error) + }); + } + }); +} + +function readSampleAudits() { + return fs.readdirSync(sampleDir) + .filter((file) => file.endsWith('.json')) + .sort() + .flatMap((file) => { + const input = JSON.parse(fs.readFileSync(path.join(sampleDir, file), 'utf8')); + if (!input.task || !input.delivery) return []; + return [{ + input_file: file, + title: titleFromFile(file), + audit: auditDelivery(input) + }]; + }); +} + +function titleFromFile(file) { + return file + .replace(/^\d+-/, '') + .replace(/\.json$/, '') + .replaceAll('-', ' '); +} + +function serveStatic(pathname, res) { + const relativePath = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, ''); + const filePath = path.resolve(demoDir, relativePath); + + if (!filePath.startsWith(demoDir)) { + return sendText(res, 403, 'Forbidden'); + } + + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + return sendText(res, 404, 'Not found'); + } + + const ext = path.extname(filePath); + const contentType = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8' + }[ext] ?? 'text/plain; charset=utf-8'; + + res.writeHead(200, { 'content-type': contentType }); + fs.createReadStream(filePath).pipe(res); +} + +function readJsonBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => { + body += chunk; + if (body.length > 1_000_000) { + req.destroy(); + reject(new Error('Request body too large')); + } + }); + req.on('end', () => { + try { + resolve(JSON.parse(body || '{}')); + } catch (error) { + reject(error); + } + }); + req.on('error', reject); + }); +} + +function sendJson(res, status, payload) { + res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); + res.end(`${JSON.stringify(payload, null, 2)}\n`); +} + +function sendText(res, status, text) { + res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' }); + res.end(text); +} + +function sendFile(res, filePath, contentType) { + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + return sendText(res, 404, 'Not found'); + } + + res.writeHead(200, { 'content-type': contentType }); + fs.createReadStream(filePath).pipe(res); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/market-stance.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/market-stance.mjs new file mode 100644 index 00000000..2d421b2d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/market-stance.mjs @@ -0,0 +1,67 @@ +// Shared asset-stance classifier for Polymarket crypto event markets. +// +// Extracted 2026-07-30 from duplicated copies in crypto-market-regime.mjs and +// event-price-divergence.mjs, which had drifted into the same defect. +// +// Measured against 362 live Gamma markets (bitcoin/ethereum/solana, active only, +// 2026-07-30): the old rule ended with `|| /\$\s?\d/ → +1`, i.e. any Yes-market +// question carrying a dollar figure that missed the bearish keyword list was +// called bullish. 66 of the 362 markets fell through to that branch and every one +// was scored +1, but only ~6 deserved it: +// +// "Will the price of Ethereum be less than $1,400 on July 30?" → scored +1, is bearish +// "Will the price of Ethereum be between $1,400 and $1,500 …?" (×54) → scored +1, has NO direction +// "Will the price of Ethereum be greater than $2,300 …?" → scored +1, correct +// +// A range bucket cannot be signed without knowing where spot sits relative to the +// bucket, so it must be skipped rather than guessed. Because the bucket markets are +// numerous and carry volume, the old fallback pushed a volume-weighted bullish drift +// into the regime score (Polymarket sentiment carries 25% weight) — a systematic +// long bias, not a rounding error. +// +// Rule order now: explicit Up/Down outcome → range bucket (skip) → bearish compare → +// bullish compare → keyword polarity → unknown (0). Fail closed: when the phrasing is +// not recognised the market is skipped, never assumed bullish. + +/** Range buckets: "between $A and $B", "$A to $B", "$A–$B". No direction. */ +const RANGE_BUCKET = /\bbetween\b[^?]*\band\b|\$\s?[\d,.]+\s*(?:–|—|-|to)\s*\$\s?[\d,.]+/; + +/** Explicit downside comparisons, including the ones the old list missed. */ +const BEARISH_COMPARE = + /\b(?:less than|lower than|below|under|beneath|at or below|no more than|worth less)\b|\bsub[- ]?\$?\d/; + +/** Explicit upside comparisons. */ +const BULLISH_COMPARE = + /\b(?:greater than|more than|higher than|above|over|at or above|at least)\b/; + +const BEARISH_KEYWORD = /\b(?:dip|drop|fall|crash|down|decline|plunge|tumble)\b/; +const BULLISH_KEYWORD = /\b(?:reach|hit|up|exceed|surpass|all[- ]time high|ath|rally|moon)\b/; + +/** + * +1 when the first outcome rising is bullish for the asset, -1 when bearish, + * 0 when the market cannot be interpreted safely (skip it, do not guess). + * + * @param {{ primary_outcome: string, title: string }} market + * @returns {-1|0|1} + */ +export function classifyAssetStance(market) { + const outcome = String(market?.primary_outcome ?? '').trim().toLowerCase(); + if (outcome === 'up') return 1; + if (outcome === 'down') return -1; + if (outcome !== 'yes') return 0; + + const question = String(market?.title ?? '').toLowerCase(); + if (!question) return 0; + + // Buckets first: "between $1,400 and $1,500" also contains no comparison word, + // but it would otherwise reach the dollar-amount branch and be called bullish. + if (RANGE_BUCKET.test(question)) return 0; + + if (BEARISH_COMPARE.test(question)) return -1; + if (BULLISH_COMPARE.test(question)) return 1; + if (BEARISH_KEYWORD.test(question)) return -1; + if (BULLISH_KEYWORD.test(question)) return 1; + + // Deliberately no dollar-amount fallback — see header note. + return 0; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/okx-ai-data-services.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/okx-ai-data-services.mjs new file mode 100644 index 00000000..df3fe3c8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/okx-ai-data-services.mjs @@ -0,0 +1,142 @@ +const SERVICES = { + polymarket_smart_money_radar: { + path: '/polymarket-smart-money-radar', + title: 'Polymarket Smart Money Radar', + category: 'finance', + fee_usdt: '1', + description: 'Tracks profitable Polymarket wallets across markets and highlights position changes.', + sampleSignals: [ + { + market_id: 'fed-rate-september-2026', + market_title: 'Fed rate decision by September 2026', + address_label: 'pm-alpha-104', + side: 'No rate cut', + action: 'increased_position', + notional_usdt: 2360, + seven_day_pnl_usdt: 804, + confidence: 0.74, + rationale: 'Profitable macro wallet added exposure while public probability stayed flat.' + }, + { + market_id: 'crypto-etf-approval-2026', + market_title: 'Crypto ETF approval in 2026', + address_label: 'pm-alpha-033', + side: 'Yes', + action: 'new_position', + notional_usdt: 1180, + seven_day_pnl_usdt: 289, + confidence: 0.62, + rationale: 'High win-rate wallet opened a fresh position after market depth improved.' + } + ] + }, + event_probability_crypto_divergence: { + path: '/event-probability-crypto-divergence', + title: 'Event Probability Crypto Divergence', + category: 'finance', + fee_usdt: '1', + description: 'Compares prediction-market event probability with crypto price and funding moves.', + sampleSignals: [ + { + event: 'Crypto ETF approval in 2026', + probability_change_24h: 4.2, + linked_asset: 'BTC', + spot_change_24h: -1.1, + funding_bias: 'neutral_to_short', + divergence: 'event_probability_up_price_down', + confidence: 0.67, + rationale: 'Prediction-market probability rose while spot and funding did not confirm the move.' + }, + { + event: 'Major exchange enforcement action', + probability_change_24h: 3.5, + linked_asset: 'ETH', + spot_change_24h: 0.4, + funding_bias: 'long_crowded', + divergence: 'event_risk_up_leverage_still_long', + confidence: 0.61, + rationale: 'Event risk increased while perp positioning remained crowded long.' + } + ] + }, + crypto_market_pulse_report: { + path: '/crypto-market-pulse-report', + title: 'Crypto Market Pulse Report', + category: 'finance', + fee_usdt: '1', + description: 'Returns a compact agent-readable crypto market pulse with flows, anomalies, and watch items.', + sampleSignals: [ + { + segment: 'majors', + status: 'mixed', + flow_bias: 'btc_outperforming_eth', + anomaly: 'stablecoin inflow without broad risk-on confirmation', + confidence: 0.58, + rationale: 'Liquidity improved but breadth remains weak across majors.' + }, + { + segment: 'perps', + status: 'fragile', + flow_bias: 'funding_reheating', + anomaly: 'long leverage rebuilding after shallow spot bounce', + confidence: 0.63, + rationale: 'Funding reset is incomplete, so reversal risk remains elevated.' + } + ] + } +}; + +export function listOkxAiDataServices() { + return Object.entries(SERVICES).map(([service_id, service]) => ({ + service_id, + path: service.path, + title: service.title, + category: service.category, + fee_usdt: service.fee_usdt, + description: service.description, + mode: 'public_safe_demo' + })); +} + +export function getOkxAiDataServiceByPath(pathname) { + return Object.entries(SERVICES) + .map(([service_id, service]) => ({ service_id, ...service })) + .find((service) => service.path === pathname); +} + +export function assessOkxAiDataService(service, input = {}) { + const query = input.market ?? input.event ?? input.asset ?? input.query ?? 'all'; + const limit = clampInteger(input.limit, 1, 10, 5); + const signals = service.sampleSignals.slice(0, limit); + + return { + schema_version: '0.1', + service_id: service.service_id, + service_name: service.title, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + query, + limit + }, + summary: buildSummary(service, signals), + signals, + caveats: [ + 'Demo data only. Production service must use fresh data before listing.', + 'Data and analytics only. Not investment advice, not betting advice, and not a guarantee of future returns.', + 'No wallet custody, no user funds, no trade execution, no order routing.' + ], + next_gate: 'production_data_feed_and_OKX_ASP_listing_require_Leo_approval' + }; +} + +function buildSummary(service, signals) { + if (!signals.length) return `${service.title}: no signal found for the requested query.`; + return `${service.title}: ${signals.length} demo signals generated for agent research workflows.`; +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-brier.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-brier.mjs new file mode 100644 index 00000000..dc58b594 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-brier.mjs @@ -0,0 +1,237 @@ +// PM Brier — read-only calibration score from settled Polymarket positions. +// Productizes polymarket-toolkit computeBrierScoreFromSettledPositions. + +const SERVICE_ID = 'pm_brier'; +const LB_BASE = 'https://lb-api.polymarket.com'; +const DATA_BASE = 'https://data-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; +const EVM = /^0x[a-fA-F0-9]{40}$/; + +const STANDARD_CAVEATS = [ + 'Read-only calibration sample from Data API positions page — not full-history Brier.', + 'Settled rows use redeemable=true; win if currentValue > 0.', + 'No wallet custody, no trade execution.' +]; + +// 2026-07-30 — measured against live Data API, three leaderboard wallets plus one +// active BTC trader: +// swisstony (#1 all-time profit): 200 positions, redeemable=0 → no sample at all +// Theo4 / Fredi9999: 0 positions → no sample at all +// 0x63ce…(active): 3 positions, all redeemable, all currentValue=0 +// /positions only carries positions that have NOT been redeemed yet. Winners get +// claimed and drop off; losers linger as zero-value dust rows. So a redeemable=true +// sample is survivorship-biased toward losses, and the bias signature is exactly +// "few rows, zero wins". Worse, cheap losing longshots produce small squared errors: +// 0x63ce scored brier=0.118 → rating "good" while win_rate was 0. A number that says +// "well calibrated" about an 0-for-3 wallet is not a soft edge case, it is wrong. +// Fix below: refuse to rate an unrepresentative sample, and report the base-rate +// reference so the score is interpretable instead of compared to a hardcoded 0.15. +const MIN_RATEABLE_SAMPLE = 10; + +export async function assessPmBrierLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const raw = String(input.address ?? input.wallet ?? input.username ?? input.query ?? '').trim(); + if (!raw) throw new Error('address (0x…) or username is required'); + + let address = null; + let resolved_via = null; + if (EVM.test(raw)) { + address = raw.toLowerCase(); + resolved_via = 'evm_address'; + } else { + address = await resolveUsername(fetchImpl, raw); + resolved_via = address ? 'leaderboard_username' : null; + } + if (!address) { + throw new Error(`Could not resolve wallet for "${raw}" via leaderboard username search`); + } + + const limit = clampInt(input.limit, 50, 200, 200); + const positions = await fetchJson( + fetchImpl, + `${DATA_BASE}/positions?user=${address}&limit=${limit}&sizeThreshold=0` + ).catch(() => []); + + const list = Array.isArray(positions) ? positions : []; + const settled = list.filter((p) => p.redeemable === true); + const result = computeBrier(settled); + + const brier = Number.isFinite(result.brier) ? round3(result.brier) : null; + const win_rate = result.n ? round3(result.wins / result.n) : null; + + // Brier is meaningless without a reference: predicting the sample's own base rate + // for every market scores base_rate*(1-base_rate). Beating that is the only claim + // this sample can support. + const base_rate = win_rate; + const baseline_brier = + base_rate === null ? null : round3(base_rate * (1 - base_rate)); + const skill_vs_baseline = + brier === null || baseline_brier === null ? null : round3(baseline_brier - brier); + + const sample_bias = + result.n === 0 + ? 'no_settled_rows_in_positions_page' + : result.wins === 0 + ? 'zero_wins_survivorship_suspected' + : result.n < MIN_RATEABLE_SAMPLE + ? 'sample_below_rateable_threshold' + : null; + + const rating = !Number.isFinite(result.brier) + ? 'insufficient_sample' + : sample_bias + ? 'not_rateable' + : skill_vs_baseline > 0 + ? 'beats_base_rate' + : 'no_edge_vs_base_rate'; + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { query: raw, address, resolved_via, positions_limit: limit }, + brier, + settled_markets: result.n, + wins: result.wins, + win_rate, + baseline_brier, + skill_vs_baseline, + sample_bias, + rating, + buyer_summary_zh: buildBuyerSummaryZh({ + brier, + rating, + n: result.n, + wins: result.wins, + win_rate, + baseline_brier, + skill_vs_baseline, + sample_bias + }), + sample: settled.slice(0, 10).map((p) => ({ + title: p.title ?? p.slug ?? null, + avg_price: toNumber(p.avgPrice), + won: toNumber(p.currentValue) > 0, + current_value: toNumber(p.currentValue) + })), + confidence_gaps: [ + 'positions_page_capped', + 'redeemed_winners_absent_from_positions_page', + ...(result.n < MIN_RATEABLE_SAMPLE ? ['small_settled_sample'] : []), + ...(sample_bias ? [sample_bias] : []) + ], + caveats: [...STANDARD_CAVEATS], + next_gate: 'Use_polymarket-brier_skill_for_full_calibration', + source: { + provider: 'polymarket_public_api', + oss_lineage: 'polymarket-toolkit computeBrierScoreFromSettledPositions', + method: 'mean((avgPrice - actual)^2) over redeemable positions' + } + }; +} + +export function buildPmBrierFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { query: input.address ?? input.username ?? null, address: null, resolved_via: null }, + brier: null, + settled_markets: 0, + wins: 0, + win_rate: null, + rating: 'insufficient_sample', + sample: [], + confidence_gaps: ['live_data_unavailable'], + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Use_polymarket-brier_skill_for_full_calibration', + source: { provider: 'static_fallback' } + }; +} + +function computeBrier(settled) { + if (!settled.length) return { brier: Number.NaN, n: 0, wins: 0 }; + let sumSq = 0; + let wins = 0; + for (const p of settled) { + const f = toNumber(p.avgPrice); + const won = toNumber(p.currentValue) > 0; + if (won) wins += 1; + const actual = won ? 1 : 0; + sumSq += (f - actual) ** 2; + } + return { brier: sumSq / settled.length, n: settled.length, wins }; +} + +async function resolveUsername(fetchImpl, username) { + const needle = username.trim().toLowerCase(); + for (let page = 0; page < 3; page++) { + const rows = await fetchJson( + fetchImpl, + `${LB_BASE}/profit?window=all&limit=500&offset=${page * 500}` + ).catch(() => []); + if (!Array.isArray(rows) || !rows.length) break; + for (const row of rows) { + const n = String(row.name ?? '').toLowerCase(); + const p = String(row.pseudonym ?? '').toLowerCase(); + if (n === needle || p === needle) return String(row.proxyWallet ?? '').toLowerCase() || null; + } + if (rows.length < 500) break; + } + return null; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function round3(n) { + return Math.round(n * 1000) / 1000; +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} + +function buildBuyerSummaryZh({ + brier, + rating, + n, + wins, + win_rate, + baseline_brier, + skill_vs_baseline, + sample_bias +}) { + if (!Number.isFinite(brier) || !n) { + return '样本不足:/positions 当前页没有已结算持仓,无法给出 Brier。注意该端点只保留未赎回持仓,赢单赎回后即消失,因此「查不到样本」本身不代表这个地址表现差。完整校准需走 polymarket-brier skill 的全历史口径。'; + } + const head = `Brier=${brier},已结算样本 ${n} 场,胜 ${wins}(胜率 ${win_rate});同样本基准率 Brier=${baseline_brier},相对基准${skill_vs_baseline > 0 ? '领先' : '落后'} ${Math.abs(skill_vs_baseline)}。`; + if (sample_bias === 'zero_wins_survivorship_suspected') { + return `${head} **不给评级**:样本零胜,符合生存者偏差特征——/positions 只保留未赎回持仓,赢单赎回后消失、输单以零值留存,所以低 Brier 在这里可能只是「买便宜的单然后输」而非校准好。需全历史数据才能定论。`; + } + if (sample_bias === 'sample_below_rateable_threshold') { + return `${head} **不给评级**:样本不足 ${MIN_RATEABLE_SAMPLE} 场,随机波动大于可解释的差异。`; + } + return `${head} 结论:${rating === 'beats_base_rate' ? '在该样本上优于基准率' : '在该样本上未跑赢基准率'}。这是抽样信号,不是完整历史审计(赢单赎回后不在本端点内)。`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-football.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-football.mjs new file mode 100644 index 00000000..d05b3f80 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-football.mjs @@ -0,0 +1,866 @@ +// Football match category plugin (L1) for PM Event Readout. +// Aligns to pm-football-match skill (exportable parts only). +// Quality bar: research/2026-07-09-hackathon-quality-bar.md +// No Leo bankroll/sizing/orders. Fixture must be honest. + +import { round2 } from './pm-gamma-market.mjs'; + +/** + * Cross-market residual cutoffs, hoisted 2026-07-30. + * + * These were inline magic numbers. They are not wrong — labelling a market as + * "draw heavy" does need a cutoff — but a buyer paying per call could not see why + * 0.55 rather than 0.6, and could not tell a tuned threshold from a typo. The + * repo already has the better pattern: crypto-market-regime declares its weights + * and normalisation scales so they surface in the response and can be audited. + * Same treatment here: named, commented, echoed back on every residual via + * `triggered_by`, and exported so a caller can read the ruleset before trusting it. + * + * Values are unchanged from the inline versions — this is a disclosure change, not + * a retune. Retuning needs settled-match backtesting, which this service does not do. + */ +export const CROSS_MARKET_RESIDUAL_THRESHOLDS = Object.freeze({ + /** Both moneyline sides below this = neither side is favoured. */ + ml_side_short: 0.35, + /** Draw priced above this while both sides are short = draw-heavy shape. */ + ml_draw_elevated: 0.3, + /** Over 2.5 above this reads as a high-scoring market. */ + over25_rich: 0.62, + /** …but only counts as tension when the best moneyline side stays under this. */ + ml_max_side_tight: 0.4, + /** BTTS Yes above this is rich. */ + btts_rich: 0.6, + /** …and conflicts when Over 2.5 sits below this. */ + over25_subdued: 0.45 +}); + +const REQUIRED_GROUPS = [ + 'moneyline_90m', + 'totals_ladder', + 'spreads_ladder', + 'btts', + 'team_totals' +]; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle may include sibling_event_slugs + * @param {object[]} args.eventMatrix + * @param {object} [args.fixture] caller-provided verification + */ +export function enrichFootballCategory({ market, eventBundle, eventMatrix, fixture = null }) { + const classified = (eventMatrix || []).map((row) => ({ + ...row, + market_group: classifyFootballGroup(row) + })); + + const marketType = detectFootballMarketType({ market, eventBundle, eventMatrix: classified }); + if (marketType === 'outright_season') { + return buildFootballOutrightCategory({ market, eventBundle, classified }); + } + + const groups = groupBy(classified, (row) => row.market_group); + const missing = []; + for (const key of REQUIRED_GROUPS) { + if (!groups[key]?.length) missing.push(key); + } + + // Knockout often has advance on sibling — note if absent + if (!groups.advance?.length) missing.push('advance_or_match_winner_optional'); + + const requiredMissing = missing.filter((m) => !m.endsWith('_optional')); + const matrix_status = requiredMissing.length ? 'incomplete' : 'complete'; + + const moneyline = summarizeMoneyline(groups.moneyline_90m || []); + const totals = summarizeLadder(groups.totals_ladder || [], 'totals'); + const spreads = summarizeLadder(groups.spreads_ladder || [], 'spreads'); + const btts = firstYes(groups.btts); + const advance = firstYes(groups.advance); + const teamTotals = summarizeTeamTotals(groups.team_totals || []); + + const fixtureGate = evaluateFixtureGate(market, eventBundle, fixture); + const shape = buildImpliedShape({ moneyline, totals, spreads, btts, advance, teamTotals, missing_market_groups: missing }); + const expressions = buildExpressionComparison({ moneyline, totals, spreads, btts, advance, shape }); + const hard_veto_gaps = buildFootballHardVetoGaps({ + requiredMissing, + fixtureGate, + moneyline, + totals, + expressions + }); + const matrix_completeness = { + status: matrix_status, + required_groups: REQUIRED_GROUPS, + required_present: REQUIRED_GROUPS.filter((g) => (groups[g] || []).length > 0), + required_missing: requiredMissing, + hard_veto_gaps + }; + + let tradability_cap = null; + const tradability_reasons = []; + if (fixtureGate.fixture_status !== 'ok') { + tradability_cap = 'weak'; + tradability_reasons.push('fixture_unverified_or_failed'); + } + if (matrix_status === 'incomplete') { + tradability_cap = tradability_cap || 'medium'; + tradability_reasons.push('football_matrix_incomplete'); + } + if (hard_veto_gaps.length) { + tradability_cap = 'weak'; + tradability_reasons.push('football_hard_veto_gaps'); + } + + const default_action_hint = hard_veto_gaps.length || fixtureGate.fixture_status !== 'ok' || matrix_status === 'incomplete' + ? 'no_trade' + : 'use_decision_card_after_expression_comparison'; + + // Full dump of player_props (often 200+) blows payload; keep count + sample. + const SURFACE_FULL = new Set([ + 'moneyline_90m', 'totals_ladder', 'spreads_ladder', 'btts', 'team_totals', + 'advance', 'first_half', 'second_half', 'extra_time', 'penalty_shootout', + 'exact_score', 'corners', 'first_to_score' + ]); + const market_surface = {}; + for (const [key, rows] of Object.entries(groups)) { + if (SURFACE_FULL.has(key)) { + market_surface[key] = rows.map(compactRow); + } else if (key === 'player_props') { + market_surface.player_props_sample = rows.slice(0, 8).map(compactRow); + } else { + market_surface[key] = rows.slice(0, 12).map(compactRow); + } + } + + return { + category: 'football', + category_depth: 'enriched', + market_type: 'match', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + sibling_event_slugs: eventBundle?.sibling_event_slugs || [], + linked_event_count: eventBundle?.linked_event_count + ?? (1 + (eventBundle?.sibling_event_slugs?.length || 0)), + discovery: eventBundle?.discovery || null, + related_market_count: classified.length, + group_counts: Object.fromEntries( + Object.entries(groups).map(([key, rows]) => [key, rows.length]) + ), + matrix_status, + matrix_completeness, + hard_veto_gaps, + missing_market_groups: missing, + market_surface, + market_implied_shape: shape, + fixture: fixtureGate, + expression_comparison: expressions, + recommended_expression: expressions.recommended || null, + default_action_hint, + tradability_cap, + tradability_reasons, + central_thesis: shape.central_thesis, + coherence: buildFootballCoherence({ moneyline, totals, spreads, btts, shape, matrix_status }), + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + skill_alignment: { + source: 'pm-football-match', + included: [ + 'fixture_gate', + 'full_same_event_matrix_via_parent_event_id', + 'missing_market_groups', + 'matrix_completeness', + 'hard_veto_gaps', + 'market_implied_shape', + 'expression_comparison', + 'adjacent_ladder_context', + 'thesis_expression_coherence', + 'heuristic_coherence_and_top_scorelines' + ], + excluded_local_only: [ + 'bankroll_pct', + 'u_amount_from_latest_anchor', + 'playbook_card_writeback', + 'personal_exposure', + 'full_player_props_dump', + 'full_joint_poisson_engine' + ] + } + }; +} + +function buildFootballHardVetoGaps({ requiredMissing, fixtureGate, moneyline, totals, expressions }) { + const gaps = []; + if (fixtureGate?.fixture_status && fixtureGate.fixture_status !== 'ok') { + gaps.push(`fixture_${fixtureGate.fixture_status}`); + } + if (requiredMissing.includes('moneyline_90m') || !moneyline?.home) { + gaps.push('missing_moneyline_90m'); + } + for (const key of requiredMissing) { + if (key === 'moneyline_90m') continue; + gaps.push(`missing_${key}`); + } + const ladderCount = expressions?.ladder_context?.totals_ladder_count ?? totals?.lines?.length ?? 0; + if (ladderCount < 2) { + gaps.push('totals_ladder_thin_or_missing'); + } + return [...new Set(gaps)]; +} + +function buildFootballCoherence({ moneyline, totals, spreads, btts, shape, matrix_status }) { + const residuals = []; + const top_scorelines = []; + + const home = moneyline?.home?.yes ?? null; + const draw = moneyline?.draw?.yes ?? null; + const away = moneyline?.away?.yes ?? null; + const over25 = totals?.lines?.find((x) => Number(x.line) === 2.5)?.yes + ?? totals?.pivot?.yes + ?? null; + const bttsYes = btts?.yes ?? null; + + const T = CROSS_MARKET_RESIDUAL_THRESHOLDS; + + if (home != null && away != null && home < T.ml_side_short && away < T.ml_side_short + && draw != null && draw > T.ml_draw_elevated) { + residuals.push({ + type: 'ml_draw_heavy', + note: 'Both sides short-priced with elevated draw — check if totals/spreads agree.', + triggered_by: `home<${T.ml_side_short} && away<${T.ml_side_short} && draw>${T.ml_draw_elevated}` + }); + } + if (over25 != null && over25 > T.over25_rich && home != null && away != null + && Math.max(home, away) < T.ml_max_side_tight) { + residuals.push({ + type: 'totals_vs_ml_tension', + note: 'Market prices high Over 2.5 while ML looks tight — possible expression conflict.', + triggered_by: `over2.5>${T.over25_rich} && max(home,away)<${T.ml_max_side_tight}` + }); + } + if (bttsYes != null && over25 != null && bttsYes > T.btts_rich && over25 < T.over25_subdued) { + residuals.push({ + type: 'btts_vs_totals_tension', + note: 'BTTS Yes rich vs subdued Over — review ladder consistency.', + triggered_by: `btts>${T.btts_rich} && over2.5<${T.over25_subdued}` + }); + } + + // Illustrative scoreline ranks from crude ML weights (not calibrated Poisson). + const weights = [ + { scoreline: '1-0', w: home != null ? Math.max(0.01, home) * 0.55 : 0.1 }, + { scoreline: '2-1', w: home != null ? Math.max(0.01, home) * 0.35 : 0.08 }, + { scoreline: '0-0', w: draw != null ? draw * 0.55 : 0.1 }, + { scoreline: '1-1', w: draw != null ? draw * 0.45 : 0.1 }, + { scoreline: '0-1', w: away != null ? Math.max(0.01, away) * 0.55 : 0.1 }, + { scoreline: '1-2', w: away != null ? Math.max(0.01, away) * 0.35 : 0.08 } + ]; + const sum = weights.reduce((a, b) => a + b.w, 0) || 1; + for (const row of weights.sort((a, b) => b.w - a.w).slice(0, 5)) { + top_scorelines.push({ + scoreline: row.scoreline, + approx_mass: round2(row.w / sum), + method: 'heuristic_ml_weights_not_poisson' + }); + } + + const coherence_status = residuals.length + ? 'tension' + : (matrix_status === 'complete' ? 'ok_heuristic' : 'incomplete_matrix'); + + return { + coherence_status, + cross_market_residuals: residuals, + top_scorelines, + distribution_note: 'Not a full joint_score_distribution_90m; use as triage only.', + market_implied_shape_ref: shape?.central_thesis ?? null, + spreads_present: Boolean(spreads?.count || spreads?.lines?.length), + // Ship the ruleset with the verdict so the buyer can audit the cutoffs. + residual_thresholds: { ...CROSS_MARKET_RESIDUAL_THRESHOLDS } + }; +} + +export function extractFootballFixture(input = {}, options = {}) { + if (options.footballFixture && typeof options.footballFixture === 'object') { + return options.footballFixture; + } + const raw = input.football && typeof input.football === 'object' + ? input.football + : (input.fixture && typeof input.fixture === 'object' ? input.fixture : null); + if (!raw) return null; + return { + requested_window: raw.requested_window ?? raw.window ?? null, + scheduled_time_utc: raw.scheduled_time_utc ?? raw.kickoff_utc ?? raw.start_time ?? null, + scheduled_time_beijing: raw.scheduled_time_beijing ?? null, + fixture_sources: Array.isArray(raw.fixture_sources) ? raw.fixture_sources : (raw.fixture_sources ? [raw.fixture_sources] : []), + market_fixture_match: raw.market_fixture_match ?? raw.match ?? null, + home_team: raw.home_team ?? raw.home ?? null, + away_team: raw.away_team ?? raw.away ?? null, + competition: raw.competition ?? null, + verified: raw.verified === true + }; +} + +export function classifyFootballGroup(row) { + const st = String(row.sports_market_type || '').toLowerCase(); + const gt = String(row.group_item_title || '').trim(); + const text = `${gt} ${row.title || ''} ${row.slug || ''}`.toLowerCase(); + + if (st.includes('team_to_advance') || /team to advance/.test(text)) return 'advance'; + if (st.includes('both_teams_to_score') || /\bbtts\b|both teams to score/.test(text)) return 'btts'; + if (st.includes('extra_time') || /extra time/.test(text)) return 'extra_time'; + if (st.includes('penalty') || /penalty shootout/.test(text)) return 'penalty_shootout'; + if (st.includes('exact_score') || /exact score|correct score/.test(text)) return 'exact_score'; + if (st.includes('corner') || /corner/.test(text)) return 'corners'; + if (st.includes('first_to_score') || /first to score|first goal/.test(text)) return 'first_to_score'; + if (st.includes('player_') || /player prop|anytime goalscorer|shots on target/.test(text)) { + return 'player_props'; + } + if (st.includes('first_half') || /1st half|first half|halftime/.test(text)) return 'first_half'; + if (st.includes('second_half') || /2nd half|second half/.test(text)) return 'second_half'; + + // Team totals: sportsMarketType or "France O/U 1.5" (not bare "O/U 1.5") + if (st.includes('team_total') || st === 'soccer_team_totals') return 'team_totals'; + if (/o\/u|over\/under/.test(gt) && !/^[ou]\/u\s*\d/i.test(gt) && !/^over\/under\s*\d/i.test(gt)) { + return 'team_totals'; + } + + if (st.includes('spread') || /spread|handicap|\(-?\d/.test(text)) return 'spreads_ladder'; + + // Match totals: bare O/U lines + if (st === 'totals' || /^[ou]\/u\s*\d/i.test(gt) || /^over\/under\s*\d/i.test(gt)) { + return 'totals_ladder'; + } + if (/o\/u|over\/under|total/.test(text) && !/team total|1st half|2nd half|first half|second half/.test(text)) { + return 'totals_ladder'; + } + + if (st === 'moneyline' || /moneyline/.test(st) || (/\bdraw\b/.test(text) && /vs\.|vs /.test(text))) { + return 'moneyline_90m'; + } + + return 'other'; +} + +export function detectFootballMarketType({ market, eventBundle, eventMatrix = [] } = {}) { + const blob = [ + market?.title, + market?.slug, + market?.group_item_title, + eventBundle?.title, + eventBundle?.slug, + ...(eventMatrix || []).flatMap((row) => [row.title, row.group_item_title, row.slug]) + ].filter(Boolean).join(' ').toLowerCase(); + + const looksMatch = /\bvs\.?\b|\bv\b|versus|moneyline|90m|team to advance/.test(blob); + const looksOutright = /outright|season winner|league winner|cup winner|championship|title winner|to win (?:the )?.*(?:cup|league|championship|tournament)|winner of .*(?:season|league|cup|championship)|lift (?:the )?cup|world.?cup.*winner|premier league.*winner|champions league.*winner/.test(blob); + if (looksOutright && !looksMatch) return 'outright_season'; + if (looksOutright && /world.?cup.*winner|season winner|league winner|cup winner|outright/.test(blob)) { + return 'outright_season'; + } + return 'match'; +} + +function buildFootballOutrightCategory({ market, eventBundle, classified }) { + const rows = (classified || []).map((row) => ({ + ...row, + market_group: 'outright_winner' + })); + const leaderboard = rows + .filter((row) => Number.isFinite(row.yes)) + .map((row) => ({ + label: cleanOutrightLabel(row), + yes: row.yes, + slug: row.slug, + best_ask: row.best_ask, + best_bid: row.best_bid, + volume_24h_usd: row.volume_24h_usd, + is_primary: row.is_primary === true + })) + .filter((row) => !isPlaceholderOutrightRow(row)) + .sort((a, b) => b.yes - a.yes || (b.volume_24h_usd || 0) - (a.volume_24h_usd || 0)); + + const leader = leaderboard[0] || null; + const runner = leaderboard[1] || null; + const yesMass = leaderboard.reduce((sum, row) => sum + (row.yes || 0), 0); + const missing = leaderboard.length >= 2 ? [] : ['outright_winner_field']; + const residuals = []; + if (yesMass > 1.15) { + residuals.push({ + type: 'yes_mass_over_one', + note: `Outright yes-mass sums to ≈${round2(yesMass)}; field may be overlapping or incomplete.` + }); + } + if (leader && runner && leader.yes - runner.yes < 0.05 && leader.yes > 0.20) { + residuals.push({ + type: 'tight_outright_leaderboard', + note: 'Leader and runner-up are tightly priced; avoid over-reading a single favorite.' + }); + } + + const central_thesis = leader + ? `Outright leaderboard leads "${leader.label}" at yes≈${leader.yes}` + + (runner ? ` vs "${runner.label}" ≈${runner.yes}` : '') + + '. This is a season/cup winner surface, not a 90m match state map.' + : 'Outright season/cup market detected, but priced team-winner rows are missing.'; + + const matrix_status = missing.length ? 'incomplete' : 'complete'; + const tradability_cap = missing.length ? 'weak' : null; + + return { + category: 'football', + category_depth: 'enriched', + market_type: 'outright_season', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + sibling_event_slugs: eventBundle?.sibling_event_slugs || [], + linked_event_count: eventBundle?.linked_event_count + ?? (1 + (eventBundle?.sibling_event_slugs?.length || 0)), + discovery: eventBundle?.discovery || null, + related_market_count: rows.length, + group_counts: { outright_winner: rows.length }, + matrix_status, + missing_market_groups: missing, + match_state_map_diagnostic: { + status: 'not_applicable_outright', + missing_groups_if_forced: REQUIRED_GROUPS, + note: 'Outright markets do not have home/draw/away, totals, spreads, or BTTS state-map requirements.' + }, + market_surface: { + outright_winner: rows.map(compactRow), + outright_leaderboard: leaderboard.slice(0, 12) + }, + market_implied_shape: { + leader: leader ? { label: leader.label, yes: leader.yes } : null, + runner_up: runner ? { label: runner.label, yes: runner.yes } : null, + yes_mass_sum: round2(yesMass), + central_thesis + }, + fixture: { + fixture_status: 'not_applicable', + note: 'Season/cup outright; single-match fixture gate not applicable.' + }, + expression_comparison: { + candidates: leaderboard.slice(0, 8).map((row) => ({ + expression: 'outright_winner', + market: row.label, + slug: row.slug, + yes: row.yes, + ask: row.best_ask, + path: 'Needs team to win the named season/cup/tournament.', + why_consider: 'Direct expression of the outright thesis.', + why_not: 'Long horizon, field/definition risk, and no match-state hedge.' + })), + recommended: null, + rule: 'Outrights require field completeness and definition checks; do not map to 90m home/draw/away.' + }, + recommended_expression: null, + default_action_hint: missing.length ? 'no_trade' : 'use_decision_card_after_field_definition_check', + tradability_cap, + tradability_reasons: missing.length ? ['football_outright_field_thin'] : [], + central_thesis, + coherence: { + coherence_status: residuals.length ? 'tension' : (matrix_status === 'complete' ? 'ok_heuristic' : 'incomplete_matrix'), + cross_market_residuals: residuals, + distribution_note: 'Outright leaderboard only; no 90m scoreline distribution.' + }, + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + skill_alignment: { + source: 'pm-football-match_outright_season_extension', + included: ['outright_leaderboard', 'yes_mass_sanity', 'match_state_map_diagnostic'], + excluded_local_only: ['bankroll_pct', 'news_scrape', 'full_field_fundamental_model'] + } + }; +} + +function evaluateFixtureGate(market, eventBundle, fixture) { + const gammaStart = eventBundle?.start_time || market?.start_time || null; + const gammaEnd = eventBundle?.end_date || market?.end_date || null; + + if (fixture?.verified === true && fixture.market_fixture_match === 'yes') { + return { + fixture_status: 'ok', + market_fixture_match: 'yes', + scheduled_time_utc: fixture.scheduled_time_utc || gammaStart, + scheduled_time_beijing: fixture.scheduled_time_beijing, + fixture_sources: fixture.fixture_sources?.length + ? fixture.fixture_sources + : ['caller_verified'], + note: 'Caller marked fixture verified and matching the market.' + }; + } + + if (fixture?.market_fixture_match === 'no') { + return { + fixture_status: 'failed_or_unverified', + market_fixture_match: 'no', + scheduled_time_utc: fixture.scheduled_time_utc || gammaStart, + fixture_sources: fixture.fixture_sources || [], + note: 'Caller reports market/fixture mismatch — no_trade.' + }; + } + + // Without independent verification, Gamma kickoff alone is weak evidence. + if (gammaStart) { + return { + fixture_status: 'unverified', + market_fixture_match: 'unclear', + scheduled_time_utc: gammaStart, + scheduled_time_end_or_resolve: gammaEnd, + fixture_sources: ['polymarket_gamma_startTime'], + note: 'Gamma startTime present but not independently verified. Pass football.verified=true + sources for fixture_status=ok.' + }; + } + + return { + fixture_status: 'failed_or_unverified', + market_fixture_match: 'unclear', + scheduled_time_utc: null, + fixture_sources: [], + note: 'No kickoff on Gamma and no caller verification — stop per pm-football-match fixture gate.' + }; +} + +function summarizeMoneyline(rows) { + const out = { home: null, draw: null, away: null, raw: rows.map(compactRow) }; + for (const row of rows) { + const label = `${row.group_item_title || ''} ${row.title || ''}`.toLowerCase(); + const item = { label: row.group_item_title || row.title, yes: row.yes, slug: row.slug, ask: row.best_ask }; + if (/\bdraw\b/.test(label)) out.draw = item; + else if (!out.home) out.home = item; + else out.away = item; + } + // If three moneylines without clear draw tag, keep order by matrix + if (!out.draw && rows.length >= 3) { + out.home = { label: rows[0].group_item_title || rows[0].title, yes: rows[0].yes, slug: rows[0].slug, ask: rows[0].best_ask }; + out.draw = { label: rows[1].group_item_title || rows[1].title, yes: rows[1].yes, slug: rows[1].slug, ask: rows[1].best_ask }; + out.away = { label: rows[2].group_item_title || rows[2].title, yes: rows[2].yes, slug: rows[2].slug, ask: rows[2].best_ask }; + } + return out; +} + +function summarizeLadder(rows, kind) { + const lines = rows + .map((row) => { + const line = extractLineNumber(`${row.group_item_title || ''} ${row.title || ''} ${row.slug || ''}`); + return { + label: row.group_item_title || row.title, + line, + yes: row.yes, + ask: row.best_ask, + slug: row.slug + }; + }) + .filter((r) => r.yes !== null) + .sort((a, b) => (a.line ?? 0) - (b.line ?? 0)); + + const nearCoin = lines + .filter((l) => l.yes >= 0.35 && l.yes <= 0.65) + .sort((a, b) => Math.abs(0.5 - a.yes) - Math.abs(0.5 - b.yes))[0] ?? null; + + return { kind, lines, pivot: nearCoin, count: lines.length }; +} + +function summarizeTeamTotals(rows) { + return rows.map((row) => ({ + label: row.group_item_title || row.title, + yes: row.yes, + ask: row.best_ask, + slug: row.slug, + line: extractLineNumber(`${row.group_item_title || ''} ${row.slug || ''}`) + })); +} + +function buildImpliedShape({ moneyline, totals, spreads, btts, advance, teamTotals, missing_market_groups = [] }) { + const homeYes = moneyline.home?.yes; + const drawYes = moneyline.draw?.yes; + const awayYes = moneyline.away?.yes; + const fav = pickFavorite(moneyline); + const ou25 = totals.lines.find((l) => l.line === 2.5) || totals.pivot; + const spread15 = spreads.lines.find((l) => Math.abs((l.line ?? 0) - 1.5) < 0.01); + + const states = []; + if (fav && homeYes !== null && homeYes >= 0.55) { + states.push('favorite_leans_90m_win'); + } + if (drawYes !== null && drawYes >= 0.22) { + states.push('draw_has_material_mass'); + } + if (ou25 && ou25.yes >= 0.55) states.push('market_leans_open_game_over_2pt5'); + if (ou25 && ou25.yes <= 0.45) states.push('market_leans_tight_under_2pt5'); + if (btts?.yes >= 0.55) states.push('btts_likely'); + if (btts?.yes <= 0.45) states.push('btts_unlikely'); + if (spread15 && fav && spread15.yes < (fav.yes ?? 1) - 0.15) { + states.push('favorite_win_but_margin_not_fully_priced'); + } + if (advance?.yes != null && fav?.yes != null && advance.yes > fav.yes + 0.08) { + states.push('advance_richer_than_90m_ml_knockout_variance'); + } + + const missingRequired = (missing_market_groups || []).filter((m) => !m.endsWith('_optional')); + let central_thesis = missingRequired.length + ? `Match state map incomplete: missing ${missingRequired.join(', ')}.` + : 'Insufficient structure for a sharp state map.'; + if (states.includes('favorite_leans_90m_win') && states.includes('market_leans_tight_under_2pt5')) { + central_thesis = 'Favorite favored in 90m with a relatively tight totals regime — prefer expressions that do not require a blowout.'; + } else if (states.includes('favorite_leans_90m_win') && states.includes('market_leans_open_game_over_2pt5')) { + central_thesis = 'Favorite favored with an open-game totals lean — ML / team totals / overs may share the same path; compare prices.'; + } else if (states.includes('draw_has_material_mass')) { + central_thesis = 'Draw carries material probability — favorite ML is not a free lunch; check spreads and unders.'; + } else if (fav) { + central_thesis = `Market favorite leans ${fav.label} in 90m (Yes≈${fav.yes}). Compare advance/spreads/totals before choosing expression.`; + } + if (missingRequired.length && !central_thesis.includes('missing')) { + central_thesis += ` Missing groups limiting match state map: ${missingRequired.join(', ')}.`; + } + + return { + moneyline: { + home: moneyline.home, + draw: moneyline.draw, + away: moneyline.away + }, + totals_pivot: ou25, + spreads_note: spread15 || spreads.pivot, + btts, + advance, + team_totals_sample: teamTotals.slice(0, 6), + state_flags: states, + central_thesis + }; +} + +function buildExpressionComparison({ moneyline, totals, spreads, btts, advance, shape }) { + const candidates = []; + if (moneyline.home) { + candidates.push({ + expression: 'home_90m_ml', + market: moneyline.home.label, + slug: moneyline.home.slug, + yes: moneyline.home.yes, + ask: moneyline.home.ask, + path: 'Needs home win in 90 minutes.', + why_consider: 'Direct 90m result expression.', + why_not: 'Ignores knockout advance paths; can be expensive vs advance.', + aligns_with_thesis: !shape.state_flags.includes('draw_has_material_mass') + || (moneyline.home.yes != null && moneyline.home.yes < 0.55) + }); + } + if (moneyline.draw) { + candidates.push({ + expression: 'draw_90m', + market: moneyline.draw.label, + slug: moneyline.draw.slug, + yes: moneyline.draw.yes, + ask: moneyline.draw.ask, + path: 'Needs 90m draw.', + why_consider: 'Prices tactical/tight-game thesis.', + why_not: 'Binary on draw; no margin for favorite win.', + aligns_with_thesis: shape.state_flags.includes('draw_has_material_mass') + || shape.state_flags.includes('market_leans_tight_under_2pt5') + }); + } + if (advance) { + candidates.push({ + expression: 'team_to_advance', + market: advance.label || 'Team to Advance', + slug: advance.slug, + yes: advance.yes, + ask: advance.ask, + path: 'Needs side to win tie (may include ET/PEN).', + why_consider: 'Wider path than 90m ML in knockout.', + why_not: 'Often richer/more expensive than 90m ML; wrong pick when thesis is tight/draw-heavy.', + aligns_with_thesis: shape.state_flags.includes('advance_richer_than_90m_ml_knockout_variance') + && !shape.state_flags.includes('draw_has_material_mass') + }); + } + const ou = shape.totals_pivot; + if (ou) { + candidates.push({ + expression: 'totals_pivot', + market: ou.label, + slug: ou.slug, + yes: ou.yes, + ask: ou.ask, + path: 'Needs goals relative to the pivot line (check adjacent O/U).', + why_consider: 'Expresses open vs tight game without picking a winner.', + why_not: 'Must compare adjacent ladder lines — single O/U is incomplete alone.', + aligns_with_thesis: shape.state_flags.includes('market_leans_tight_under_2pt5') + || shape.state_flags.includes('market_leans_open_game_over_2pt5') + || shape.state_flags.includes('draw_has_material_mass') + }); + } + if (btts) { + candidates.push({ + expression: 'btts', + market: btts.label || 'BTTS', + slug: btts.slug, + yes: btts.yes, + ask: btts.ask, + path: 'Both teams score.', + why_consider: 'Aligns with open-game / both-attacks thesis.', + why_not: 'Orthogonal to pure favorite-win thesis.', + aligns_with_thesis: shape.state_flags.includes('btts_likely') + || shape.state_flags.includes('market_leans_open_game_over_2pt5') + }); + } + + const ladder_context = buildAdjacentLadderContext(totals, spreads); + + // Consistency gate (pm-football-match): central_thesis → shape → expression must cohere. + // Prefer expressions that align with the dominant thesis; never pick advance when draw mass is the story. + let recommended = null; + const flags = shape.state_flags || []; + if (flags.includes('draw_has_material_mass')) { + recommended = candidates.find((c) => c.expression === 'draw_90m') + || candidates.find((c) => c.expression === 'totals_pivot') + || null; + } else if (flags.includes('market_leans_tight_under_2pt5') && ou) { + recommended = candidates.find((c) => c.expression === 'totals_pivot') || null; + } else if (flags.includes('advance_richer_than_90m_ml_knockout_variance') && advance) { + recommended = candidates.find((c) => c.expression === 'team_to_advance') || null; + } else if (flags.includes('market_leans_open_game_over_2pt5') && (ou || btts)) { + recommended = candidates.find((c) => c.expression === 'totals_pivot') + || candidates.find((c) => c.expression === 'btts') + || null; + } else if (moneyline.home) { + recommended = candidates.find((c) => c.expression === 'home_90m_ml') || null; + } + + const coherence_ok = !recommended + || recommended.aligns_with_thesis !== false + || !flags.includes('draw_has_material_mass') + || recommended.expression === 'draw_90m' + || recommended.expression === 'totals_pivot'; + + if (!coherence_ok) { + recommended = candidates.find((c) => c.expression === 'draw_90m') + || candidates.find((c) => c.expression === 'totals_pivot') + || null; + } + + return { + candidates, + ladder_context, + recommended: recommended + ? { + ...recommended, + note: 'Heuristic expression pick from market shape only — not a buy tip; run decision-card for price_status/edge.', + coherence_with_thesis: true + } + : null, + rule: 'Always compare ≥2 expressions; never recommend a total/handicap without adjacent ladder context; thesis and recommended expression must cohere.' + }; +} + +function buildAdjacentLadderContext(totals, spreads) { + const totalsLines = (totals?.lines || []).slice().sort((a, b) => (a.line ?? 0) - (b.line ?? 0)); + const spreadLines = (spreads?.lines || []).slice().sort((a, b) => (a.line ?? 0) - (b.line ?? 0)); + const pivot = totals?.pivot || null; + const adjacentTotals = pivot + ? totalsLines.filter((l) => l.line != null && Math.abs(l.line - (pivot.line ?? 2.5)) <= 1.01) + : totalsLines.slice(0, 5); + + return { + totals_ladder_count: totalsLines.length, + spreads_ladder_count: spreadLines.length, + totals_adjacent_to_pivot: adjacentTotals.map((l) => ({ + label: l.label, + line: l.line, + yes: l.yes, + ask: l.ask, + slug: l.slug + })), + spreads_sample: spreadLines.slice(0, 6).map((l) => ({ + label: l.label, + line: l.line, + yes: l.yes, + ask: l.ask, + slug: l.slug + })), + note: totalsLines.length < 2 + ? 'Totals ladder thin — do not treat a single O/U as sufficient.' + : 'Compare adjacent O/U and spread lines before locking an interval expression.' + }; +} + +function pickFavorite(moneyline) { + const sides = [moneyline.home, moneyline.away].filter(Boolean); + if (!sides.length) return null; + return sides.slice().sort((a, b) => (b.yes ?? 0) - (a.yes ?? 0))[0]; +} + +function firstYes(rows) { + if (!rows?.length) return null; + const row = rows[0]; + return { label: row.group_item_title || row.title, yes: row.yes, ask: row.best_ask, slug: row.slug }; +} + +function extractLineNumber(text) { + const m = String(text).match(/(-?\d+(?:\.\d+)?)\s*(?:pt)?/i) || String(text).match(/([OU])\s*(\d+(?:\.\d+)?)/i); + if (!m) return null; + if (m[2] && (m[1] === 'O' || m[1] === 'U')) return Number(m[2]); + const n = Number(m[1]); + return Number.isFinite(n) ? n : null; +} + +function compactRow(row) { + return { + slug: row.slug, + title: row.title, + group_item_title: row.group_item_title, + sports_market_type: row.sports_market_type, + market_group: row.market_group, + yes: row.yes, + best_ask: row.best_ask, + best_bid: row.best_bid, + volume_24h_usd: row.volume_24h_usd, + is_primary: row.is_primary + }; +} + +function cleanOutrightLabel(row) { + const raw = row.group_item_title || row.title || row.slug || 'unknown'; + let label = String(raw) + .replace(/^will\s+/i, '') + .replace(/\s+win\s+(?:the\s+)?(?:english\s+)?(?:premier league|epl|champions league|world cup|fifa world cup|championship|cup|tournament|serie a|la liga|bundesliga).*$/i, '') + .replace(/\s+to\s+win\s+(?:the\s+)?.+$/i, '') + .replace(/\?$/, '') + .trim(); + // Avoid opaque placeholders when title parsing failed + if (!label || /^team\s*[a-z]$/i.test(label) || /^outcome\s*\d+$/i.test(label)) { + const fromSlug = String(row.slug || '') + .replace(/^will-/, '') + .replace(/-win-the-.*$/, '') + .replace(/-/g, ' ') + .trim(); + if (fromSlug && !/^team\s*[a-z]$/i.test(fromSlug) && !/^another team/i.test(fromSlug)) { + label = fromSlug; + } + } + return label || String(raw); +} + +function isPlaceholderOutrightRow(row) { + const label = String(row.label || '').trim(); + const slug = String(row.slug || '').toLowerCase(); + if (/^team\s*[a-z]$/i.test(label) || /^other$/i.test(label) || /^another team/i.test(label)) { + return true; + } + if (/will-team-[a-z]-|will-another-team-/.test(slug)) return true; + // Ghost quotes: flat 0.5 with no bid / ask=1 / zero volume + const vol = Number(row.volume_24h_usd) || 0; + const ask = row.best_ask; + const bid = row.best_bid; + if (vol <= 0 && (ask == null || ask >= 0.99) && (bid == null) && Number(row.yes) === 0.5) { + return true; + } + return false; +} + +function groupBy(items, fn) { + const out = {}; + for (const item of items) { + const key = fn(item) || 'other'; + if (!out[key]) out[key] = []; + out[key].push(item); + } + return out; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-macro-fed.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-macro-fed.mjs new file mode 100644 index 00000000..1f8cf3f7 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-macro-fed.mjs @@ -0,0 +1,155 @@ +// Macro Fed rate-decision category plugin (L1) for PM Event Readout. +// Parses FOMC decision brackets into a priced ladder; no scraping, no orders. + +import { round2 } from './pm-gamma-market.mjs'; + +const EXPECTED_BUCKETS = ['hold', 'cut_25', 'hike_25']; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix + */ +export function enrichMacroFedCategory({ market, eventBundle, eventMatrix }) { + const rows = (eventMatrix || []).map((row) => ({ + ...row, + bucket: classifyRateDecisionBucket(row) + })); + const classified = rows.filter((row) => row.bucket.kind !== 'other'); + const leaderboard = classified + .filter((row) => Number.isFinite(row.yes)) + .map((row) => ({ + bucket: row.bucket.id, + direction: row.bucket.direction, + move_bps: row.bucket.move_bps, + label: row.group_item_title || row.title || row.slug || row.bucket.id, + yes: row.yes, + slug: row.slug, + is_primary: row.is_primary === true + })) + .sort((a, b) => b.yes - a.yes); + + const bucketIds = new Set(classified.map((row) => row.bucket.id)); + const missing_brackets = EXPECTED_BUCKETS.filter((bucket) => !bucketIds.has(bucket)); + if (!classified.some((row) => row.bucket.direction === 'cut')) missing_brackets.push('any_cut'); + if (!classified.some((row) => row.bucket.direction === 'hike')) missing_brackets.push('any_hike'); + + const yesMass = leaderboard.reduce((sum, row) => sum + (row.yes || 0), 0); + const expectedMove = buildExpectedMove(leaderboard); + const leader = leaderboard[0] || null; + const runner = leaderboard[1] || null; + const ladder_status = leaderboard.length >= 3 + ? (missing_brackets.length ? 'partial_rate_ladder' : 'rate_ladder') + : (leaderboard.length >= 2 ? 'thin_rate_pair' : 'thin'); + + const residuals = []; + if (yesMass > 1.15) { + residuals.push({ + type: 'yes_mass_over_one', + note: `Fed decision yes-mass sums to ≈${round2(yesMass)}; brackets may overlap or include nested definitions.` + }); + } + if (leader && runner && leader.yes - runner.yes < 0.05 && leader.yes > 0.30) { + residuals.push({ + type: 'tight_rate_leaderboard', + note: 'Top two decision buckets are tightly priced; thesis should stay bracket-aware.' + }); + } + if (missing_brackets.includes('hold')) { + residuals.push({ + type: 'missing_hold_bracket', + note: 'No hold/no-change bracket found, so cut/hike mass cannot be centered cleanly.' + }); + } + + const central_thesis = leader + ? `Fed ladder leads "${leader.label}" at yes≈${leader.yes}` + + (runner ? ` vs "${runner.label}" ≈${runner.yes}` : '') + + (expectedMove.status === 'estimated' ? `; implied expected move ≈${expectedMove.expected_move_bps} bps.` : '.') + : `Insufficient Fed decision brackets for a central thesis; missing ${missing_brackets.join(', ') || 'priced buckets'}.`; + + return { + category: 'macro_fed', + category_depth: 'enriched', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + related_market_count: rows.length, + ladder_status, + missing_brackets, + leaderboard: leaderboard.slice(0, 10), + yes_mass_sum: round2(yesMass), + implied_expected_move: expectedMove, + market_implied_shape: { + leader: leader ? { bucket: leader.bucket, label: leader.label, yes: leader.yes, move_bps: leader.move_bps } : null, + runner_up: runner ? { bucket: runner.bucket, label: runner.label, yes: runner.yes, move_bps: runner.move_bps } : null, + central_thesis + }, + coherence: { + coherence_status: residuals.length ? 'tension' : (ladder_status === 'thin' ? 'incomplete_matrix' : 'ok_heuristic'), + cross_market_residuals: residuals + }, + central_thesis, + default_action_hint: ladder_status === 'thin' ? 'no_trade' : 'use_decision_card_after_macro_definition_check', + tradability_cap: ladder_status === 'thin' ? 'weak' : null, + tradability_reasons: ladder_status === 'thin' ? ['macro_fed_ladder_thin'] : [], + hard_gate: 'no_orders_no_account_mutation_no_macro_scrape', + skill_alignment: { + source: 'macro_fed_rate_decision_l1', + included: ['rate_decision_bucket_parse', 'yes_price_leaderboard', 'expected_move_heuristic', 'coherence_residuals'], + excluded_local_only: ['live_cme_fedwatch_scrape', 'bankroll', 'news_scrape'] + } + }; +} + +export function classifyRateDecisionBucket(row) { + const text = `${row.group_item_title || ''} ${row.title || ''} ${row.slug || ''}`.toLowerCase(); + if (/no change|unchanged|hold|leave (?:rates|interest rates)|keeps? (?:rates|interest rates)/.test(text)) { + return { id: 'hold', kind: 'rate_decision', direction: 'hold', move_bps: 0 }; + } + + const bps = extractBps(text); + if (/decrease|cut|lower|reduc/.test(text) || /-\s*\d+\s*bps/.test(text)) { + const move = bps || 25; + return { id: `cut_${move}`, kind: 'rate_decision', direction: 'cut', move_bps: -move }; + } + if (/increase|hike|raise/.test(text) || /\+\s*\d+\s*bps/.test(text)) { + const move = bps || 25; + return { id: `hike_${move}`, kind: 'rate_decision', direction: 'hike', move_bps: move }; + } + if (/\b\d+\s*bps\b/.test(text)) { + return { id: `other_${bps}`, kind: 'other_rate_bracket', direction: 'other', move_bps: bps }; + } + return { id: 'other', kind: 'other', direction: 'other', move_bps: null }; +} + +function buildExpectedMove(leaderboard) { + const exact = leaderboard.filter((row) => Number.isFinite(row.move_bps) && Number.isFinite(row.yes)); + const hasHold = exact.some((row) => row.move_bps === 0); + const hasCutOrHike = exact.some((row) => row.move_bps !== 0); + if (exact.length < 3 || !hasHold || !hasCutOrHike) { + return { + status: 'insufficient_brackets', + expected_move_bps: null, + bracket_count: exact.length, + note: 'Need hold plus multiple cut/hike brackets for a useful expected-move heuristic.' + }; + } + const mass = exact.reduce((sum, row) => sum + row.yes, 0); + if (mass <= 0) { + return { status: 'insufficient_brackets', expected_move_bps: null, bracket_count: exact.length }; + } + const weighted = exact.reduce((sum, row) => sum + row.yes * row.move_bps, 0) / mass; + return { + status: 'estimated', + expected_move_bps: round2(weighted), + bracket_count: exact.length, + method: 'yes_price_weighted_rate_brackets_not_cme' + }; +} + +function extractBps(text) { + const m = String(text).match(/(\d{1,3})\s*(?:bps|bp|basis points?)/); + if (!m) return null; + const n = Number(m[1]); + return Number.isFinite(n) ? n : null; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-musk.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-musk.mjs new file mode 100644 index 00000000..092477c1 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-musk.mjs @@ -0,0 +1,281 @@ +// Musk tweet-count category plugin (L1) for PM Event Readout. +// Framework: research/2026-07-09-pm-event-analyst-framework.md +// Adds ladder distribution + optional count snapshot. No Leo relay, no orders, no sizing. + +import { round2 } from './pm-gamma-market.mjs'; + +/** + * @param {object} args + * @param {object} args.market primary normalized market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix from L0 + * @param {object} [args.snapshot] { current_count, hours_left, window_time, window_label, recent_6h, recent_12h } + */ +export function enrichMuskCategory({ market, eventBundle, eventMatrix, snapshot = null }) { + const buckets = (eventMatrix || []).map((row) => { + const label = normalizeBucketLabel(row.group_item_title || row.title); + return { + bucket: label, + slug: row.slug, + yes: row.yes, + volume_24h_usd: row.volume_24h_usd, + best_ask: row.best_ask, + best_bid: row.best_bid, + spread: row.spread, + is_primary: row.is_primary + }; + }).sort(compareBuckets); + + const yesSum = buckets.reduce((sum, b) => sum + (Number.isFinite(b.yes) ? b.yes : 0), 0); + const market_implied_distribution = buckets.map((b) => ({ + bucket: b.bucket, + yes: b.yes, + share_of_yes_mass: yesSum > 0 && Number.isFinite(b.yes) ? round2(b.yes / yesSum) : null + })); + + const modal = buckets + .filter((b) => Number.isFinite(b.yes)) + .slice() + .sort((a, b) => b.yes - a.yes)[0] ?? null; + + const count = snapshot && Number.isFinite(Number(snapshot.current_count)) + ? Number(snapshot.current_count) + : null; + const hoursLeft = snapshot && Number.isFinite(Number(snapshot.hours_left)) + ? Number(snapshot.hours_left) + : null; + + const pace = buildPaceNotes(count, hoursLeft, snapshot); + const countVsLadder = buildCountVsLadder(count, buckets, modal); + const thesis = buildThesis(modal, count, countVsLadder, yesSum); + + const freshness = snapshot?.snapshot_time || count !== null + ? { + status: count !== null ? 'provided' : 'missing', + snapshot_time: snapshot?.snapshot_time ?? null, + detail: count !== null + ? 'Caller-provided count snapshot; ASP does not scrape X.' + : 'Pass musk.current_count (+ hours_left) for pace notes; ASP does not scrape X.' + } + : { status: 'missing', snapshot_time: null, detail: 'Pass musk.current_count (+ hours_left) for pace notes; ASP does not scrape X.' }; + + // Without a live count, ladder shape alone must not look highly tradable. + const tradability_cap = count === null ? 'medium' : null; + const tradability_reasons = count === null + ? ['musk_count_snapshot_missing'] + : []; + + const plugin = { + category: 'musk', + category_depth: 'enriched', + window: snapshot?.window_label || eventBundle?.title || market?.title || null, + count_source_freshness: freshness, + current_count: count, + hours_left: hoursLeft, + recent_6h: snapshot?.recent_6h ?? null, + recent_12h: snapshot?.recent_12h ?? null, + full_bucket_surface: buckets, + market_implied_distribution, + yes_mass_sum: round2(yesSum), + modal_bucket: modal ? { bucket: modal.bucket, yes: modal.yes, slug: modal.slug } : null, + pace_notes: pace, + count_vs_ladder: countVsLadder, + direct_bucket_thesis: thesis, + split_ladder_paper: { + status: 'not_computed_in_asp_v1', + detail: 'Paper split-ladder / bankroll sizing stays in local pm-musk-count skill; ASP returns ladder + count context only.' + }, + default_action_hint: 'use_decision_card_or_local_skill_for_size', + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + tradability_cap, + tradability_reasons + }; + + plugin.batch2_public = toMuskBatch2PublicCard(plugin, { + matrix_status: buckets.length >= 3 ? 'complete' : 'incomplete', + event_slug: eventBundle?.slug || null, + market_slug: market?.slug || modal?.slug || null + }); + + return plugin; +} + +/** + * Map Musk L1 plugin → Batch 2 shared public schema + * (research/2026-07-09-pm-category-analyst-batch2-brief.md). + * Never emits bankroll / leo_private / order instructions. + */ +export function toMuskBatch2PublicCard(plugin, meta = {}) { + const countMissing = plugin?.count_source_freshness?.status === 'missing' || plugin?.current_count == null; + const modal = plugin?.modal_bucket || null; + const mapped = plugin?.count_vs_ladder?.matching_buckets?.[0] || null; + const yesMass = Number(plugin?.yes_mass_sum); + const risk_flags = []; + if (countMissing) risk_flags.push('count_snapshot_missing'); + if (Number.isFinite(yesMass) && (yesMass > 1.15 || yesMass < 0.85)) { + risk_flags.push('yes_mass_not_exclusive'); + } + if (plugin?.count_vs_ladder?.status === 'unmapped') risk_flags.push('count_unmapped_to_ladder'); + if (mapped && modal && mapped.bucket !== modal.bucket) { + risk_flags.push('count_bucket_diverges_from_modal'); + } + + // ASP is analysis-only: never return "trade" as a tip. + let action = 'watch'; + if (countMissing) action = 'skip'; + else if (plugin?.hours_left != null && Number(plugin.hours_left) <= 0) action = 'no_trade'; + + const confidence = countMissing + ? 0.25 + : (mapped && modal && mapped.bucket === modal.bucket ? 0.55 : 0.4); + + const window = plugin?.window || 'musk-window'; + const snap = plugin?.count_source_freshness?.snapshot_time || 'no-snap'; + const postmortem_key = [ + meta.event_slug || 'musk-event', + String(window).replace(/\s+/g, '_').slice(0, 48), + snap + ].join('|'); + + return { + category: 'musk', + fixture_status: countMissing ? 'failed_or_unverified' : 'ok', + matrix_status: meta.matrix_status || (plugin?.full_bucket_surface?.length >= 3 ? 'complete' : 'incomplete'), + market_implied_shape: { + modal_bucket: modal, + distribution: plugin?.market_implied_distribution || [], + yes_mass_sum: plugin?.yes_mass_sum ?? null, + count_vs_ladder: plugin?.count_vs_ladder || null + }, + thesis: plugin?.direct_bucket_thesis || '', + best_expression: modal + ? { + market: modal.slug || modal.bucket, + side: 'Yes', + why: mapped && mapped.bucket !== modal.bucket + ? `Market modal is ${modal.bucket}, but live count maps to ${mapped.bucket} — expression is shape context only, not a tip.` + : `Highest Yes-mass bucket on the ladder: ${modal.bucket} @ ${modal.yes}. Not a buy tip.` + } + : { market: '', side: '', why: 'No modal bucket available.' }, + action, + confidence, + risk_flags, + caveats: [ + 'ASP Musk card is ladder + optional caller count only; does not scrape X.', + 'action never means place an order — use local decision-card / manual gate.', + ...(plugin?.pace_notes || []) + ], + postmortem_key, + next_gate: 'Use_pm_trade_preflight_or_manual_decision_card_before_orders' + }; +} + +export function extractMuskSnapshot(input = {}, options = {}) { + if (options.muskSnapshot && typeof options.muskSnapshot === 'object') { + return options.muskSnapshot; + } + const raw = input.musk && typeof input.musk === 'object' ? input.musk : input; + const count = raw.current_count ?? raw.count ?? null; + if (count === null || count === undefined || count === '') return null; + return { + current_count: Number(count), + hours_left: raw.hours_left != null ? Number(raw.hours_left) : null, + snapshot_time: raw.snapshot_time ?? raw.as_of ?? null, + window_label: raw.window_label ?? raw.window ?? null, + recent_6h: raw.recent_6h != null ? Number(raw.recent_6h) : null, + recent_12h: raw.recent_12h != null ? Number(raw.recent_12h) : null + }; +} + +function normalizeBucketLabel(raw) { + const text = String(raw ?? '').trim(); + if (!text) return 'unknown'; + // "160-179", "<20", "240+", "40-59" + const range = text.match(/(\d+)\s*[-–]\s*(\d+)/); + if (range) return `${range[1]}-${range[2]}`; + const lt = text.match(/<\s*(\d+)/) || text.match(/(\d+)\s*or below/i); + if (lt) return `<${lt[1]}`; + const plus = text.match(/(\d+)\s*\+/); + if (plus) return `${plus[1]}+`; + return text; +} + +function parseBucketBounds(label) { + const s = String(label); + if (s.startsWith('<')) { + const n = Number(s.slice(1)); + return Number.isFinite(n) ? { lo: 0, hi: n - 1, sort: n } : null; + } + if (s.endsWith('+')) { + const n = Number(s.slice(0, -1)); + return Number.isFinite(n) ? { lo: n, hi: Infinity, sort: n } : null; + } + const m = s.match(/^(\d+)-(\d+)$/); + if (m) return { lo: Number(m[1]), hi: Number(m[2]), sort: Number(m[1]) }; + return null; +} + +function compareBuckets(a, b) { + const ba = parseBucketBounds(a.bucket); + const bb = parseBucketBounds(b.bucket); + if (ba && bb) return ba.sort - bb.sort; + return String(a.bucket).localeCompare(String(b.bucket)); +} + +function buildPaceNotes(count, hoursLeft, snapshot) { + if (count === null) { + return ['No current_count provided — market ladder only; pace unknown.']; + } + const notes = [`Current count snapshot: ${count}.`]; + if (hoursLeft === null) { + notes.push('hours_left missing — cannot pace remaining window.'); + } else if (hoursLeft <= 0) { + notes.push('Window appears ended or hours_left≤0 — treat as settlement/lock-in regime, not fresh pace.'); + } else { + notes.push(`Hours left (caller-provided): ${hoursLeft}.`); + if (snapshot?.recent_6h != null) notes.push(`Recent 6h count delta: ${snapshot.recent_6h}.`); + if (snapshot?.recent_12h != null) notes.push(`Recent 12h count delta: ${snapshot.recent_12h}.`); + } + return notes; +} + +function buildCountVsLadder(count, buckets, modal) { + if (count === null) { + return { + status: 'no_count', + matching_buckets: [], + note: 'Provide musk.current_count to map count into ladder buckets.' + }; + } + const matching = buckets.filter((b) => { + const bounds = parseBucketBounds(b.bucket); + if (!bounds) return false; + return count >= bounds.lo && count <= bounds.hi; + }); + return { + status: matching.length ? 'mapped' : 'unmapped', + matching_buckets: matching.map((b) => ({ bucket: b.bucket, yes: b.yes, slug: b.slug })), + modal_bucket: modal?.bucket ?? null, + note: matching.length + ? `Count ${count} sits in bucket(s): ${matching.map((b) => b.bucket).join(', ')}.` + : `Count ${count} did not map to a parsed bucket label — check ladder naming.` + }; +} + +function buildThesis(modal, count, countVsLadder, yesSum) { + const parts = []; + if (modal) { + parts.push(`Market modal bucket by Yes mass: ${modal.bucket} @ ${modal.yes}.`); + } + if (yesSum > 1.15 || yesSum < 0.85) { + parts.push(`Yes-mass sum across buckets is ${round2(yesSum)} (neg-risk ladders often ≠ 1; do not treat as exclusive probs).`); + } + if (count !== null && countVsLadder?.matching_buckets?.length) { + const m = countVsLadder.matching_buckets[0]; + parts.push(`Live count maps to ${m.bucket} (market Yes ${m.yes}) — compare to modal before any size decision.`); + } else if (count === null) { + parts.push('Without live count, this is ladder shape only — not a pace edge.'); + } + parts.push('Not a buy tip; use local pm-musk-count / decision-card for action and size.'); + return parts.join(' '); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-nba.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-nba.mjs new file mode 100644 index 00000000..c79c709f --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-nba.mjs @@ -0,0 +1,486 @@ +// NBA / basketball match category plugin (L1) for PM Event Readout. +// Lighter than football: moneyline + spread + totals + player-props sample. +// No bankroll, no orders, no scrape — fixture must be caller-verified when used. + +import { round2 } from './pm-gamma-market.mjs'; + +/** + * NBA shape and residual cutoffs, hoisted 2026-07-31. + * + * Same treatment already applied to football and tennis: these decide what a buyer is + * told about the market ("clear favorite", "ML vs spread mismatch"), and they were + * inline literals nobody could audit — indistinguishable from a typo once written. + * crypto-market-regime is the pattern being followed: declare the numbers, ship them + * with the verdict. + * + * Values unchanged. Retuning would need settled-game backtesting, which this service + * does not do. + */ +export const NBA_SHAPE_THRESHOLDS = Object.freeze({ + /** Favourite at or above this reads as a clear favourite. */ + clear_favorite: 0.62, + /** …and above this as a lean favourite; below it the ML is a coin flip. */ + lean_favorite: 0.55, + /** Heavy favourite paired with a tiny spread is worth flagging. */ + heavy_favorite: 0.7, + /** Spread at or under this magnitude counts as tight. */ + tight_spread_abs: 2.5, + /** Near-even ML at or below this… */ + near_even_ml: 0.55, + /** …paired with a spread at or above this magnitude is suspicious. */ + wide_spread_abs: 8, + /** Totals pivot outside this band is extreme. */ + totals_extreme_high: 0.7, + totals_extreme_low: 0.3 +}); + +const REQUIRED_GROUPS = [ + 'moneyline', + 'spreads_ladder', + 'totals_ladder' +]; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix + * @param {object} [args.fixture] + */ +export function enrichNbaCategory({ market, eventBundle, eventMatrix, fixture = null }) { + const classified = (eventMatrix || []).map((row) => ({ + ...row, + market_group: classifyNbaGroup(row) + })); + + const marketType = detectNbaMarketType({ market, eventBundle, eventMatrix }); + if (marketType === 'outright_season') { + return buildNbaOutrightCategory({ market, eventBundle, classified }); + } + + const groups = groupBy(classified, (row) => row.market_group); + const missing = []; + for (const key of REQUIRED_GROUPS) { + if (!groups[key]?.length) missing.push(key); + } + if (!groups.player_props?.length) missing.push('player_props_optional'); + if (!groups.first_half?.length) missing.push('first_half_optional'); + + const matrix_status = missing.filter((m) => !m.endsWith('_optional')).length + ? 'incomplete' + : 'complete'; + + const moneyline = summarizeBinaryMoneyline(groups.moneyline || [], market); + const spreads = summarizeLineLadder(groups.spreads_ladder || [], 'spread'); + const totals = summarizeLineLadder(groups.totals_ladder || [], 'total'); + const fixtureGate = evaluateFixtureGate(market, eventBundle, fixture); + const shape = buildImpliedShape({ moneyline, spreads, totals }); + const coherence = buildNbaCoherence({ moneyline, spreads, totals, matrix_status }); + + let tradability_cap = null; + const tradability_reasons = []; + if (fixtureGate.fixture_status !== 'ok') { + tradability_cap = 'weak'; + tradability_reasons.push('fixture_unverified_or_failed'); + } + if (matrix_status === 'incomplete') { + tradability_cap = tradability_cap || 'medium'; + tradability_reasons.push('nba_matrix_incomplete'); + } + + const market_surface = {}; + for (const [key, rows] of Object.entries(groups)) { + if (key === 'player_props') { + market_surface.player_props_sample = rows.slice(0, 8).map(compactRow); + market_surface.player_props_count = rows.length; + } else { + market_surface[key] = rows.map(compactRow); + } + } + + return { + category: 'nba', + category_depth: 'enriched', + market_type: 'match', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + sibling_event_slugs: eventBundle?.sibling_event_slugs || [], + linked_event_count: eventBundle?.linked_event_count + ?? (1 + (eventBundle?.sibling_event_slugs?.length || 0)), + discovery: eventBundle?.discovery || null, + related_market_count: classified.length, + group_counts: Object.fromEntries( + Object.entries(groups).map(([key, rows]) => [key, rows.length]) + ), + matrix_status, + missing_market_groups: missing, + market_surface, + market_implied_shape: shape, + fixture: fixtureGate, + coherence, + recommended_expression: shape.favored_side + ? { + expression: 'moneyline_favorite', + side: shape.favored_side, + yes: shape.favorite_yes, + note: 'Heuristic favorite from ML mass only — not a buy tip.' + } + : null, + default_action_hint: fixtureGate.fixture_status !== 'ok' || matrix_status === 'incomplete' + ? 'no_trade' + : 'use_decision_card_after_shape_check', + tradability_cap, + tradability_reasons, + central_thesis: shape.central_thesis, + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + skill_alignment: { + source: 'sports_generalization_nba_l1', + included: [ + 'fixture_gate', + 'moneyline_spread_totals_matrix', + 'heuristic_coherence', + 'player_props_sample' + ], + excluded_local_only: [ + 'bankroll_pct', + 'player_prop_full_dump', + 'live_injury_scrape' + ] + } + }; +} + +export function extractNbaFixture(input = {}, options = {}) { + if (options.nbaFixture && typeof options.nbaFixture === 'object') { + return options.nbaFixture; + } + const raw = input.nba && typeof input.nba === 'object' + ? input.nba + : (input.nfl && typeof input.nfl === 'object' + ? input.nfl + : (input.ufc && typeof input.ufc === 'object' + ? input.ufc + : (input.mlb && typeof input.mlb === 'object' + ? input.mlb + : (input.basketball && typeof input.basketball === 'object' + ? input.basketball + : (input.fixture && typeof input.fixture === 'object' ? input.fixture : null))))); + if (!raw) return null; + const competition = raw.competition + ?? (input.ufc ? 'UFC' : (input.mlb ? 'MLB' : (input.nfl ? 'NFL' : 'NBA'))); + return { + scheduled_time_utc: raw.scheduled_time_utc ?? raw.tipoff_utc ?? raw.kickoff_utc ?? raw.start_time ?? null, + fixture_sources: Array.isArray(raw.fixture_sources) ? raw.fixture_sources : (raw.fixture_sources ? [raw.fixture_sources] : []), + market_fixture_match: raw.market_fixture_match ?? raw.match ?? null, + home_team: raw.home_team ?? raw.home ?? raw.fighter_a ?? null, + away_team: raw.away_team ?? raw.away ?? raw.fighter_b ?? null, + competition, + verified: raw.verified === true + }; +} + +function detectNbaMarketType({ market, eventBundle, eventMatrix }) { + const blob = [ + market?.question, + market?.slug, + eventBundle?.title, + eventBundle?.slug, + ...(eventMatrix || []).flatMap((row) => [row.title, row.group_item_title, row.slug]) + ].filter(Boolean).join(' ').toLowerCase(); + const looksMatch = /\bvs\.?\b|\bv\b|versus|moneyline|spread|o\/u|over\/under|tip-?off/.test(blob); + const looksOutright = /nba finals|championship|conference|outright|season|to win the|title|mvp/.test(blob) + && /win|champion|finals|title|mvp/.test(blob); + if (looksOutright && !looksMatch) return 'outright_season'; + return 'match'; +} + +function buildNbaOutrightCategory({ market, eventBundle, classified }) { + const leaderboard = (classified || []) + .filter((row) => Number.isFinite(row.yes)) + .map((row) => ({ + label: cleanNbaOutrightLabel(row), + yes: row.yes, + slug: row.slug, + best_ask: row.best_ask, + best_bid: row.best_bid, + volume_24h_usd: row.volume_24h_usd, + is_primary: row.is_primary === true + })) + .filter((row) => !isPlaceholderNbaOutrightRow(row)) + .sort((a, b) => b.yes - a.yes || (b.volume_24h_usd || 0) - (a.volume_24h_usd || 0)); + + const leader = leaderboard[0] || null; + const runner = leaderboard[1] || null; + const yesMass = leaderboard.reduce((sum, row) => sum + (row.yes || 0), 0); + const missing = leaderboard.length >= 2 ? [] : ['outright_winner_field']; + const central_thesis = leader + ? `NBA outright/futures leaderboard leads "${leader.label}" at yes≈${leader.yes}` + + (runner ? ` vs "${runner.label}" ≈${runner.yes}` : '') + + '. Not a single-game moneyline/spread/totals map.' + : 'NBA finals/futures surface detected, but priced field rows are thin.'; + + return { + category: 'nba', + category_depth: 'enriched', + market_type: 'outright_season', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + sibling_event_slugs: eventBundle?.sibling_event_slugs || [], + linked_event_count: eventBundle?.linked_event_count + ?? (1 + (eventBundle?.sibling_event_slugs?.length || 0)), + discovery: eventBundle?.discovery || null, + related_market_count: (classified || []).length, + group_counts: { outright_winner: (classified || []).length }, + matrix_status: missing.length ? 'incomplete' : 'complete', + missing_market_groups: missing, + market_surface: { + outright_leaderboard: leaderboard.slice(0, 12) + }, + market_implied_shape: { + leader: leader ? { label: leader.label, yes: leader.yes } : null, + runner_up: runner ? { label: runner.label, yes: runner.yes } : null, + yes_mass_sum: round2(yesMass), + central_thesis + }, + fixture: { + fixture_status: 'not_applicable', + note: 'NBA finals/futures outright; tipoff fixture gate not applicable.' + }, + coherence: { + coherence_status: missing.length ? 'incomplete_matrix' : 'ok_heuristic', + cross_market_residuals: yesMass > 1.15 + ? [{ type: 'yes_mass_over_one', note: `Yes-mass ≈${round2(yesMass)}` }] + : [], + distribution_note: 'Futures leaderboard only; not a single-game state map.' + }, + recommended_expression: null, + default_action_hint: missing.length ? 'no_trade' : 'use_decision_card_after_field_definition_check', + tradability_cap: missing.length ? 'weak' : null, + tradability_reasons: missing.length ? ['nba_outright_field_thin'] : [], + central_thesis, + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + skill_alignment: { + source: 'sports_generalization_nba_outright_extension', + included: ['outright_leaderboard', 'yes_mass_sanity'], + excluded_local_only: ['injury_scrape', 'full_player_props'] + } + }; +} + +function cleanNbaOutrightLabel(row) { + const raw = row.group_item_title || row.title || row.slug || 'unknown'; + return String(raw) + .replace(/^will\s+(?:the\s+)?/i, '') + .replace(/\s+win\s+(?:the\s+)?(?:nba finals|nba championship|championship|title|conference).*$/i, '') + .replace(/\s+be\s+(?:the\s+)?(?:202\d\s+)?nba\s+(?:western|eastern)\s+conference\s+champion.*$/i, '') + .replace(/\s+be\s+(?:the\s+)?(?:nba\s+)?mvp.*$/i, '') + .replace(/\?$/, '') + .trim() || String(raw); +} + +function isPlaceholderNbaOutrightRow(row) { + const label = String(row.label || '').trim(); + const slug = String(row.slug || '').toLowerCase(); + if (/^team\s*[a-z]$/i.test(label) || /^other$/i.test(label) || /^another team/i.test(label)) { + return true; + } + if (/will-team-[a-z]-|will-another-team-/.test(slug)) return true; + const vol = Number(row.volume_24h_usd) || 0; + const ask = row.best_ask; + const bid = row.best_bid; + if (vol <= 0 && (ask == null || ask >= 0.99) && bid == null && Number(row.yes) === 0.5) { + return true; + } + return false; +} + +export function classifyNbaGroup(row) { + const st = String(row.sports_market_type || '').toLowerCase(); + const gt = String(row.group_item_title || '').trim(); + const text = `${gt} ${row.title || ''} ${row.slug || ''}`.toLowerCase(); + + if (st.includes('player') || /points|rebounds|assists|threes|pra\b|player prop/.test(text)) { + return 'player_props'; + } + if (st.includes('first_half') || /1st half|first half|halftime/.test(text)) return 'first_half'; + if (st.includes('second_half') || /2nd half|second half/.test(text)) return 'second_half'; + if (st.includes('spread') || /spread|handicap|\([+-]?\d/.test(text)) return 'spreads_ladder'; + if (st.includes('total') || /o\/u|over\/under|total points|totals/.test(text)) return 'totals_ladder'; + if (st.includes('moneyline') || st.includes('winner') || /moneyline|to win|winner/.test(text)) { + return 'moneyline'; + } + // Binary team-named markets often are ML when not O/U/spread. + if (gt && !/o\/u|over|under|spread|\+|\-/.test(gt) && (row.yes != null || row.no != null)) { + return 'moneyline'; + } + return 'other'; +} + +function evaluateFixtureGate(market, eventBundle, fixture) { + if (!fixture) { + return { + fixture_status: 'missing', + detail: 'Pass nba:{verified:true, scheduled_time_utc, market_fixture_match:"yes"} for tipoff honesty.', + scheduled_time_utc: eventBundle?.start_time || market?.start_time || null + }; + } + if (fixture.verified !== true) { + return { + fixture_status: 'unverified', + detail: 'nba.verified must be true — ASP does not scrape tipoff sources.', + ...fixture + }; + } + const match = String(fixture.market_fixture_match || '').toLowerCase(); + if (match && match !== 'yes' && match !== 'match' && match !== 'true') { + return { + fixture_status: 'mismatch', + detail: 'Caller marked market_fixture_match as not yes.', + ...fixture + }; + } + return { + fixture_status: 'ok', + detail: 'Caller-verified tipoff/fixture.', + ...fixture + }; +} + +function summarizeBinaryMoneyline(rows, market) { + if (!rows.length) return null; + const sorted = rows.slice().sort((a, b) => (b.yes ?? 0) - (a.yes ?? 0)); + const favorite = sorted[0]; + const underdog = sorted[1] || null; + return { + count: rows.length, + favorite: favorite ? { + label: favorite.group_item_title || favorite.title, + yes: favorite.yes, + slug: favorite.slug, + is_primary: favorite.is_primary + } : null, + underdog: underdog ? { + label: underdog.group_item_title || underdog.title, + yes: underdog.yes, + slug: underdog.slug + } : null, + primary_yes: market?.yes ?? favorite?.yes ?? null + }; +} + +function summarizeLineLadder(rows, kind) { + const lines = rows.map((row) => { + const label = row.group_item_title || row.title || row.slug || ''; + const line = parseLine(label); + return { + label, + line, + yes: row.yes, + ask: row.best_ask, + bid: row.best_bid, + slug: row.slug + }; + }).sort((a, b) => (a.line ?? 0) - (b.line ?? 0)); + const pivot = lines.filter((l) => l.yes != null).sort((a, b) => Math.abs((a.yes ?? 0.5) - 0.5) - Math.abs((b.yes ?? 0.5) - 0.5))[0] || null; + return { kind, count: lines.length, lines, pivot }; +} + +function parseLine(label) { + const m = String(label).match(/([+-]?\d+(?:\.\d+)?)/); + return m ? Number(m[1]) : null; +} + +function buildImpliedShape({ moneyline, spreads, totals }) { + const favoriteYes = moneyline?.favorite?.yes ?? null; + const favored_side = moneyline?.favorite?.label ?? null; + const spreadPivot = spreads?.pivot?.line ?? null; + const totalPivot = totals?.pivot?.line ?? null; + let central_thesis = 'Insufficient ML/spread/totals mass for a central thesis.'; + if (favoriteYes != null && favored_side) { + const S = NBA_SHAPE_THRESHOLDS; + const edge = favoriteYes >= S.clear_favorite + ? 'clear favorite' + : (favoriteYes >= S.lean_favorite ? 'lean favorite' : 'coin-flip ML'); + central_thesis = `${favored_side} is ${edge} (yes≈${favoriteYes})` + + (spreadPivot != null ? `; spread pivot ${spreadPivot}` : '') + + (totalPivot != null ? `; total pivot ${totalPivot}` : '') + + '.'; + } + return { + favored_side, + favorite_yes: favoriteYes, + spread_pivot: spreadPivot, + total_pivot: totalPivot, + central_thesis + }; +} + +function buildNbaCoherence({ moneyline, spreads, totals, matrix_status }) { + const residuals = []; + const fav = moneyline?.favorite?.yes ?? null; + const spreadLine = spreads?.pivot?.line ?? null; + const spreadYes = spreads?.pivot?.yes ?? null; + const totalYes = totals?.pivot?.yes ?? null; + + const S = NBA_SHAPE_THRESHOLDS; + + if (fav != null && fav >= S.heavy_favorite && spreadLine != null + && Math.abs(spreadLine) <= S.tight_spread_abs) { + residuals.push({ + type: 'ml_vs_spread_tight', + note: 'Heavy ML favorite with a tiny spread — check whether juice/alt lines are misaligned.', + triggered_by: `fav>=${S.heavy_favorite} && |spread|<=${S.tight_spread_abs}` + }); + } + if (fav != null && fav <= S.near_even_ml && spreadLine != null + && Math.abs(spreadLine) >= S.wide_spread_abs) { + residuals.push({ + type: 'ml_vs_spread_wide', + note: 'Near-even ML with a large spread is unusual — verify team mapping.', + triggered_by: `fav<=${S.near_even_ml} && |spread|>=${S.wide_spread_abs}` + }); + } + if (totalYes != null && (totalYes > S.totals_extreme_high || totalYes < S.totals_extreme_low) + && spreads?.count) { + residuals.push({ + type: 'totals_extreme_vs_spread_present', + note: 'Totals pivot is extreme while spreads exist — confirm tipoff/context before acting.', + triggered_by: `total>${S.totals_extreme_high} || total<${S.totals_extreme_low}` + }); + } + + return { + coherence_status: residuals.length + ? 'tension' + : (matrix_status === 'complete' ? 'ok_heuristic' : 'incomplete_matrix'), + cross_market_residuals: residuals, + spread_yes_at_pivot: spreadYes, + distribution_note: 'Heuristic only — not a player-prop or possession model.', + // Ship the ruleset with the verdict so the buyer can audit the cutoffs. + shape_thresholds: { ...NBA_SHAPE_THRESHOLDS } + }; +} + +function compactRow(row) { + return { + title: row.title ?? null, + group_item_title: row.group_item_title ?? null, + slug: row.slug ?? null, + yes: row.yes ?? null, + no: row.no ?? null, + best_ask: row.best_ask ?? null, + best_bid: row.best_bid ?? null, + spread: row.spread ?? null, + volume_24h_usd: row.volume_24h_usd ?? null, + sports_market_type: row.sports_market_type ?? null, + is_primary: row.is_primary === true + }; +} + +function groupBy(rows, keyFn) { + const out = {}; + for (const row of rows) { + const key = keyFn(row) || 'other'; + if (!out[key]) out[key] = []; + out[key].push(row); + } + return out; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-politics.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-politics.mjs new file mode 100644 index 00000000..ab613d85 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-politics.mjs @@ -0,0 +1,90 @@ +// Politics / election ladder plugin (L1) for PM Event Readout. +// Binary + multi-candidate yes-mass surface; no scrape, no orders. + +import { round2 } from './pm-gamma-market.mjs'; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix + */ +export function enrichPoliticsCategory({ market, eventBundle, eventMatrix }) { + const rows = (eventMatrix || []).map((row) => ({ + label: row.group_item_title || row.title || row.slug || 'unknown', + slug: row.slug, + yes: row.yes, + no: row.no, + volume_24h_usd: row.volume_24h_usd, + best_ask: row.best_ask, + best_bid: row.best_bid, + spread: row.spread, + is_primary: row.is_primary === true + })); + + const ranked = rows + .filter((r) => Number.isFinite(r.yes)) + .slice() + .sort((a, b) => b.yes - a.yes); + + const yesSum = ranked.reduce((sum, r) => sum + (r.yes || 0), 0); + const leader = ranked[0] || null; + const runner = ranked[1] || null; + const ladder_status = ranked.length >= 3 ? 'multi_candidate' : (ranked.length === 2 ? 'binary_or_pair' : 'thin'); + + const residuals = []; + if (yesSum > 1.15) { + residuals.push({ + type: 'yes_mass_over_one', + note: `Sum of yes prices ≈ ${round2(yesSum)} — overlapping/non-exclusive outcomes or nested markets.` + }); + } + if (leader && runner && leader.yes - runner.yes < 0.05 && leader.yes > 0.35) { + residuals.push({ + type: 'tight_leaderboard', + note: 'Top two candidates nearly tied — watch liquidity and nested contract definitions.' + }); + } + + const primaryYes = market?.yes ?? leader?.yes ?? null; + const central_thesis = leader + ? `Market mass leads "${leader.label}" at yes≈${leader.yes}` + + (runner ? ` vs "${runner.label}" ≈${runner.yes}` : '') + + '.' + : 'Insufficient priced candidates for a politics thesis.'; + + return { + category: 'politics', + category_depth: 'enriched', + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + related_market_count: rows.length, + ladder_status, + yes_mass_sum: round2(yesSum), + leaderboard: ranked.slice(0, 8).map((r) => ({ + label: r.label, + yes: r.yes, + share_of_yes_mass: yesSum > 0 ? round2(r.yes / yesSum) : null, + slug: r.slug, + is_primary: r.is_primary + })), + market_implied_shape: { + leader: leader ? { label: leader.label, yes: leader.yes } : null, + runner_up: runner ? { label: runner.label, yes: runner.yes } : null, + primary_yes: primaryYes, + central_thesis + }, + coherence: { + coherence_status: residuals.length ? 'tension' : (ladder_status === 'thin' ? 'incomplete_matrix' : 'ok_heuristic'), + cross_market_residuals: residuals + }, + default_action_hint: ladder_status === 'thin' ? 'no_trade' : 'use_decision_card_after_definition_check', + tradability_cap: ladder_status === 'thin' ? 'weak' : null, + tradability_reasons: ladder_status === 'thin' ? ['politics_ladder_thin'] : [], + hard_gate: 'no_orders_no_account_mutation_no_polling_scrape', + skill_alignment: { + source: 'prediction-copilot_politics_surface_lite', + included: ['candidate_yes_mass_leaderboard', 'yes_mass_sanity', 'definition_caution'], + excluded_local_only: ['poll_aggregation', 'bankroll', 'news_scrape'] + } + }; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-tennis.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-tennis.mjs new file mode 100644 index 00000000..f7bc1743 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-tennis.mjs @@ -0,0 +1,816 @@ +// Tennis match category plugin (L1) for PM Event Readout. +// Aligns to pm-tennis-match skill (exportable parts only). +// Quality bar: research/2026-07-09-tennis-asp-quality-spec.md +// No Leo bankroll/sizing/orders. Fixture + format must be honest. + +import { round2 } from './pm-gamma-market.mjs'; + +const REQUIRED_GROUPS = [ + 'match_moneyline', + 'set_handicap', + 'total_sets', + 'match_games_totals' +]; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix + * @param {object} [args.fixture] + */ +export function enrichTennisCategory({ market, eventBundle, eventMatrix, fixture = null }) { + const classified = (eventMatrix || []).map((row) => ({ + ...row, + market_group: classifyTennisGroup(row) + })); + + const groups = groupBy(classified, (row) => row.market_group); + const missing = []; + for (const key of REQUIRED_GROUPS) { + if (!groups[key]?.length) missing.push(key); + } + if (!groups.set1_winner?.length) missing.push('set1_winner_optional'); + if (!groups.completed_match?.length) missing.push('completed_match_optional'); + + const requiredMissing = missing.filter((m) => !m.endsWith('_optional')); + const matrix_status = requiredMissing.length ? 'incomplete' : 'complete'; + + const format = detectFormat(market, eventBundle, fixture); + const moneyline = summarizeNamedMoneyline(groups.match_moneyline || [], market); + const setHandicap = summarizeSetHandicap(groups.set_handicap || [], moneyline); + const totalSets = summarizeOverUnderLadder(groups.total_sets || [], 'total_sets'); + const matchGames = summarizeOverUnderLadder(groups.match_games_totals || [], 'match_games'); + const set1 = summarizeNamedMoneyline(groups.set1_winner || []); + const completed = firstBinary(groups.completed_match); + + const fixtureGate = evaluateFixtureGate(market, eventBundle, fixture); + const shape = buildImpliedShape({ + format, + moneyline, + setHandicap, + totalSets, + matchGames, + set1, + completed + }); + const domination = buildStraightSetDominationCheck({ format, moneyline, totalSets, setHandicap, shape }); + const expressions = buildExpressionComparison({ + format, + moneyline, + setHandicap, + totalSets, + matchGames, + shape, + domination + }); + const hard_veto_gaps = buildTennisHardVetoGaps({ + requiredMissing, + fixtureGate, + moneyline, + format + }); + const matrix_completeness = { + status: matrix_status, + required_groups: REQUIRED_GROUPS, + required_present: REQUIRED_GROUPS.filter((g) => (groups[g] || []).length > 0), + required_missing: requiredMissing, + hard_veto_gaps + }; + + let tradability_cap = null; + const tradability_reasons = []; + if (fixtureGate.fixture_status !== 'ok') { + tradability_cap = 'weak'; + tradability_reasons.push('fixture_unverified_or_failed'); + } + if (matrix_status === 'incomplete') { + tradability_cap = tradability_cap || 'medium'; + tradability_reasons.push('tennis_matrix_incomplete'); + } + if (hard_veto_gaps.length) { + tradability_cap = 'weak'; + tradability_reasons.push('tennis_hard_veto_gaps'); + } + + const default_action_hint = hard_veto_gaps.length || fixtureGate.fixture_status !== 'ok' || matrix_status === 'incomplete' + ? 'no_trade' + : (expressions.recommended + ? 'use_decision_card_after_expression_comparison' + : (expressions.all_core_expressions_overpriced ? 'no_trade' : 'use_decision_card_after_expression_comparison')); + + const market_surface = {}; + for (const [key, rows] of Object.entries(groups)) { + market_surface[key] = rows.map(compactRow); + } + + return { + category: 'tennis', + category_depth: 'enriched', + format, + primary_event_slug: eventBundle?.primary_event_slug || eventBundle?.slug || null, + sibling_event_slugs: eventBundle?.sibling_event_slugs || [], + linked_event_count: eventBundle?.linked_event_count + ?? (1 + (eventBundle?.sibling_event_slugs?.length || 0)), + discovery: eventBundle?.discovery || null, + related_market_count: classified.length, + group_counts: Object.fromEntries( + Object.entries(groups).map(([key, rows]) => [key, rows.length]) + ), + matrix_status, + matrix_completeness, + hard_veto_gaps, + missing_market_groups: missing, + market_surface, + market_implied_shape: shape, + straight_set_domination_check: domination, + fixture: fixtureGate, + expression_comparison: expressions, + recommended_expression: expressions.recommended || null, + default_action_hint, + tradability_cap, + tradability_reasons, + central_thesis: shape.central_thesis, + hard_gate: 'no_orders_no_account_mutation_no_leo_bankroll', + skill_alignment: { + source: 'pm-tennis-match', + included: [ + 'fixture_gate', + 'format_best_of_3_or_5', + 'full_same_event_matrix', + 'named_outcome_moneyline', + 'missing_market_groups', + 'matrix_completeness', + 'hard_veto_gaps', + 'market_implied_shape', + 'expression_comparison', + 'straight_set_domination_check', + 'thesis_expression_coherence' + ], + excluded_local_only: [ + 'bankroll_pct', + 'u_amount_from_latest_anchor', + 'playbook_card_writeback', + 'personal_exposure', + 'independent_order_of_play_scrape' + ] + } + }; +} + +function buildTennisHardVetoGaps({ requiredMissing, fixtureGate, moneyline, format }) { + const gaps = []; + if (fixtureGate?.fixture_status && fixtureGate.fixture_status !== 'ok') { + gaps.push(`fixture_${fixtureGate.fixture_status}`); + } + const hasMl = Boolean(moneyline?.player_a || moneyline?.player_b); + if (requiredMissing.includes('match_moneyline') || !hasMl) { + gaps.push('missing_match_moneyline'); + } + for (const key of requiredMissing) { + if (key === 'match_moneyline') continue; + gaps.push(`missing_${key}`); + } + if (!format?.best_of || format?.source === 'unknown') { + gaps.push('format_unknown'); + } + return [...new Set(gaps)]; +} + +export function extractTennisFixture(input = {}, options = {}) { + if (options.tennisFixture && typeof options.tennisFixture === 'object') { + return options.tennisFixture; + } + const raw = input.tennis && typeof input.tennis === 'object' + ? input.tennis + : (input.fixture && typeof input.fixture === 'object' ? input.fixture : null); + if (!raw) return null; + return { + requested_window: raw.requested_window ?? raw.window ?? null, + scheduled_time_utc: raw.scheduled_time_utc ?? raw.kickoff_utc ?? raw.start_time ?? null, + scheduled_time_beijing: raw.scheduled_time_beijing ?? null, + fixture_sources: Array.isArray(raw.fixture_sources) + ? raw.fixture_sources + : (raw.fixture_sources ? [raw.fixture_sources] : []), + market_fixture_match: raw.market_fixture_match ?? null, + player_a: raw.player_a ?? raw.home ?? null, + player_b: raw.player_b ?? raw.away ?? null, + tournament: raw.tournament ?? null, + round: raw.round ?? null, + format: raw.format ?? null, + verified: raw.verified === true + }; +} + +export function classifyTennisGroup(row) { + const st = String(row.sports_market_type || '').toLowerCase(); + const text = `${row.group_item_title || ''} ${row.title || ''} ${row.slug || ''}`.toLowerCase(); + + if (st.includes('completed_match') || /completed match/.test(text)) return 'completed_match'; + if (st.includes('set_handicap') || /set handicap|handicap\s*\+\/-/.test(text)) return 'set_handicap'; + // Check set_games / first_set before bare set_totals (substring traps). + if (st.includes('first_set_winner') || /set 1 winner|first set winner/.test(text)) return 'set1_winner'; + if (st.includes('first_set_totals') || /set 1 .*o\/u|first set .*o\/u|set 1 games/.test(text)) { + return 'set1_games'; + } + if (st.includes('set_games_totals') || /set \d .*o\/u|set \d games/.test(text)) return 'set_games'; + if (st.includes('set_winner') || /set \d winner/.test(text)) return 'set_winner'; + if (st === 'tennis_set_totals' || st.includes('set_totals') || /total sets/.test(text)) { + return 'total_sets'; + } + if (st.includes('match_totals') || (/match o\/u|match over\/under/.test(text) && !/set/.test(text))) { + return 'match_games_totals'; + } + if (st === 'moneyline' || (st.includes('moneyline') && !/set/.test(text))) return 'match_moneyline'; + + return 'other'; +} + +function detectFormat(market, eventBundle, fixture) { + if (fixture?.format === 'best_of_5' || fixture?.format === 'best_of_3') { + return { + best_of: fixture.format === 'best_of_5' ? 5 : 3, + source: 'caller', + note: fixture.format + }; + } + const blob = [ + market?.title, + market?.slug, + eventBundle?.title, + eventBundle?.slug, + fixture?.tournament + ].filter(Boolean).join(' ').toLowerCase(); + + if (/\bwta\b/.test(blob)) { + return { best_of: 3, source: 'tour_wta', note: 'WTA singles default best of 3' }; + } + if (/\batp\b/.test(blob) && /wimbledon|us open|australian|roland|french open|grand slam/.test(blob)) { + return { best_of: 5, source: 'tour_atp_gs', note: 'ATP Grand Slam singles default best of 5' }; + } + if (/\batp\b/.test(blob)) { + return { best_of: 3, source: 'tour_atp_tour', note: 'ATP tour default best of 3 (non-GS heuristic)' }; + } + return { best_of: null, source: 'unknown', note: 'Pass tennis.format=best_of_3|best_of_5 when known.' }; +} + +function evaluateFixtureGate(market, eventBundle, fixture) { + const gammaStart = eventBundle?.start_time || market?.start_time || null; + const gammaEnd = eventBundle?.end_date || market?.end_date || null; + + if (fixture?.verified === true && fixture.market_fixture_match === 'yes') { + return { + fixture_status: 'ok', + market_fixture_match: 'yes', + scheduled_time_utc: fixture.scheduled_time_utc || gammaStart, + scheduled_time_beijing: fixture.scheduled_time_beijing, + tournament: fixture.tournament, + round: fixture.round, + player_a: fixture.player_a, + player_b: fixture.player_b, + fixture_sources: fixture.fixture_sources?.length + ? fixture.fixture_sources + : ['caller_verified'], + note: 'Caller marked fixture verified and matching the market.' + }; + } + + if (fixture?.market_fixture_match === 'no') { + return { + fixture_status: 'failed_or_unverified', + market_fixture_match: 'no', + scheduled_time_utc: fixture.scheduled_time_utc || gammaStart, + fixture_sources: fixture.fixture_sources || [], + note: 'Caller reports market/fixture mismatch — no_trade.' + }; + } + + if (gammaStart || gammaEnd) { + return { + fixture_status: 'unverified', + market_fixture_match: 'unclear', + scheduled_time_utc: gammaStart, + scheduled_time_end_or_resolve: gammaEnd, + fixture_sources: gammaStart ? ['polymarket_gamma_startTime'] : ['polymarket_gamma_endDate'], + note: 'Gamma time present but not independently verified. Pass tennis.verified=true + sources for fixture_status=ok.' + }; + } + + return { + fixture_status: 'failed_or_unverified', + market_fixture_match: 'unclear', + scheduled_time_utc: null, + fixture_sources: [], + note: 'No schedule on Gamma and no caller verification — stop per pm-tennis-match fixture gate.' + }; +} + +function summarizeNamedMoneyline(rows, primaryMarket = null) { + // Prefer the live-fetched primary market prices when available — Gamma event + // bundle rows can lag the /markets slug fetch mid-match. + if (primaryMarket?.outcomes?.length >= 2 && primaryMarket.outcome_prices?.length >= 2) { + const player_a = { + label: String(primaryMarket.outcomes[0]), + yes: Number.isFinite(primaryMarket.outcome_prices[0]) + ? round2(primaryMarket.outcome_prices[0]) + : null, + slug: primaryMarket.slug, + ask: primaryMarket.best_ask + }; + const player_b = { + label: String(primaryMarket.outcomes[1]), + yes: Number.isFinite(primaryMarket.outcome_prices[1]) + ? round2(primaryMarket.outcome_prices[1]) + : null, + slug: primaryMarket.slug, + ask: Number.isFinite(primaryMarket.outcome_prices[1]) + ? round2(primaryMarket.outcome_prices[1]) + : null + }; + return { + player_a, + player_b, + raw: rows.map(compactRow), + primary_slug: primaryMarket.slug, + price_source: 'primary_market_live' + }; + } + + if (!rows.length) return { player_a: null, player_b: null, raw: [], price_source: 'none' }; + const row = rows.find((r) => r.is_primary) || rows[0]; + const named = row.named_outcomes?.length + ? row.named_outcomes + : (row.outcomes || []).map((label, idx) => ({ + label: String(label), + price: idx === 0 ? row.yes : null + })); + + const player_a = named[0] + ? { label: named[0].label, yes: named[0].price, slug: row.slug, ask: row.best_ask } + : null; + const player_b = named[1] + ? { + label: named[1].label, + yes: named[1].price, + slug: row.slug, + ask: named[1].price + } + : null; + + return { + player_a, + player_b, + raw: rows.map(compactRow), + primary_slug: row.slug, + price_source: 'event_matrix_row' + }; +} + +function summarizeSetHandicap(rows, moneyline) { + return rows.map((row) => { + const text = `${row.group_item_title || ''} ${row.title || ''}`; + const line = extractLineNumber(text) ?? 1.5; + const named = row.named_outcomes || []; + const favoriteLabel = pickFavoriteLabel(moneyline); + let favorite_side = null; + let underdog_side = null; + for (const n of named) { + const item = { label: n.label, yes: n.price, slug: row.slug, line }; + if (favoriteLabel && namesLikelyMatch(n.label, favoriteLabel)) favorite_side = item; + else underdog_side = underdog_side || item; + } + if (!favorite_side && named[0]) { + favorite_side = { label: named[0].label, yes: named[0].price, slug: row.slug, line }; + underdog_side = named[1] + ? { label: named[1].label, yes: named[1].price, slug: row.slug, line } + : null; + } + return { + label: row.group_item_title || row.title, + line, + slug: row.slug, + favorite_side, + underdog_side, + ask: row.best_ask + }; + }); +} + +function summarizeOverUnderLadder(rows, kind) { + const lines = rows.map((row) => { + const text = `${row.group_item_title || ''} ${row.title || ''} ${row.slug || ''}`; + const line = extractLineNumber(text); + const named = row.named_outcomes || []; + const over = named.find((n) => /^over/i.test(n.label)) || named[0]; + const under = named.find((n) => /^under/i.test(n.label)) || named[1]; + return { + label: row.group_item_title || row.title, + line, + over: over?.price ?? row.yes, + under: under?.price ?? null, + ask: row.best_ask, + slug: row.slug + }; + }).sort((a, b) => (a.line ?? 0) - (b.line ?? 0)); + + // Prefer tradeable mid-range lines; avoid settled 0/1 pivots polluting shape. + const pivot = lines + .filter((l) => l.over != null && l.over >= 0.2 && l.over <= 0.8) + .sort((a, b) => Math.abs(0.5 - a.over) - Math.abs(0.5 - b.over))[0] + ?? lines + .filter((l) => l.over != null) + .sort((a, b) => Math.abs(0.5 - a.over) - Math.abs(0.5 - b.over))[0] + ?? null; + + return { kind, lines, pivot, count: lines.length }; +} + +function buildImpliedShape({ format, moneyline, setHandicap, totalSets, matchGames, set1, completed }) { + const fav = pickFavorite(moneyline); + const dog = pickUnderdog(moneyline); + const setsPivot = totalSets.pivot; + const gamesPivot = matchGames.pivot; + const sh = setHandicap[0] || null; + + const states = []; + if (fav?.yes != null && fav.yes >= 0.58) states.push('favorite_leans_match'); + if (fav?.yes != null && fav.yes >= 0.7) states.push('strong_favorite'); + if (fav?.yes != null && dog?.yes != null && Math.abs(fav.yes - dog.yes) <= 0.12) { + states.push('match_priced_close'); + } + if (setsPivot?.over != null && setsPivot.over >= 0.55) states.push('market_leans_long_match_sets'); + if (setsPivot?.over != null && setsPivot.over <= 0.45) states.push('market_leans_short_match_sets'); + if (gamesPivot?.over != null && gamesPivot.over >= 0.55) states.push('market_leans_high_match_games'); + if (sh?.underdog_side?.yes != null && sh.underdog_side.yes >= 0.55) { + states.push('underdog_set_cover_live'); + } + if (sh?.favorite_side?.yes != null && sh.favorite_side.yes <= 0.25) { + states.push('favorite_set_domination_not_priced'); + } + if (completed?.yes != null && completed.yes < 0.85) { + states.push('retirement_or_incomplete_risk_priced'); + } + + let central_thesis = 'Insufficient structure for a sharp tennis state map.'; + if (states.includes('match_priced_close') || states.includes('market_leans_long_match_sets')) { + central_thesis = 'Match priced competitive / long — compare underdog set cover, total sets over, and match-games over before ML.'; + } else if (states.includes('strong_favorite') && states.includes('market_leans_short_match_sets')) { + central_thesis = 'Strong favorite with short-sets lean — compare favorite ML vs set handicap; avoid overs unless domination check fails.'; + } else if (states.includes('favorite_leans_match') && states.includes('market_leans_long_match_sets')) { + central_thesis = 'Favorite leans but market prices a push path — prefer total-sets over or underdog +handicap over rich ML.'; + } else if (fav) { + central_thesis = `Market favorite leans ${fav.label} (≈${fav.yes}). Compare set handicap / total sets / match games before locking expression.`; + } + + return { + format, + moneyline: { + player_a: moneyline.player_a, + player_b: moneyline.player_b, + favorite: fav, + underdog: dog + }, + set_handicap_sample: sh, + total_sets_pivot: setsPivot, + match_games_pivot: gamesPivot, + set1_sample: set1.player_a ? set1 : null, + completed_match: completed, + state_flags: states, + central_thesis + }; +} + +function buildStraightSetDominationCheck({ format, moneyline, totalSets, setHandicap, shape }) { + const fav = shape.moneyline?.favorite; + const setsPivot = totalSets.pivot; + const sh = setHandicap[0]; + const bo = format?.best_of; + + const favorite_2_0_or_3_0_path = bo === 5 + ? `Favorite wins 3-0 (straight sets in BO5). If priced strongly, overs/covers that need a push are fragile.` + : `Favorite wins 2-0 (straight sets in BO3). If likely, total-sets Over 2.5 and underdog +1.5 can die together.`; + + const underdog_collapse_path = fav + ? `${fav.label} holds serve / breaks early; underdog fails to take a set.` + : 'Favorite closes quickly; underdog never holds a set.'; + + const serve_hold_break_risk = 'Serve-hold leagues (grass) raise straight-set risk; break-heavy surfaces raise push paths. ASP does not scrape live serve stats — caller must supply external_context if critical.'; + + const overNeedsPush = setsPivot?.over != null && setsPivot.over >= 0.5; + const coverNeedsPush = sh?.underdog_side?.yes != null && sh.underdog_side.yes >= 0.5; + const strongFav = (fav?.yes ?? 0) >= 0.68; + + let survives = true; + let why = 'Domination path not dominant enough to veto competitive expressions by default.'; + if ((overNeedsPush || coverNeedsPush) && strongFav && shape.state_flags.includes('market_leans_short_match_sets')) { + survives = false; + why = 'Strong favorite + short-sets lean: over / underdog cover do not clearly survive straight-set domination.'; + } else if (overNeedsPush && shape.state_flags.includes('market_leans_long_match_sets')) { + why = 'Market already leans long sets — over/cover can survive if price is not full (decision-card still required).'; + } + + return { + favorite_2_0_or_3_0_path, + underdog_collapse_path, + serve_hold_break_risk, + why_over_or_cover_survives: why, + survives, + applies_when_recommending: ['total_sets_over', 'underdog_set_handicap', 'match_games_over'] + }; +} + +function buildExpressionComparison({ + format, + moneyline, + setHandicap, + totalSets, + matchGames, + shape, + domination +}) { + const candidates = []; + const fav = shape.moneyline?.favorite; + const dog = shape.moneyline?.underdog; + + if (fav) { + candidates.push({ + expression: 'favorite_match_ml', + market: fav.label, + slug: moneyline.primary_slug, + yes: fav.yes, + ask: fav.ask, + path: 'Needs favorite to win the match.', + why_consider: 'Direct match result.', + why_not: 'Ignores push/set-cover paths; can be expensive vs handicap/totals.', + aligns_with_thesis: shape.state_flags.includes('favorite_leans_match') + && !shape.state_flags.includes('match_priced_close') + }); + } + if (dog) { + candidates.push({ + expression: 'underdog_match_ml', + market: dog.label, + slug: moneyline.primary_slug, + yes: dog.yes, + ask: dog.ask, + path: 'Needs underdog to win the match.', + why_consider: 'Upset expression.', + why_not: 'Usually thinner path than set cover.', + aligns_with_thesis: shape.state_flags.includes('match_priced_close') + }); + } + + const sh = setHandicap[0]; + if (sh?.underdog_side) { + candidates.push({ + expression: 'underdog_set_handicap', + market: `${sh.underdog_side.label} (+${sh.line})`, + slug: sh.slug, + yes: sh.underdog_side.yes, + ask: sh.ask, + path: format?.best_of === 5 + ? `Underdog wins match or loses 2-${Math.ceil(format.best_of / 2)} (BO5 +${sh.line} cover rules).` + : `Underdog wins match or loses 1-2 (BO3 +${sh.line}).`, + why_consider: 'Wider path than underdog ML when favorite may be pushed.', + why_not: 'Dies on straight-set domination.', + aligns_with_thesis: shape.state_flags.includes('market_leans_long_match_sets') + || shape.state_flags.includes('match_priced_close') + || shape.state_flags.includes('underdog_set_cover_live'), + needs_domination_check: true + }); + } + if (sh?.favorite_side) { + candidates.push({ + expression: 'favorite_set_handicap', + market: `${sh.favorite_side.label} (-${sh.line})`, + slug: sh.slug, + yes: sh.favorite_side.yes, + ask: sh.ask, + path: 'Favorite covers set handicap (domination / multi-set margin).', + why_consider: 'Expresses straight-set / clear-win thesis.', + why_not: 'Fragile if match goes long.', + aligns_with_thesis: shape.state_flags.includes('strong_favorite') + && shape.state_flags.includes('market_leans_short_match_sets'), + needs_domination_check: false + }); + } + + const sets = totalSets.pivot; + if (sets) { + candidates.push({ + expression: 'total_sets_over', + market: sets.label, + slug: sets.slug, + yes: sets.over, + ask: sets.ask, + path: `Needs match to go over ${sets.line} sets.`, + why_consider: 'Expresses push / competitive match without picking winner.', + why_not: 'Fails under straight-set domination.', + aligns_with_thesis: shape.state_flags.includes('market_leans_long_match_sets') + || shape.state_flags.includes('match_priced_close'), + needs_domination_check: true + }); + candidates.push({ + expression: 'total_sets_under', + market: sets.label.replace(/O\/U/i, 'Under'), + slug: sets.slug, + yes: sets.under, + ask: sets.under, + path: `Needs match to stay under ${sets.line} sets.`, + why_consider: 'Aligns with domination / short-match thesis.', + why_not: 'Dies if underdog takes a set early and match extends.', + aligns_with_thesis: shape.state_flags.includes('market_leans_short_match_sets') + || shape.state_flags.includes('strong_favorite'), + needs_domination_check: false + }); + } + + const games = matchGames.pivot; + if (games) { + candidates.push({ + expression: 'match_games_over', + market: games.label, + slug: games.slug, + yes: games.over, + ask: games.ask, + path: `Needs total match games over ${games.line}.`, + why_consider: 'Competitive / hold-heavy match expression.', + why_not: 'Correlated with long sets; domination kills it.', + aligns_with_thesis: shape.state_flags.includes('market_leans_high_match_games') + || shape.state_flags.includes('market_leans_long_match_sets'), + needs_domination_check: true + }); + } + + // Coherence: competitive thesis → cover/over; domination → ML/under/fav handicap + // Price gate: never recommend full/rich expressions (yes>=0.85 or <=0.15) as tip. + const withStatus = candidates.map((c) => ({ + ...c, + price_status: scorePriceStatus(c.yes) + })); + + let recommended = null; + const flags = shape.state_flags || []; + const pickFirstActionable = (exprs) => { + for (const name of exprs) { + const hit = withStatus.find((c) => c.expression === name); + if (!hit) continue; + if (hit.needs_domination_check && domination && domination.survives === false) continue; + // Skill: full / rich / no_edge cannot be the final tip expression. + if (['full', 'rich', 'no_edge'].includes(hit.price_status)) continue; + return hit; + } + return null; + }; + + if (flags.includes('match_priced_close') || flags.includes('market_leans_long_match_sets')) { + recommended = pickFirstActionable([ + 'underdog_set_handicap', + 'total_sets_over', + 'match_games_over', + 'underdog_match_ml', + 'favorite_match_ml' + ]); + } else if (flags.includes('strong_favorite') && flags.includes('market_leans_short_match_sets')) { + recommended = pickFirstActionable([ + 'favorite_set_handicap', + 'total_sets_under', + 'favorite_match_ml' + ]); + } else if (flags.includes('favorite_leans_match')) { + recommended = pickFirstActionable(['favorite_match_ml', 'total_sets_under']); + } + + if (!recommended) { + // All core expressions overpriced / gated → honest no tip + recommended = null; + } + + const all_core_expressions_overpriced = withStatus + .filter((c) => [ + 'favorite_match_ml', + 'underdog_match_ml', + 'underdog_set_handicap', + 'total_sets_over', + 'total_sets_under', + 'match_games_over' + ].includes(c.expression)) + .every((c) => ['full', 'rich', 'no_edge'].includes(c.price_status)); + + return { + candidates: withStatus, + recommended: recommended + ? { + ...recommended, + note: 'Heuristic expression pick from market shape only — not a buy tip; run decision-card for price_status/edge.', + coherence_with_thesis: true, + domination_check_applied: Boolean(recommended.needs_domination_check) + } + : null, + all_core_expressions_overpriced, + rule: 'Always compare ≥2 expressions; convert set handicap into covered scores using format; run straight_set_domination_check before overs/covers; never recommend full/rich prices.' + }; +} + +/** + * Price gate cutoffs, hoisted and exported 2026-07-30. + * + * This gate decides whether an expression may be handed to the caller as the tip: + * `full` / `rich` / `no_edge` are refused outright. That makes these numbers the + * most consequential constants in the file — they are the difference between + * "recommended" and "withheld" — yet they were inline and the module had no test + * coverage at all. Exported so a buyer can read the rail before trusting the tip, + * and so the boundaries can be pinned by tests. + * + * Values unchanged from the inline versions. Retuning would need settled-match + * backtesting, which this service does not do. + */ +export const TENNIS_PRICE_GATE = Object.freeze({ + /** At or beyond this (either tail) the price is fully paid — never a tip. */ + full: 0.85, + /** At or beyond this (either tail) the price is rich — never a tip. */ + rich: 0.72, + /** Inside this band the price is acceptable. */ + acceptable_low: 0.4, + acceptable_high: 0.6 +}); + +export function scorePriceStatus(yes) { + const G = TENNIS_PRICE_GATE; + if (yes == null || !Number.isFinite(yes)) return 'unknown'; + if (yes >= G.full || yes <= 1 - G.full) return 'full'; + if (yes >= G.rich || yes <= 1 - G.rich) return 'rich'; + if (yes >= G.acceptable_low && yes <= G.acceptable_high) return 'acceptable'; + return 'watch'; +} + +function pickFavorite(moneyline) { + const sides = [moneyline.player_a, moneyline.player_b].filter(Boolean); + if (!sides.length) return null; + return sides.slice().sort((a, b) => (b.yes ?? 0) - (a.yes ?? 0))[0]; +} + +function pickUnderdog(moneyline) { + const fav = pickFavorite(moneyline); + const sides = [moneyline.player_a, moneyline.player_b].filter(Boolean); + return sides.find((s) => s.label !== fav?.label) || null; +} + +function pickFavoriteLabel(moneyline) { + return pickFavorite(moneyline)?.label || null; +} + +function namesLikelyMatch(a, b) { + const na = String(a).toLowerCase().split(/\s+/).filter(Boolean); + const nb = String(b).toLowerCase().split(/\s+/).filter(Boolean); + if (!na.length || !nb.length) return false; + return na.some((part) => nb.includes(part)) || nb.some((part) => na.includes(part)); +} + +function firstBinary(rows) { + if (!rows?.length) return null; + const row = rows[0]; + return { + label: row.group_item_title || row.title, + yes: row.yes, + ask: row.best_ask, + slug: row.slug + }; +} + +function extractLineNumber(text) { + const m = String(text).match(/([+-]?\d+(?:\.\d+)?)\s*(?:pt)?/i) + || String(text).match(/([OU])\s*(\d+(?:\.\d+)?)/i) + || String(text).match(/(\d+(?:\.\d+)?)/); + if (!m) return null; + if (m[2] && (m[1] === 'O' || m[1] === 'U')) return Number(m[2]); + const n = Number(m[1]); + return Number.isFinite(n) ? Math.abs(n) : null; +} + +function compactRow(row) { + return { + slug: row.slug, + title: row.title, + group_item_title: row.group_item_title, + sports_market_type: row.sports_market_type, + market_group: row.market_group, + outcomes: row.outcomes, + named_outcomes: row.named_outcomes, + yes: row.yes, + best_ask: row.best_ask, + best_bid: row.best_bid, + volume_24h_usd: row.volume_24h_usd, + is_primary: row.is_primary + }; +} + +function groupBy(items, fn) { + const out = {}; + for (const item of items) { + const key = fn(item) || 'other'; + if (!out[key]) out[key] = []; + out[key].push(item); + } + return out; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-weather.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-weather.mjs new file mode 100644 index 00000000..e79ceaf2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-category-weather.mjs @@ -0,0 +1,257 @@ +// Weather temperature-ladder category plugin (L1) for PM Event Readout. +// Public ASP surface: Gamma ladder + optional caller forecast/obs snapshot. +// No station scrape, no orders, no bankroll sizing (local skill only). + +import { round2 } from './pm-gamma-market.mjs'; + +/** + * @param {object} args + * @param {object} args.market + * @param {object|null} args.eventBundle + * @param {object[]} args.eventMatrix + * @param {object} [args.snapshot] + * { city, station, snapshot_time, observed_temp_c|f, forecast_mode_bucket, + * forecast_distribution:[{bucket,prob}], source_label } + */ +export function enrichWeatherCategory({ market, eventBundle, eventMatrix, snapshot = null }) { + const buckets = (eventMatrix || []).map((row) => { + const label = normalizeTempBucket(row.group_item_title || row.title || row.slug); + return { + bucket: label, + slug: row.slug, + yes: row.yes, + volume_24h_usd: row.volume_24h_usd, + best_ask: row.best_ask, + best_bid: row.best_bid, + spread: row.spread, + is_primary: row.is_primary, + temp_c: parseTempC(label) + }; + }).sort(compareBuckets); + + const yesSum = buckets.reduce((sum, b) => sum + (Number.isFinite(b.yes) ? b.yes : 0), 0); + const market_implied_distribution = buckets.map((b) => ({ + bucket: b.bucket, + yes: b.yes, + share_of_yes_mass: yesSum > 0 && Number.isFinite(b.yes) ? round2(b.yes / yesSum) : null + })); + + const modal = buckets + .filter((b) => Number.isFinite(b.yes)) + .slice() + .sort((a, b) => b.yes - a.yes)[0] ?? null; + + const freshness = buildFreshness(snapshot); + const forecast = normalizeForecast(snapshot?.forecast_distribution); + const observed = readObserved(snapshot); + const thesis = buildThesis({ modal, forecast, observed, yesSum, buckets }); + const ladder_status = buckets.length >= 4 ? 'complete' : (buckets.length >= 2 ? 'thin' : 'incomplete'); + const adjacent_ladder = buildAdjacentLadderDiagnostics(buckets, modal); + const hard_veto_gaps = buildWeatherHardVetoGaps({ + snapshot, + freshness, + observed, + ladder_status, + buckets, + yesSum + }); + + const tradability_cap = hard_veto_gaps.length + ? 'weak' + : (freshness.status !== 'provided' ? 'medium' : null); + const tradability_reasons = []; + if (freshness.status !== 'provided') tradability_reasons.push('weather_source_snapshot_missing'); + if (ladder_status !== 'complete') tradability_reasons.push('weather_ladder_thin'); + if (hard_veto_gaps.length) tradability_reasons.push('weather_hard_veto_gaps'); + + return { + category: 'weather', + category_depth: 'enriched', + city: snapshot?.city || extractCity(eventBundle?.title || market?.title) || null, + station: snapshot?.station ?? null, + station_coordinates: snapshot?.station_coordinates ?? null, + count_source_freshness: freshness, + source_label: snapshot?.source_label ?? null, + observed_temp: observed, + ladder_status, + matrix_completeness: { + status: ladder_status === 'complete' ? 'complete' : 'incomplete', + bucket_count: buckets.length, + hard_veto_gaps + }, + hard_veto_gaps, + adjacent_ladder, + full_bucket_surface: buckets, + market_implied_distribution, + yes_mass_sum: round2(yesSum), + modal_bucket: modal ? { bucket: modal.bucket, yes: modal.yes, slug: modal.slug } : null, + forecast_distribution: forecast, + forecast_vs_market: compareForecastToMarket(forecast, buckets), + direct_bucket_thesis: thesis, + split_ladder_paper: { + status: 'not_computed_in_asp_v1', + detail: 'Paper split-ladder / bankroll stays in local pm-weather-ladder skill; ASP returns ladder + optional forecast context only.' + }, + default_action_hint: hard_veto_gaps.length || tradability_reasons.length + ? 'no_trade_until_source_and_ladder_ok' + : 'use_decision_card_or_local_weather_skill', + hard_gate: 'no_orders_no_account_mutation_no_station_scrape', + tradability_cap, + tradability_reasons, + skill_alignment: { + source: 'pm-weather-ladder', + included: [ + 'full_bucket_surface', + 'market_implied_distribution', + 'optional_forecast_distribution', + 'source_freshness_gate', + 'hard_veto_gaps', + 'adjacent_ladder_diagnostics' + ], + excluded_local_only: [ + 'station_scrape', + 'bankroll_pct', + 'split_ladder_paper_execution', + 'u_amount_from_latest_anchor' + ] + } + }; +} + +function buildWeatherHardVetoGaps({ snapshot, freshness, observed, ladder_status, buckets, yesSum }) { + const gaps = []; + if (!snapshot?.station) gaps.push('station_identity_missing'); + if (!snapshot?.station_coordinates && !snapshot?.station) gaps.push('station_coordinates_missing'); + if (freshness.status !== 'provided') gaps.push('source_snapshot_missing'); + if (!observed) gaps.push('observed_temp_missing'); + if (ladder_status === 'incomplete') gaps.push('ladder_incomplete'); + if (ladder_status === 'thin') gaps.push('ladder_thin'); + if (buckets.length && (yesSum > 1.2 || yesSum < 0.8)) gaps.push('yes_mass_incoherent'); + return gaps; +} + +function buildAdjacentLadderDiagnostics(buckets, modal) { + if (!buckets.length) { + return { status: 'unavailable', note: 'No temperature buckets on surface.' }; + } + const ordered = buckets.slice().sort(compareBuckets); + const modalIdx = modal + ? ordered.findIndex((b) => b.bucket === modal.bucket || b.slug === modal.slug) + : -1; + const adjacent = modalIdx >= 0 + ? ordered.slice(Math.max(0, modalIdx - 1), modalIdx + 2) + : ordered.slice(0, 3); + return { + status: ordered.length >= 3 ? 'ok' : 'thin', + modal_index: modalIdx, + adjacent_buckets: adjacent.map((b) => ({ + bucket: b.bucket, + yes: b.yes, + ask: b.best_ask, + slug: b.slug + })), + note: 'Exact-degree buckets are mutually exclusive; always read adjacent buckets with the modal.' + }; +} + +export function extractWeatherSnapshot(input = {}, options = {}) { + if (options.weatherSnapshot && typeof options.weatherSnapshot === 'object') { + return options.weatherSnapshot; + } + const w = input.weather; + if (w && typeof w === 'object') return w; + return null; +} + +function buildFreshness(snapshot) { + if (!snapshot || typeof snapshot !== 'object') { + return { + status: 'missing', + snapshot_time: null, + detail: 'Pass weather.snapshot_time + observed/forecast fields; ASP does not scrape METAR/WU.' + }; + } + if (snapshot.snapshot_time || snapshot.observed_temp_c != null || snapshot.forecast_distribution) { + return { + status: 'provided', + snapshot_time: snapshot.snapshot_time ?? null, + detail: 'Caller-provided weather snapshot; not independently verified by ASP.' + }; + } + return { + status: 'missing', + snapshot_time: null, + detail: 'weather object present but no snapshot_time / observed / forecast.' + }; +} + +function normalizeForecast(rows) { + if (!Array.isArray(rows)) return []; + return rows.map((r) => ({ + bucket: normalizeTempBucket(r.bucket || r.label || r.temp), + prob: Number.isFinite(Number(r.prob ?? r.probability)) ? round2(Number(r.prob ?? r.probability)) : null + })).filter((r) => r.bucket); +} + +function readObserved(snapshot) { + if (!snapshot) return null; + if (snapshot.observed_temp_c != null) { + return { value: Number(snapshot.observed_temp_c), unit: 'C' }; + } + if (snapshot.observed_temp_f != null) { + return { value: Number(snapshot.observed_temp_f), unit: 'F' }; + } + return null; +} + +function compareForecastToMarket(forecast, buckets) { + if (!forecast.length || !buckets.length) { + return { status: 'unavailable', note: 'Need forecast_distribution and ladder.' }; + } + const topF = forecast.slice().sort((a, b) => (b.prob ?? 0) - (a.prob ?? 0))[0]; + const marketRow = buckets.find((b) => b.bucket === topF.bucket); + return { + status: marketRow ? 'aligned_bucket_present' : 'forecast_mode_missing_on_ladder', + forecast_mode_bucket: topF.bucket, + forecast_mode_prob: topF.prob, + market_yes_for_mode: marketRow?.yes ?? null + }; +} + +function buildThesis({ modal, forecast, observed, yesSum, buckets }) { + const parts = []; + if (modal) parts.push(`Market modal bucket ${modal.bucket} (Yes≈${modal.yes}).`); + if (forecast[0]) { + const top = forecast.slice().sort((a, b) => (b.prob ?? 0) - (a.prob ?? 0))[0]; + parts.push(`Caller forecast mode ${top.bucket} (p≈${top.prob}).`); + } + if (observed) parts.push(`Observed ${observed.value}°${observed.unit} (caller-supplied).`); + if (yesSum > 1.15 || yesSum < 0.85) parts.push(`Yes-mass sum ${round2(yesSum)} looks off — check overlapping buckets.`); + if (!buckets.length) parts.push('No temperature buckets parsed from matrix.'); + return parts.join(' ') || 'Insufficient weather ladder structure.'; +} + +function normalizeTempBucket(text) { + const s = String(text ?? '').trim(); + if (!s) return 'unknown'; + // Prefer compact degree labels already on Polymarket titles + const m = s.match(/(-?\d+(?:\.\d+)?)\s*°?\s*[CF]?(?:\s*[-–]\s*(-?\d+(?:\.\d+)?))?/i); + if (m && m[2]) return `${m[1]}-${m[2]}`; + if (m) return `${m[1]}`; + return s.slice(0, 48); +} + +function parseTempC(label) { + const m = String(label).match(/(-?\d+(?:\.\d+)?)/); + return m ? Number(m[1]) : null; +} + +function compareBuckets(a, b) { + if (a.temp_c != null && b.temp_c != null) return a.temp_c - b.temp_c; + return String(a.bucket).localeCompare(String(b.bucket)); +} + +function extractCity(title) { + const m = String(title ?? '').match(/\b(in|at)\s+([A-Z][A-Za-z\- ]{2,40})\b/); + return m ? m[2].trim() : null; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-decision-card.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-decision-card.mjs new file mode 100644 index 00000000..eaf2492f --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-decision-card.mjs @@ -0,0 +1,783 @@ +// PM Decision Card — paid trading-loop SKU. +// Composes trade-preflight (+ optional event readout snippet) into one replayable +// gate: skip / watch / eligible_for_manual_review. Not a buy tip, no orders. + +import { + assessPmTradePreflightLive, + buildPmTradePreflightFallback +} from './pm-trade-preflight.mjs'; +import { + assessPmEventReadoutLive, + buildPmEventReadoutFallback +} from './pm-event-readout.mjs'; +import { resolveMarketRef } from './pm-gamma-market.mjs'; + +const SERVICE_ID = 'pm_decision_card'; +const SCHEMA_VERSION = '0.4'; +const PUBLIC_THRESHOLD_SOURCE = 'asp_public_heuristic'; +const PUBLIC_THRESHOLD_VERSION = 'v0.4'; +const PUBLIC_HARD_GATE = 'no_orders_no_signing_no_wallet_custody_no_leo_private_bankroll'; +const DEFAULT_FEE_BUFFER = 0.02; +/** Caller-supplied bankroll only — never Leo private SSOT. */ +const CALLER_SIZING_AUTHORITY = 'caller_supplied_bankroll_or_size_usd'; +const B_MINUS_PROBE_BANKROLL_FRAC = 0.015; + +const STANDARD_CAVEATS = [ + 'Decision card is a mechanical + event-context gate only. Not investment advice.', + 'eligible_for_manual_review ≠ buy tip; no order routing, no wallet custody.', + 'Fair band / max_entry are heuristic buffers around market price — not an external model fair value.' +]; + +/** + * @param {object} input + * @param {string} [input.market_url] + * @param {string} [input.slug] + * @param {string} [input.condition_id] + * @param {string} [input.side] + * @param {number} [input.size_usd] + * @param {number} [input.bankroll_usd] caller-supplied only; never Leo private SSOT + * @param {number} [input.existing_exposure_usd] + * @param {number} [input.fair_prob] + * @param {boolean} [input.include_event_context=true] + */ +export async function assessPmDecisionCardLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const includeEvent = input.include_event_context !== false; + const ref = resolveMarketRef(input); + if (!ref.slug && !ref.condition_id) { + throw new Error('pm-decision-card requires market_url, slug, or condition_id'); + } + + const [preflightResult, readoutResult] = await Promise.allSettled([ + assessPmTradePreflightLive(input, { fetchImpl }), + includeEvent + ? assessPmEventReadoutLive({ + ...input, + include_matrix: input.include_matrix !== false, + enrich_category: input.enrich_category !== false + }, { fetchImpl }) + : Promise.resolve(null) + ]); + + if (preflightResult.status === 'rejected') { + throw preflightResult.reason; + } + const preflight = preflightResult.value; + const readout = readoutResult.status === 'fulfilled' ? readoutResult.value : null; + const readoutError = readoutResult.status === 'rejected' + ? (readoutResult.reason?.message || String(readoutResult.reason)) + : null; + + const decision = composeDecision(preflight, readout, input, { readoutError, includeEvent }); + const generated_at = new Date().toISOString(); + const stale_after_minutes = 5; + const stale_at = new Date(Date.parse(generated_at) + stale_after_minutes * 60_000).toISOString(); + const paid_checks = buildPaidChecks(preflight, readout, decision); + const value_loop = { + why_pay_again: 'Market price, spread, volume and event matrix change; re-run before each order attempt.', + stale_after_minutes, + stale_at, + best_used_in: 'agent_trading_loop_before_manual_or_automated_order', + not_a_subscription_to: 'price_alerts_or_auto_execution', + paid_value_tier: 'A_repeat_trading_loop', + fulfillment: 'edge_on_demand_no_llm', + operator_always_online: false, + llm_api_key_required: false + }; + + return { + schema_version: SCHEMA_VERSION, + service_id: SERVICE_ID, + mode: 'live', + generated_at, + input: { + market_url: input.market_url ?? null, + condition_id: ref.condition_id, + slug: ref.slug, + side: preflight.input?.side, + size_usd: preflight.input?.size_usd ?? null, + bankroll_usd: decision.public_fields.bankroll_usd, + include_event_context: includeEvent, + existing_exposure_usd: decision.public_fields.existing_exposure_usd, + decision_mode_override: normalizeDecisionMode(input.decision_mode), + fair_prob: parseProbability(input.fair_prob ?? input.fair_probability) + }, + action: decision.action, + confidence: decision.confidence, + opportunity_state: decision.public_fields.opportunity_state, + decision_mode: decision.public_fields.decision_mode, + current_price: decision.public_fields.current_price, + current_executable_ask: decision.public_fields.current_executable_ask, + max_entry: decision.public_fields.max_entry, + order_quantity_shares: decision.public_fields.order_quantity_shares, + estimated_cost_u: decision.public_fields.estimated_cost_u, + sizing_role: decision.public_fields.sizing_role, + sizing_authority: decision.public_fields.sizing_authority, + market_implied_prob: decision.public_fields.market_implied_prob, + fair_prob_range: decision.public_fields.fair_prob_range, + price_status: decision.public_fields.price_status, + edge_after_fees_buffer: decision.public_fields.edge_after_fees_buffer, + threshold_role: decision.public_fields.threshold_role, + threshold_scope: decision.public_fields.threshold_scope, + threshold_source: decision.public_fields.threshold_source, + threshold_version: decision.public_fields.threshold_version, + best_alternative_market: decision.public_fields.best_alternative_market, + best_alternative_ask: decision.public_fields.best_alternative_ask, + best_alternative_why: decision.public_fields.best_alternative_why, + chosen_market_beats_alternative: decision.public_fields.chosen_market_beats_alternative, + missing_evidence: decision.public_fields.missing_evidence, + consistency_check: decision.public_fields.consistency_check, + hard_gate: decision.public_fields.hard_gate, + buyer_summary_zh: decision.buyer_summary_zh, + buyer_summary_en: decision.buyer_summary_en, + paid_checks, + value_loop, + agent_loop: { + step_1: 'Call this card with market ref + side (+ optional size_usd / bankroll_usd / existing_exposure_usd)', + step_2: 'Read order_quantity_shares (share-first); if pending_* → supply size/bankroll or stop', + step_3: 'If skip → stop; if watch → shrink/wait; if eligible_for_manual_review → human risk check', + step_4: 'Only then place order elsewhere (this ASP never routes orders); re-run if stale' + }, + decision_card: { + ...preflight.decision_card_lite, + ...decision.public_fields, + action: decision.action, + confidence: decision.confidence, + reasons: decision.reasons, + risk_flags: decision.risk_flags, + next_actions: decision.next_actions, + event_context: decision.event_context, + paid_checks + }, + preflight: { + action: preflight.action, + confidence: preflight.confidence, + side_price: preflight.side_price, + reasons: preflight.reasons, + risk_flags: preflight.risk_flags, + market: preflight.market + }, + event_readout: readout + ? { + category: readout.category, + category_depth: readout.category_depth, + tradability: readout.tradability, + tradability_reasons: readout.tradability_reasons, + matrix_status: readout.matrix_status, + base_case: readout.base_case, + category_plugin_hint: readout.category_plugin + ? { + category: readout.category_plugin.category, + default_action_hint: readout.category_plugin.default_action_hint, + central_thesis: readout.category_plugin.central_thesis + || readout.category_plugin.market_implied_shape?.central_thesis + || null + } + : null + } + : null, + caveats: [ + ...STANDARD_CAVEATS, + ...(preflight.caveats || []).slice(0, 3), + ...(readoutResult.status === 'rejected' + ? [`Event context unavailable: ${readoutResult.reason?.message || readoutResult.reason}`] + : []) + ], + next_gate: 'Human_or_hard_risk_limit_before_any_order', + source: { + method: 'compose_pm_trade_preflight_plus_optional_pm_event_readout', + paid_value_tier: 'A_repeat_trading_loop' + } + }; +} + +export function buildPmDecisionCardFallback(input = {}) { + const preflight = buildPmTradePreflightFallback(input); + const generated_at = new Date().toISOString(); + const fallbackFields = { + opportunity_state: 'data_blocked', + decision_mode: normalizeDecisionMode(input.decision_mode) ?? 'no_trade', + current_price: preflight.side_price ?? null, + current_executable_ask: preflight.side_price ?? null, + max_entry: preflight.decision_card_lite?.max_entry ?? null, + market_implied_prob: preflight.side_price ?? null, + fair_prob_range: preflight.decision_card_lite?.fair_prob_range ?? null, + price_status: 'no_edge', + edge_after_fees_buffer: null, + threshold_role: 'block_or_verify', + threshold_scope: 'public_market_price_liquidity_event_matrix_no_private_bankroll', + threshold_source: PUBLIC_THRESHOLD_SOURCE, + threshold_version: PUBLIC_THRESHOLD_VERSION, + best_alternative_market: null, + best_alternative_ask: null, + best_alternative_why: null, + chosen_market_beats_alternative: 'unknown', + missing_evidence: ['live_market_data_unavailable', 'event_context_unavailable'], + consistency_check: 'incomplete', + hard_gate: PUBLIC_HARD_GATE, + existing_exposure_usd: parseOptionalUsd(input.existing_exposure_usd ?? input.exposure_usd), + bankroll_usd: parseOptionalUsd(input.bankroll_usd), + order_quantity_shares: 'pending_caller_size', + estimated_cost_u: null, + sizing_role: 'none', + sizing_authority: CALLER_SIZING_AUTHORITY + }; + return { + schema_version: SCHEMA_VERSION, + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at, + input: preflight.input, + action: 'watch', + confidence: 0.4, + ...fallbackFields, + buyer_summary_zh: '演示回退:opportunity_state=data_blocked;缺口 live_market_data_unavailable。默认 watch。', + buyer_summary_en: 'Demo fallback: opportunity_state=data_blocked; missing live_market_data_unavailable. Default watch.', + paid_checks: { + pass_count: 0, + fail_count: 1, + warn_count: 0, + checks: [{ id: 'live_market', status: 'fail', detail: 'live_data_unavailable' }] + }, + value_loop: { + why_pay_again: 'Live market state changes; re-run before each order attempt.', + stale_after_minutes: 5, + stale_at: new Date(Date.parse(generated_at) + 5 * 60_000).toISOString(), + best_used_in: 'agent_trading_loop_before_manual_or_automated_order', + not_a_subscription_to: 'price_alerts_or_auto_execution' + }, + decision_card: { + ...preflight.decision_card_lite, + ...fallbackFields, + action: 'watch', + next_actions: ['retry_with_live_market_ref'] + }, + preflight, + event_readout: null, + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Human_or_hard_risk_limit_before_any_order', + source: { method: 'static_fallback' } + }; +} + +function composeDecision(preflight, readout, input = {}, context = {}) { + let action = mapPreflightAction(preflight.action); + const reasons = [...(preflight.reasons || [])]; + const risk_flags = [...(preflight.risk_flags || [])]; + let confidence = Number(preflight.confidence) || 0.5; + + const event_context = readout + ? { + category: readout.category, + tradability: readout.tradability, + matrix_status: readout.matrix_status, + plugin_action_hint: readout.category_plugin?.default_action_hint ?? null + } + : null; + + if (readout) { + if (readout.tradability === 'weak' || readout.tradability === 'low') { + if (action === 'eligible_for_manual_review') action = 'watch'; + risk_flags.push('event_tradability_weak'); + reasons.push(`Event readout tradability=${readout.tradability}; tighten to watch.`); + confidence -= 0.08; + } + const hint = readout.category_plugin?.default_action_hint; + if (hint === 'no_trade' || hint === 'no_trade_until_source_and_ladder_ok') { + action = action === 'skip' ? 'skip' : 'watch'; + risk_flags.push('category_plugin_no_trade_hint'); + reasons.push(`Category plugin hint=${hint}.`); + confidence -= 0.1; + } + if (readout.matrix_status === 'incomplete') { + risk_flags.push('event_matrix_incomplete'); + reasons.push('Same-event matrix incomplete — compare expressions carefully.'); + confidence -= 0.05; + } + } + + confidence = Math.max(0.35, Math.min(0.9, Math.round(confidence * 100) / 100)); + + const public_fields = buildPublicDecisionFields({ + preflight, + readout, + input, + action, + risk_flags, + readoutError: context.readoutError, + includeEvent: context.includeEvent + }); + + const next_actions = []; + if (action === 'skip') { + next_actions.push('do_not_enter', 'pick_another_market_or_wait_for_reopen'); + } else if (action === 'watch') { + next_actions.push('reduce_size_or_wait', 'recheck_after_liquidity_improves', 'optional_run_category_match_card'); + } else { + next_actions.push('manual_risk_check', 'optional_compare_adjacent_ladder', 'only_then_consider_order'); + } + + const buyer_summary_zh = buildBuyerSummaryZh(action, preflight, readout, confidence, public_fields); + const buyer_summary_en = buildBuyerSummaryEn(action, preflight, readout, confidence, public_fields); + + return { + action, + confidence, + reasons, + risk_flags: [...new Set(risk_flags)], + next_actions, + event_context, + public_fields, + buyer_summary_zh, + buyer_summary_en + }; +} + +function mapPreflightAction(action) { + if (action === 'skip') return 'skip'; + if (action === 'watch') return 'watch'; + if (action === 'eligible') return 'eligible_for_manual_review'; + return 'watch'; +} + +function buildPublicDecisionFields({ + preflight, + readout, + input, + action, + risk_flags, + readoutError, + includeEvent +}) { + const current_price = numberOrNull(preflight.side_price); + const current_executable_ask = resolveExecutableAsk(preflight, input); + const market_implied_prob = current_price; + const callerFair = parseProbability(input.fair_prob ?? input.fair_probability); + const lite = preflight.decision_card_lite || {}; + const fair_prob_range = callerFair !== null + ? [roundProb(clamp(callerFair - 0.02, 0.01, 0.99)), roundProb(clamp(callerFair + 0.02, 0.01, 0.99))] + : (Array.isArray(lite.fair_prob_range) ? lite.fair_prob_range : null); + const max_entry = callerFair !== null + ? roundProb(clamp(callerFair - DEFAULT_FEE_BUFFER, 0.01, 0.99)) + : numberOrNull(lite.max_entry); + const edge_after_fees_buffer = callerFair !== null && current_executable_ask !== null + ? roundProb(callerFair - current_executable_ask - DEFAULT_FEE_BUFFER) + : null; + + const decision_mode = normalizeDecisionMode(input.decision_mode) + ?? inferDecisionModeFromReadout(readout); + const price_status = classifyPublicPriceStatus({ + action, + current_executable_ask, + max_entry, + callerFair, + edge_after_fees_buffer, + risk_flags, + decision_mode + }); + const alternative = extractBestAlternative(readout, preflight.market?.slug); + const chosen_market_beats_alternative = computeChosenBeatsAlternative(readout, preflight.market?.slug); + const missing_evidence = collectMissingEvidence({ + preflight, + readout, + input, + risk_flags, + readoutError, + includeEvent + }); + const criticalMissing = missing_evidence.filter((item) => ![ + 'existing_exposure_usd_not_supplied', + 'caller_size_or_bankroll_not_supplied' + ].includes(item)); + const opportunity_state = inferOpportunityState({ + action, + decision_mode, + price_status, + criticalMissing, + chosen_market_beats_alternative + }); + const consistency_check = inferConsistencyCheck({ + criticalMissing, + chosen_market_beats_alternative, + decision_mode, + action + }); + const threshold_role = inferThresholdRole({ opportunity_state, decision_mode, price_status }); + const sizing = computeCallerShareFirstSizing({ + input, + current_executable_ask, + opportunity_state, + action + }); + + return { + opportunity_state, + decision_mode, + current_price, + current_executable_ask, + max_entry, + order_quantity_shares: sizing.order_quantity_shares, + estimated_cost_u: sizing.estimated_cost_u, + sizing_role: sizing.sizing_role, + sizing_authority: sizing.sizing_authority, + market_implied_prob, + fair_prob_range, + price_status, + edge_after_fees_buffer, + threshold_role, + threshold_scope: 'public_market_price_liquidity_event_matrix_no_private_bankroll', + threshold_source: PUBLIC_THRESHOLD_SOURCE, + threshold_version: PUBLIC_THRESHOLD_VERSION, + best_alternative_market: alternative?.market ?? null, + best_alternative_ask: alternative?.ask ?? null, + best_alternative_why: alternative?.why ?? null, + chosen_market_beats_alternative, + missing_evidence, + consistency_check, + hard_gate: PUBLIC_HARD_GATE, + existing_exposure_usd: parseOptionalUsd(input.existing_exposure_usd ?? input.exposure_usd), + bankroll_usd: sizing.bankroll_usd + }; +} + +/** + * Share-first sizing from caller-supplied bankroll/size only. + * Never reads Leo private bankroll SSOT. No orders. + */ +function computeCallerShareFirstSizing({ input, current_executable_ask, opportunity_state, action }) { + const bankroll_usd = parseOptionalUsd(input.bankroll_usd); + const size_usd = parseOptionalUsd(input.size_usd ?? input.target_cost_usd); + const exposure_usd = parseOptionalUsd(input.existing_exposure_usd ?? input.exposure_usd); + const ask = current_executable_ask; + + if (action === 'skip' || ask == null || ask <= 0) { + return { + bankroll_usd, + order_quantity_shares: size_usd == null && bankroll_usd == null ? 'pending_caller_size' : 'pending_anchor', + estimated_cost_u: null, + sizing_role: 'none', + sizing_authority: CALLER_SIZING_AUTHORITY + }; + } + + if (size_usd == null && bankroll_usd == null) { + return { + bankroll_usd: null, + order_quantity_shares: 'pending_caller_size', + estimated_cost_u: null, + sizing_role: 'none', + sizing_authority: CALLER_SIZING_AUTHORITY + }; + } + + // Share math is still returned when opportunity_state=data_blocked so the + // caller gets an enterable quantity; missing_evidence remains the hard gate. + + let targetCost = size_usd; + let sizing_role = 'caller_supplied'; + if (targetCost == null && bankroll_usd != null) { + // Public heuristic probe envelope only — not Leo risk SSOT B_normal ceiling. + targetCost = Math.round(bankroll_usd * B_MINUS_PROBE_BANKROLL_FRAC * 100) / 100; + sizing_role = 'B_minus_probe'; + } else if (bankroll_usd != null && size_usd != null) { + const frac = size_usd / bankroll_usd; + sizing_role = frac <= B_MINUS_PROBE_BANKROLL_FRAC + 1e-9 ? 'B_minus_probe' : 'caller_supplied'; + } + + if (exposure_usd != null && bankroll_usd != null && exposure_usd + (targetCost || 0) > bankroll_usd * 0.25) { + return { + bankroll_usd, + order_quantity_shares: 'pending_anchor', + estimated_cost_u: null, + sizing_role: 'none', + sizing_authority: CALLER_SIZING_AUTHORITY + }; + } + + const shares = Math.floor((targetCost || 0) / ask); + if (shares <= 0) { + return { + bankroll_usd, + order_quantity_shares: 'pending_caller_size', + estimated_cost_u: null, + sizing_role: 'none', + sizing_authority: CALLER_SIZING_AUTHORITY + }; + } + + return { + bankroll_usd, + order_quantity_shares: shares, + estimated_cost_u: Math.round(shares * ask * 100) / 100, + sizing_role, + sizing_authority: CALLER_SIZING_AUTHORITY + }; +} + +function resolveExecutableAsk(preflight, input = {}) { + const current = numberOrNull(preflight.side_price); + if (current === null) return null; + const side = String(preflight.input?.side ?? input.side ?? 'yes').trim().toLowerCase(); + const bestAsk = numberOrNull(preflight.market?.best_ask); + // Gamma bestAsk is reliable for the primary Yes token; for No, outcomePrices is + // the safer public approximation unless a side-aware CLOB book is added later. + if (side === 'yes' && bestAsk !== null) return roundProb(bestAsk); + return current; +} + +function inferDecisionModeFromReadout(readout) { + const hint = readout?.category_plugin?.default_action_hint; + if (typeof hint === 'string' && hint.includes('no_trade')) return 'no_trade'; + const batchAction = readout?.category_plugin?.batch2_public?.action; + if (batchAction === 'skip' || batchAction === 'no_trade') return 'no_trade'; + return 'price_edge'; +} + +function classifyPublicPriceStatus({ + action, + current_executable_ask, + max_entry, + callerFair, + edge_after_fees_buffer, + risk_flags, + decision_mode +}) { + if (current_executable_ask === null || action === 'skip' || decision_mode === 'no_trade') return 'no_edge'; + if (callerFair !== null && edge_after_fees_buffer !== null) { + if (edge_after_fees_buffer >= 0.05) return 'cheap'; + if (edge_after_fees_buffer >= 0) return 'acceptable'; + if (current_executable_ask <= callerFair + 0.02) return 'full'; + return 'rich'; + } + if (max_entry !== null && current_executable_ask <= max_entry - 0.03) return 'cheap'; + if (action === 'eligible_for_manual_review') return 'acceptable'; + if ((risk_flags || []).includes('extreme_implied_probability')) return 'rich'; + if (action === 'watch') return 'full'; + return 'no_edge'; +} + +function extractBestAlternative(readout, currentSlug) { + const plugin = readout?.category_plugin; + const cmp = plugin?.expression_comparison; + if (cmp?.recommended) { + const rec = compactAlternative(cmp.recommended); + if (rec && !sameSlug(rec.slug, currentSlug)) return rec; + } + if (Array.isArray(cmp?.candidates)) { + const candidate = cmp.candidates.find((row) => row && !sameSlug(row.slug, currentSlug)); + const alt = compactAlternative(candidate); + if (alt) return alt; + } + const bestExpression = plugin?.batch2_public?.best_expression ?? plugin?.best_expression; + if (bestExpression?.market) { + return { + market: bestExpression.market, + slug: bestExpression.market, + ask: numberOrNull(bestExpression.ask ?? bestExpression.yes), + why: bestExpression.why ?? bestExpression.note ?? 'Best expression from category plugin.' + }; + } + return null; +} + +function compactAlternative(row) { + if (!row) return null; + return { + market: row.market ?? row.expression ?? row.slug ?? null, + slug: row.slug ?? null, + ask: numberOrNull(row.ask ?? row.yes ?? row.price), + why: row.why_consider ?? row.why ?? row.path ?? row.note ?? null + }; +} + +function computeChosenBeatsAlternative(readout, currentSlug) { + const recommended = readout?.category_plugin?.expression_comparison?.recommended; + if (!recommended?.slug || !currentSlug) return 'unknown'; + return sameSlug(recommended.slug, currentSlug) ? 'yes' : 'no'; +} + +function collectMissingEvidence({ + preflight, + readout, + input, + risk_flags, + readoutError, + includeEvent +}) { + const missing = []; + if ((risk_flags || []).includes('missing_side_price')) missing.push('current_side_price_missing'); + if ((risk_flags || []).includes('market_closed_or_inactive')) missing.push('market_closed_or_inactive'); + if (readoutError) missing.push('event_context_unavailable'); + if (includeEvent === false) missing.push('event_context_omitted'); + if (!readout && includeEvent !== false) missing.push('event_context_unavailable'); + if (readout?.matrix_status === 'incomplete') missing.push('same_event_matrix_incomplete'); + const plugin = readout?.category_plugin; + const fixtureStatus = plugin?.fixture?.fixture_status ?? plugin?.batch2_public?.fixture_status ?? plugin?.fixture_status; + if (fixtureStatus && !['ok', 'not_applicable'].includes(fixtureStatus)) { + missing.push(`fixture_${fixtureStatus}`); + } + if (Array.isArray(plugin?.missing_market_groups) && plugin.missing_market_groups.length) { + missing.push(`missing_market_groups:${plugin.missing_market_groups.slice(0, 5).join(',')}`); + } + const hint = plugin?.default_action_hint; + if (typeof hint === 'string' && hint.includes('no_trade')) { + missing.push(`category_plugin_${hint}`); + } + if (readout?.tradability === 'weak' || readout?.tradability === 'low') { + missing.push(`event_tradability_${readout.tradability}`); + } + if (parseOptionalUsd(input.existing_exposure_usd ?? input.exposure_usd) === null) { + missing.push('existing_exposure_usd_not_supplied'); + } + if ( + parseOptionalUsd(input.size_usd ?? input.target_cost_usd) === null + && parseOptionalUsd(input.bankroll_usd) === null + ) { + missing.push('caller_size_or_bankroll_not_supplied'); + } + return [...new Set(missing)]; +} + +function inferOpportunityState({ + action, + decision_mode, + price_status, + criticalMissing, + chosen_market_beats_alternative +}) { + if (criticalMissing.length) return 'data_blocked'; + if (chosen_market_beats_alternative === 'no') return 'no_edge'; + if (decision_mode === 'event_outcome') return 'event_outcome'; + if (action === 'skip' || decision_mode === 'no_trade' || ['rich', 'no_edge'].includes(price_status)) { + return 'no_edge'; + } + if (action === 'eligible_for_manual_review' && ['cheap', 'acceptable'].includes(price_status)) { + return 'strong_micro_candidate'; + } + if (action === 'watch') return 'manual_micro_validation'; + return 'no_edge'; +} + +function inferConsistencyCheck({ criticalMissing, chosen_market_beats_alternative, decision_mode, action }) { + if (criticalMissing.length) return 'incomplete'; + if (chosen_market_beats_alternative === 'no') return 'conflict'; + if (decision_mode === 'no_trade' && action === 'eligible_for_manual_review') return 'conflict'; + return 'ok'; +} + +function inferThresholdRole({ opportunity_state, decision_mode, price_status }) { + if (opportunity_state === 'data_blocked') return 'verification_block'; + if (opportunity_state === 'no_edge') return 'block_or_wait'; + if (decision_mode === 'event_outcome') return 'event_path_participation'; + if (opportunity_state === 'strong_micro_candidate' && ['cheap', 'acceptable'].includes(price_status)) { + return 'public_promotion_candidate'; + } + return 'manual_micro_validation'; +} + +function buildPaidChecks(preflight, readout, decision) { + const flags = new Set(decision.risk_flags || []); + const checks = [ + checkFromFlag('market_open', !flags.has('market_closed_or_inactive'), 'market not closed/inactive'), + checkFromFlag('side_price', !flags.has('missing_side_price'), `side_price=${preflight.side_price ?? 'n/a'}`), + checkFromFlag('liquidity_24h', !flags.has('low_liquidity'), '24h volume vs threshold'), + checkFromFlag('spread', !flags.has('wide_spread'), 'bid/ask spread heuristic'), + checkFromFlag('price_zone', !flags.has('extreme_implied_probability'), 'not extreme entry zone'), + checkFromFlag('size_vs_volume', !flags.has('size_large_vs_daily_volume'), 'size vs 24h volume') + ]; + if (readout) { + checks.push( + checkFromFlag( + 'event_tradability', + !flags.has('event_tradability_weak'), + `tradability=${readout.tradability ?? 'n/a'}` + ), + checkFromFlag( + 'event_matrix', + !flags.has('event_matrix_incomplete'), + `matrix_status=${readout.matrix_status ?? 'n/a'}` + ), + checkFromFlag( + 'category_plugin', + !flags.has('category_plugin_no_trade_hint'), + `plugin_hint=${readout.category_plugin?.default_action_hint ?? 'none'}` + ) + ); + } else { + checks.push({ id: 'event_context', status: 'warn', detail: 'event context omitted or unavailable' }); + } + + const pass_count = checks.filter((c) => c.status === 'pass').length; + const fail_count = checks.filter((c) => c.status === 'fail').length; + const warn_count = checks.filter((c) => c.status === 'warn').length; + return { pass_count, fail_count, warn_count, checks }; +} + +function checkFromFlag(id, ok, detail) { + return { id, status: ok ? 'pass' : 'fail', detail }; +} + +function buildBuyerSummaryZh(action, preflight, readout, confidence, publicFields = {}) { + const actionZh = { + skip: '跳过', + watch: '先观望', + eligible_for_manual_review: '机械检查过关,仍需人工风控' + }[action] || action; + const price = preflight.side_price; + const cat = readout?.category ? `;事件品类 ${readout.category}/${readout.tradability}` : ''; + const missingTop = publicFields.missing_evidence?.[0] + ? `;主要缺口 ${publicFields.missing_evidence[0]}` + : ''; + return `决策卡:${actionZh}(置信 ${confidence})。opportunity_state=${publicFields.opportunity_state ?? 'unknown'}${missingTop}。侧价 ${price ?? 'n/a'}${cat}。eligible≠买点;下单前再跑一次。`; +} + +function buildBuyerSummaryEn(action, preflight, readout, confidence, publicFields = {}) { + const actionEn = { + skip: 'skip', + watch: 'watch', + eligible_for_manual_review: 'eligible for manual review (not a buy tip)' + }[action] || action; + const price = preflight.side_price; + const cat = readout?.category ? `; event ${readout.category}/${readout.tradability}` : ''; + const missingTop = publicFields.missing_evidence?.[0] + ? `; top missing evidence: ${publicFields.missing_evidence[0]}` + : ''; + return `Decision card: ${actionEn} (confidence ${confidence}). opportunity_state=${publicFields.opportunity_state ?? 'unknown'}${missingTop}. Side price ${price ?? 'n/a'}${cat}. Re-run before any order.`; +} + +function normalizeDecisionMode(value) { + const raw = String(value ?? '').trim().toLowerCase(); + if (raw === 'event_outcome' || raw === 'price_edge' || raw === 'no_trade') return raw; + return null; +} + +function parseProbability(value) { + if (value === null || value === undefined || value === '') return null; + const n = Number(value); + if (!Number.isFinite(n)) return null; + return n > 0 && n < 1 ? roundProb(n) : null; +} + +function parseOptionalUsd(value) { + if (value === null || value === undefined || value === '') return null; + const n = Number(value); + return Number.isFinite(n) && n >= 0 ? Math.round(n * 100) / 100 : null; +} + +function numberOrNull(value) { + const n = Number(value); + return Number.isFinite(n) ? roundProb(n) : null; +} + +function roundProb(value) { + return Math.round(value * 1000) / 1000; +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function sameSlug(a, b) { + if (!a || !b) return false; + return String(a).trim().toLowerCase() === String(b).trim().toLowerCase(); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-readout.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-readout.mjs new file mode 100644 index 00000000..f44f5655 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-readout.mjs @@ -0,0 +1,753 @@ +// PM Event Readout (pm_event_readout) — L0 generic event evidence card. +// Framework: research/2026-07-09-pm-event-analyst-framework.md +// Rule-based + public Gamma (+ optional external anchors). No Leo relay / no orders. + +import { + fetchMarket, + fetchEventBySlug, + fetchEventBundleWithSiblings, + eventSlugFromMarket, + resolveMarketRef, + round2, + toNumber +} from './pm-gamma-market.mjs'; +import { enrichMuskCategory, extractMuskSnapshot } from './pm-category-musk.mjs'; +import { enrichFootballCategory, extractFootballFixture } from './pm-category-football.mjs'; +import { enrichTennisCategory, extractTennisFixture } from './pm-category-tennis.mjs'; +import { enrichWeatherCategory, extractWeatherSnapshot } from './pm-category-weather.mjs'; +import { enrichNbaCategory, extractNbaFixture } from './pm-category-nba.mjs'; +import { enrichPoliticsCategory } from './pm-category-politics.mjs'; +import { enrichMacroFedCategory } from './pm-category-macro-fed.mjs'; + +const SERVICE_ID = 'pm_event_readout'; +const SCHEMA_VERSION = '0.2'; + +const STANDARD_CAVEATS = [ + 'Event readout only. Not investment advice; does not place or route orders.', + 'Separates event likelihood from trade attractiveness — use pm-trade-preflight or pm-decision-card for action.', + 'L0 core: Gamma matrix + optional public anchors. Category plugins (sports/weather/musk) are not required for generic events.' +]; + +const FED_HOLD_HINTS = [ + /no change in fed interest rates/i, + /fed.*no change/i, + /interest rates after the .+ meeting/i +]; + +/** + * @param {object} input + * @param {object} [options] + * @param {typeof fetch} [options.fetchImpl] + * @param {object[]} [options.externalAnchors] inject anchors in tests + * @param {boolean} [options.includeMatrix=true] + */ +export async function assessPmEventReadoutLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const includeMatrix = input.include_matrix !== false && options.includeMatrix !== false; + const marketRef = resolveMarketRef(input); + const market = await fetchMarket(fetchImpl, marketRef); + if (!market) { + throw new Error('Market not found for the provided slug, condition_id, or market_url'); + } + + let eventBundle = null; + const parentSlug = eventSlugFromMarket(market); + if (includeMatrix && parentSlug) { + try { + // Football/tennis often split ML vs more-markets — merge siblings for serious matrix. + eventBundle = await fetchEventBundleWithSiblings(fetchImpl, parentSlug); + } catch { + try { + eventBundle = await fetchEventBySlug(fetchImpl, parentSlug); + } catch { + eventBundle = null; + } + } + } + + const injectedAnchors = Array.isArray(options.externalAnchors) ? options.externalAnchors : null; + const externalAnchors = injectedAnchors ?? await resolveExternalAnchors(market, eventBundle, fetchImpl); + + const readout = buildEventReadout(market, eventBundle, externalAnchors); + const muskSnapshot = extractMuskSnapshot(input, options); + const footballFixture = extractFootballFixture(input, options); + const tennisFixture = extractTennisFixture(input, options); + const weatherSnapshot = extractWeatherSnapshot(input, options); + const nbaFixture = extractNbaFixture(input, options); + const categoryPlugin = maybeApplyCategoryPlugin({ + readout, + market, + eventBundle, + muskSnapshot, + footballFixture, + tennisFixture, + weatherSnapshot, + nbaFixture, + enrichCategory: input.enrich_category !== false && options.enrichCategory !== false + }); + + // Category plugins may tighten tradability (fixture/matrix honesty). + let tradability = readout.tradability; + let tradability_reasons = [...(readout.tradability_reasons || [])]; + if (categoryPlugin.category_plugin?.tradability_cap) { + tradability = capTradability(tradability, categoryPlugin.category_plugin.tradability_cap); + tradability_reasons = [ + ...tradability_reasons, + ...(categoryPlugin.category_plugin.tradability_reasons || []) + ]; + } + + return { + schema_version: SCHEMA_VERSION, + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + market_url: input.market_url ?? null, + condition_id: marketRef.condition_id, + slug: marketRef.slug, + include_matrix: includeMatrix, + enrich_category: input.enrich_category !== false, + musk: muskSnapshot, + football: footballFixture, + tennis: tennisFixture, + nba: nbaFixture + }, + ...readout, + tradability, + tradability_reasons, + ...categoryPlugin, + caveats: [ + ...STANDARD_CAVEATS, + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'football' + ? ['Football plugin applied: sibling more-markets merged; fixture gate + expression comparison. Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'tennis' + ? ['Tennis plugin applied: format + named ML + set handicap/totals + domination check. Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'nba' + ? ['NBA plugin applied: moneyline/spread/totals matrix + heuristic coherence. Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'nfl' + ? ['NFL plugin applied (same ML/spread/totals matrix shape as NBA L1). Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'ufc' + ? ['UFC/MMA plugin applied (fight moneyline-focused matrix via US-sports L1). Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'mlb' + ? ['MLB plugin applied (ML/spread/totals matrix via US-sports L1). Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'politics' + ? ['Politics plugin applied: candidate yes-mass leaderboard + exclusivity sanity. Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'macro_fed' + ? ['Macro Fed plugin applied: rate-decision ladder + expected-move heuristic. Not a buy tip.'] + : []), + ...(categoryPlugin.category_depth === 'enriched' && categoryPlugin.category === 'musk' + ? ['Musk ladder plugin applied (shape demo). Prefer football/tennis/nba for sports depth.'] + : []) + ], + next_gate: 'Use_pm_trade_preflight_or_manual_decision_card_before_orders', + source: { + provider: 'polymarket_gamma_public_api', + fields: ['outcomePrices', 'volume24hr', 'oneDayPriceChange', 'endDate', 'bestBid/Ask', 'events', 'event.markets', 'siblings'], + framework: categoryPlugin.category_depth === 'enriched' + ? `pm-event-analyst-framework-L0+L1-${categoryPlugin.category}` + : 'pm-event-analyst-framework-L0' + } + }; +} + +function capTradability(current, maxLevel) { + const order = ['weak', 'low', 'medium', 'high']; + return order[Math.min(order.indexOf(current), order.indexOf(maxLevel))]; +} + +function maybeApplyCategoryPlugin({ + readout, + market, + eventBundle, + muskSnapshot, + footballFixture, + tennisFixture, + weatherSnapshot, + nbaFixture, + enrichCategory +}) { + if (!enrichCategory) { + return { + category_depth: 'core_only', + category_plugin: null + }; + } + if (readout.category === 'football') { + const plugin = enrichFootballCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix, + fixture: footballFixture + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + // Football plugin owns matrix honesty when enriched + matrix_status: plugin.matrix_status, + missing_market_groups: plugin.missing_market_groups, + related_market_count: plugin.related_market_count, + category_plugin: plugin + }; + } + if (readout.category === 'tennis') { + const plugin = enrichTennisCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix, + fixture: tennisFixture + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + matrix_status: plugin.matrix_status, + missing_market_groups: plugin.missing_market_groups, + related_market_count: plugin.related_market_count, + category_plugin: plugin + }; + } + if (readout.category === 'nba' || readout.category === 'nfl' || readout.category === 'ufc' || readout.category === 'mlb') { + const plugin = enrichNbaCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix, + fixture: nbaFixture + }); + if (readout.category === 'nfl' || readout.category === 'ufc' || readout.category === 'mlb') { + plugin.category = readout.category; + plugin.skill_alignment = { + ...(plugin.skill_alignment || {}), + source: `sports_generalization_${readout.category}_via_us_sports_l1_matrix` + }; + } + return { + category: plugin.category, + category_depth: plugin.category_depth, + matrix_status: plugin.matrix_status, + missing_market_groups: plugin.missing_market_groups, + related_market_count: plugin.related_market_count, + category_plugin: plugin + }; + } + if (readout.category === 'weather') { + const plugin = enrichWeatherCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix, + snapshot: weatherSnapshot + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + category_plugin: plugin + }; + } + if (readout.category === 'musk') { + const plugin = enrichMuskCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix, + snapshot: muskSnapshot + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + category_plugin: plugin + }; + } + if (readout.category === 'politics') { + const plugin = enrichPoliticsCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + category_plugin: plugin + }; + } + if (readout.category === 'macro_fed') { + const plugin = enrichMacroFedCategory({ + market, + eventBundle, + eventMatrix: readout.event_matrix + }); + return { + category: plugin.category, + category_depth: plugin.category_depth, + category_plugin: plugin + }; + } + return { + category_depth: 'core_only', + category_plugin: null + }; +} + +export function buildPmEventReadoutFallback(input = {}) { + return { + schema_version: SCHEMA_VERSION, + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + market_url: input?.market_url ?? null, + slug: input?.slug ?? 'demo-market', + include_matrix: true + }, + event: 'Demo event', + event_slug: null, + market: 'Demo market (live data unavailable)', + current_price: { yes: 0.42, no: 0.58 }, + event_time: null, + event_time_source: null, + fixture_status: 'unknown', + market_fixture_match: 'unknown', + category: 'generic', + category_depth: 'core_only', + plugins_available: ['football', 'tennis', 'nba', 'nfl', 'ufc', 'mlb', 'politics', 'macro_fed', 'weather', 'musk'], + sources_read: ['static_fallback'], + base_case: 'Demo readout only.', + key_uncertainties: ['live_data_unavailable'], + market_implied_view: 'Demo mode — no live implied view.', + what_is_already_priced: null, + what_may_not_be_priced: null, + event_matrix: [], + matrix_status: 'incomplete', + related_market_count: 0, + missing_market_groups: ['live_event_lookup_unavailable'], + external_anchors: [], + tradability: 'weak', + tradability_reasons: ['live_data_unavailable'], + next_decision_card_needed: 'no', + hard_gate: 'no_orders_no_account_mutation', + caveats: [...STANDARD_CAVEATS, 'Demo mode: do not trade on this response.'], + next_gate: 'Use_pm_trade_preflight_or_manual_decision_card_before_orders', + source: { provider: 'static_fallback' } + }; +} + +function buildEventReadout(market, eventBundle, externalAnchors) { + const yesIdx = market.outcomes.findIndex((o) => String(o).toLowerCase() === 'yes'); + const noIdx = market.outcomes.findIndex((o) => String(o).toLowerCase() === 'no'); + const primaryIdx = yesIdx >= 0 ? yesIdx : 0; + const primaryOutcome = market.outcomes[primaryIdx] ?? 'Yes'; + const primaryPrice = Number.isFinite(market.outcome_prices[primaryIdx]) + ? market.outcome_prices[primaryIdx] + : null; + + const prices = {}; + if (yesIdx >= 0 && Number.isFinite(market.outcome_prices[yesIdx])) { + prices.yes = round2(market.outcome_prices[yesIdx]); + } + if (noIdx >= 0 && Number.isFinite(market.outcome_prices[noIdx])) { + prices.no = round2(market.outcome_prices[noIdx]); + } + // Tennis / named two-way markets: expose both sides when Yes/No absent. + if (prices.yes == null && prices.no == null && market.outcomes?.length >= 2) { + prices.named = market.outcomes.map((label, idx) => ({ + label: String(label), + price: Number.isFinite(market.outcome_prices[idx]) ? round2(market.outcome_prices[idx]) : null + })); + if (Number.isFinite(market.outcome_prices[0])) prices.outcome_0 = round2(market.outcome_prices[0]); + if (Number.isFinite(market.outcome_prices[1])) prices.outcome_1 = round2(market.outcome_prices[1]); + } + + const matrixInfo = buildEventMatrix(market, eventBundle); + const category = detectCategory(market, eventBundle); + const { tradability, tradability_reasons } = scoreTradability(market, matrixInfo, externalAnchors); + const implied = buildImpliedView(market, primaryOutcome, primaryPrice, matrixInfo); + const priced = buildPricedInNotes(market, primaryPrice, matrixInfo, externalAnchors); + const uncertainties = buildUncertainties(market, matrixInfo, externalAnchors); + const sources = ['polymarket_gamma_market_metadata']; + if (eventBundle?.slug) sources.push(`polymarket_gamma_event:${eventBundle.slug}`); + for (const anchor of externalAnchors) { + if (anchor?.id) sources.push(`external_anchor:${anchor.id}`); + } + + const eventTitle = eventBundle?.title || market.events?.[0]?.title || market.title; + const eventSlug = eventBundle?.slug || eventSlugFromMarket(market); + + // Tennis fixture gate overrides L0's naive endDate→ok mapping. + if (category === 'tennis' && !eventBundle?.start_time && !market.start_time) { + // leave as-is; L1 plugin will set honest fixture + } + + return { + event: eventTitle, + event_slug: eventSlug, + market: market.slug, + market_title: market.title, + group_item_title: market.group_item_title, + current_price: prices, + event_time: market.end_date, + event_time_source: market.end_date ? 'polymarket_gamma_endDate' : null, + fixture_status: market.end_date ? 'ok' : 'unknown', + market_fixture_match: eventSlug ? 'ok' : 'single_market_no_parent_event', + category, + category_depth: 'core_only', + plugins_available: ['football', 'tennis', 'nba', 'nfl', 'ufc', 'mlb', 'politics', 'macro_fed', 'weather', 'musk'], + sources_read: sources, + base_case: primaryPrice !== null + ? `Market prices "${primaryOutcome}" at ${round2(primaryPrice)} (${Math.round(primaryPrice * 100)}% implied).` + : 'Outcome prices unavailable from Gamma.', + key_uncertainties: uncertainties, + market_implied_view: implied, + what_is_already_priced: priced.already, + what_may_not_be_priced: priced.may_not, + event_matrix: matrixInfo.event_matrix, + matrix_status: matrixInfo.matrix_status, + related_market_count: matrixInfo.related_market_count, + missing_market_groups: matrixInfo.missing_market_groups, + external_anchors: externalAnchors, + tradability, + tradability_reasons, + next_decision_card_needed: ['medium', 'high'].includes(tradability) ? 'yes' : 'no', + hard_gate: 'no_orders_no_account_mutation' + }; +} + +function buildEventMatrix(primaryMarket, eventBundle) { + if (!eventBundle?.markets?.length) { + return { + event_matrix: [matrixRow(primaryMarket, true)], + matrix_status: 'incomplete', + related_market_count: 1, + missing_market_groups: ['parent_event_not_resolved'] + }; + } + + const rows = eventBundle.markets + .map((m) => matrixRow(m, m.condition_id === primaryMarket.condition_id)) + .sort((a, b) => (b.volume_24h_usd || 0) - (a.volume_24h_usd || 0)); + + const missing = []; + if (rows.length < 2) missing.push('expected_multi_outcome_event_but_only_one_market'); + + // Soft check for FOMC-style brackets when category looks macro_fed + const titles = rows.map((r) => `${r.title} ${r.group_item_title || ''}`.toLowerCase()); + const looksFed = titles.some((t) => t.includes('fed') || t.includes('bps') || t.includes('no change')); + if (looksFed) { + const hasHold = titles.some((t) => t.includes('no change')); + const hasHike25 = titles.some((t) => t.includes('25') && t.includes('increase')); + const hasCut25 = titles.some((t) => t.includes('25') && t.includes('decrease')); + if (!hasHold) missing.push('fed_bracket_missing_no_change'); + if (!hasHike25) missing.push('fed_bracket_missing_25bps_increase'); + if (!hasCut25) missing.push('fed_bracket_missing_25bps_decrease'); + } + + return { + event_matrix: rows, + matrix_status: missing.length ? 'incomplete' : 'complete', + related_market_count: rows.length, + missing_market_groups: missing + }; +} + +function matrixRow(market, isPrimary) { + const yesIdx = market.outcomes.findIndex((o) => String(o).toLowerCase() === 'yes'); + const overIdx = market.outcomes.findIndex((o) => /^over/i.test(String(o))); + const primaryIdx = yesIdx >= 0 ? yesIdx : (overIdx >= 0 ? overIdx : 0); + const yes = Number.isFinite(market.outcome_prices[primaryIdx]) + ? round2(market.outcome_prices[primaryIdx]) + : null; + + const named_outcomes = (market.outcomes || []).map((label, idx) => ({ + label: String(label), + price: Number.isFinite(market.outcome_prices[idx]) ? round2(market.outcome_prices[idx]) : null + })); + + return { + slug: market.slug, + condition_id: market.condition_id, + title: market.title, + group_item_title: market.group_item_title, + sports_market_type: market.sports_market_type, + outcomes: market.outcomes || [], + named_outcomes, + yes, + volume_24h_usd: round2(market.volume_24hr), + best_bid: market.best_bid, + best_ask: market.best_ask, + spread: market.spread !== null ? round2(market.spread) : null, + active: market.active, + closed: market.closed, + is_primary: Boolean(isPrimary) + }; +} + +function scoreTradability(market, matrixInfo, externalAnchors) { + const reasons = []; + if (market.closed || !market.active) { + return { tradability: 'weak', tradability_reasons: ['market_closed_or_inactive'] }; + } + if (!market.outcome_prices.length || market.outcome_prices.every((p) => !Number.isFinite(p))) { + return { tradability: 'weak', tradability_reasons: ['missing_outcome_prices'] }; + } + + let level = 'medium'; + if (market.volume_24hr < 1_000) { + level = 'weak'; + reasons.push('volume_24h_below_1000'); + } else if (market.volume_24hr < 5_000) { + level = 'low'; + reasons.push('volume_24h_below_5000'); + } else if (market.volume_24hr >= 50_000 && (market.spread === null || market.spread <= 0.03)) { + level = 'high'; + reasons.push('high_volume_tight_spread'); + } else { + reasons.push('adequate_liquidity'); + } + + if (market.spread !== null && market.spread > 0.06) { + level = downgrade(level); + reasons.push('wide_spread'); + } + + if (matrixInfo.matrix_status === 'incomplete') { + level = capAt(level, 'medium'); + reasons.push('matrix_incomplete'); + } + + const conflict = externalAnchors.some((a) => a?.status === 'conflict' || a?.agreement === 'disagree'); + if (conflict) { + level = capAt(level, 'medium'); + reasons.push('external_anchor_conflict'); + } + + const unavailableMacro = externalAnchors.some( + (a) => a?.id === 'cme_fedwatch_style' && a?.status === 'unavailable' + ); + if (unavailableMacro && detectCategory(market, null) === 'macro_fed') { + level = capAt(level, 'medium'); + reasons.push('macro_anchor_unavailable'); + } + + return { tradability: level, tradability_reasons: reasons }; +} + +function downgrade(level) { + if (level === 'high') return 'medium'; + if (level === 'medium') return 'low'; + if (level === 'low') return 'weak'; + return 'weak'; +} + +function capAt(level, maxLevel) { + const order = ['weak', 'low', 'medium', 'high']; + return order[Math.min(order.indexOf(level), order.indexOf(maxLevel))]; +} + +function buildImpliedView(market, primaryOutcome, primaryPrice, matrixInfo) { + if (primaryPrice === null) return 'Implied probabilities unavailable.'; + const pct = Math.round(primaryPrice * 100); + let view = `Market implies ~${pct}% on "${primaryOutcome}".`; + if (market.one_day_price_change !== null) { + const pts = round2(market.one_day_price_change * 100); + view += ` 24h change: ${pts >= 0 ? '+' : ''}${pts} pts on primary outcome.`; + } + if (matrixInfo.related_market_count > 1) { + const tops = matrixInfo.event_matrix + .filter((row) => row.yes !== null) + .slice() + .sort((a, b) => b.yes - a.yes) + .slice(0, 3) + .map((row) => `${row.group_item_title || row.title}: ${Math.round(row.yes * 100)}%`) + .join('; '); + if (tops) view += ` Same-event top brackets — ${tops}.`; + } + return view; +} + +function buildPricedInNotes(market, primaryPrice, matrixInfo, externalAnchors) { + const already = []; + const may_not = []; + + if (primaryPrice !== null) { + if (primaryPrice >= 0.85) { + already.push('High consensus — outcome treated as likely by this market.'); + } else if (primaryPrice <= 0.15) { + already.push('Low base rate — outcome treated as unlikely by this market.'); + } else if (primaryPrice > 0.35 && primaryPrice < 0.65) { + may_not.push('Mid-range price on primary — meaningful room for news to move odds.'); + } else if (primaryPrice >= 0.65) { + already.push('Primary outcome is majority-priced (>65%); residual mass sits in other brackets or No.'); + } else { + already.push('Primary outcome is minority-priced (<35%); upside is in a re-rating, not a consensus hold.'); + } + } + + if (market.one_day_price_change !== null && Math.abs(market.one_day_price_change) >= 0.03) { + already.push('Recent 24h probability move may reflect fresh public information.'); + } else { + may_not.push('Quiet 24h move — stale narrative risk if you rely on old headlines.'); + } + + if (market.volume_24hr < 5_000) { + may_not.push('Thin liquidity — price may not reflect full information set.'); + } + + if (matrixInfo.related_market_count > 1) { + already.push(`Same-event matrix has ${matrixInfo.related_market_count} markets (see event_matrix).`); + } + + for (const anchor of externalAnchors) { + if (anchor?.status === 'ok' && typeof anchor.hold_prob === 'number' && primaryPrice !== null) { + const gap = round2(primaryPrice - anchor.hold_prob); + if (Math.abs(gap) >= 0.08) { + may_not.push( + `Cross-venue gap vs ${anchor.id}: Polymarket primary ${round2(primaryPrice)} vs anchor hold ${round2(anchor.hold_prob)} (Δ ${gap >= 0 ? '+' : ''}${gap}).` + ); + } else { + already.push(`Roughly aligned with ${anchor.id} hold≈${round2(anchor.hold_prob)}.`); + } + } + if (anchor?.status === 'unavailable') { + may_not.push(`${anchor.id} unavailable — ${anchor.detail || 'no live fetch'}.`); + } + } + + return { + already: already.length ? already.join(' ') : 'No strong priced-in flags from metadata alone.', + may_not: may_not.length ? may_not.join(' ') : 'External lineup/news not fully scanned by this endpoint.' + }; +} + +function buildUncertainties(market, matrixInfo, externalAnchors) { + const items = []; + if (market.end_date) items.push(`Resolution timing: ${market.end_date}`); + if (market.one_day_price_change === null) { + items.push('24h price change missing on Gamma — quiet or illiquid market.'); + } + if (market.spread !== null && market.spread > 0.04) { + items.push(`Wide spread (~${round2(market.spread)}) — execution uncertainty.`); + } + if (matrixInfo.matrix_status === 'incomplete') { + items.push(`Matrix incomplete: ${(matrixInfo.missing_market_groups || []).join(', ') || 'unknown'}.`); + } + if (externalAnchors.some((a) => a?.agreement === 'disagree')) { + items.push('External anchor disagrees with Polymarket primary — do not treat tradability as high.'); + } + items.push('Category plugins (lineup/weather/Musk ladder) not applied in L0 core_only depth.'); + return items; +} + +function detectCategory(market, eventBundle) { + const blob = [ + market?.title, + market?.slug, + market?.group_item_title, + eventBundle?.title, + eventBundle?.slug, + eventBundle?.description + ].filter(Boolean).join(' ').toLowerCase(); + + // Esports first — short tokens like "atp" must not steal CS/Dota titles (e.g. "Atputies"). + if (/\bcounter[-\s]?strike\b|\bcs:?go\b|\bcs2\b|\bdota\b|\bleague of legends\b|\bvalorant\b|\besports?\b|\bmap\s*\d\b/.test(blob)) { + return 'generic'; + } + + if (/\bfed\b|\bfomc\b|federal funds|interest rates after the .+ meeting|\bbps\b/.test(blob)) { + return 'macro_fed'; + } + if (/\btennis\b|\batp\b|\bwta\b|\bwimbledon\b/.test(blob)) return 'tennis'; + if (/\bnba\b|\bbasketball\b|\bwnba\b/.test(blob)) return 'nba'; + if (/\bnfl\b|super bowl|american football/.test(blob)) return 'nfl'; + if (/\bufc\b|\bmma\b|bellator|fight night/.test(blob)) return 'ufc'; + if (/\bmlb\b|\bbaseball\b|world series/.test(blob)) return 'mlb'; + if (/\bfootball\b|\bsoccer\b|\bfifa\b|\bfifwc\b|world.?cup|\bpremier league\b|\buefa\b|\bepl\b|\bucl\b|\bla liga\b|\bserie a\b|\bbundesliga\b|\bmls\b/.test(blob)) { + return 'football'; + } + if (/\btemperature\b|\bweather\b|°f|°c|\bhigh temp\b/.test(blob)) return 'weather'; + if (/musk|elon.*tweet|tweets in|# tweets/.test(blob)) return 'musk'; + if (/\bpresident\b|\belection\b|\bnominee\b|\bprimary\b|\bsenate\b|\bgovernor\b|\bparliament\b|prime minister|\belectoral\b/.test(blob)) { + return 'politics'; + } + return 'generic'; +} + +/** + * External anchors: v1 only attempts macro_fed style comparison when the market + * looks like a Fed hold bracket. Live CME HTML is not scraped (fragile); callers + * may inject anchors via options.externalAnchors. Without injection we emit an + * explicit unavailable stub so tradability stays honest. + */ +async function resolveExternalAnchors(market, eventBundle, fetchImpl) { + const category = detectCategory(market, eventBundle); + if (category !== 'macro_fed') return []; + + const title = `${market.title} ${market.group_item_title || ''}`; + const isHoldBracket = FED_HOLD_HINTS.some((re) => re.test(title)) + || /no change/i.test(market.group_item_title || ''); + + if (!isHoldBracket) { + return [{ + id: 'cme_fedwatch_style', + status: 'skipped', + detail: 'Not a hold/no-change bracket — inject anchors or use full FOMC matrix comparison later.', + hold_prob: null, + agreement: null + }]; + } + + // Optional: allow env/test injection only path for live numbers. + // Attempt a best-effort public JSON if LEO_FEDWATCH_JSON_URL is set. + const url = typeof process !== 'undefined' ? process.env?.LEO_FEDWATCH_JSON_URL : null; + if (url) { + try { + const payload = await fetchJsonLoose(fetchImpl, url); + const hold = toNumber(payload?.hold_prob ?? payload?.hold ?? payload?.no_change); + if (hold > 0 && hold <= 1) { + const yesIdx = market.outcomes.findIndex((o) => String(o).toLowerCase() === 'yes'); + const pm = yesIdx >= 0 ? market.outcome_prices[yesIdx] : market.outcome_prices[0]; + const gap = Number.isFinite(pm) ? Math.abs(pm - hold) : null; + return [{ + id: 'cme_fedwatch_style', + status: 'ok', + source_url: url, + hold_prob: round2(hold), + as_of: payload?.as_of ?? null, + agreement: gap !== null && gap >= 0.08 ? 'disagree' : 'agree', + detail: payload?.detail ?? null + }]; + } + } catch { + // fall through to unavailable + } + } + + return [{ + id: 'cme_fedwatch_style', + status: 'unavailable', + hold_prob: null, + agreement: null, + detail: 'No LEO_FEDWATCH_JSON_URL configured; set a JSON {hold_prob, as_of} feed or pass options.externalAnchors for live comparison.', + suggested_manual_check: [ + 'https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html', + 'https://www.investing.com/central-banks/fed-rate-monitor' + ] + }]; +} + +async function fetchJsonLoose(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 8000); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) throw new Error(`anchor ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-to-copilot-research-unit.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-to-copilot-research-unit.mjs new file mode 100644 index 00000000..b767e7bc --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-event-to-copilot-research-unit.mjs @@ -0,0 +1,247 @@ +/** + * Project agent-facing PM Event Readout (schema 0.2) onto + * Copilot Research Unit (copilot.read-model.v0.1). + * + * Pure / sync. No network. Never invents BET eligibility from L0 tradability. + * Contract: research/2026-07-12-pm-event-analyst-dual-surface-contract.md + */ + +export const COPILOT_SCHEMA_ID = 'copilot.read-model.v0.1'; +export const COPILOT_SCHEMA_VERSION = '0.1.0'; +export const AGENT_EXTENSION_KEY = 'pm-event-readout.v0.2'; + +const FORBIDDEN_ROOT_KEYS = new Set([ + 'city', + 'cities', + 'temperature', + 'tempF', + 'tempC', + 'weather', + 'bucketId', + 'buckets', + 'metar', + 'stationId', + 'articleSlug', + 'blogPath', + 'contentTaxonomy', + 'seoTitle', + 'internalLinks', + 'order', + 'orders', + 'positionSize', + 'wallet', + 'privateKey', + 'apiKey', + 'copyTrade' +]); + +/** + * @param {object} agentPayload — assessPmEventReadoutLive result (or fixture) + * @param {{ unitId: string }} options + * @returns {{ ok: true, value: object } | { ok: false, errors: string[] }} + */ +export function toCopilotResearchUnit(agentPayload, options = {}) { + const errors = []; + if (!agentPayload || typeof agentPayload !== 'object' || Array.isArray(agentPayload)) { + return { ok: false, errors: ['agentPayload must be a non-null object'] }; + } + const unitId = typeof options.unitId === 'string' ? options.unitId.trim() : ''; + if (!unitId) { + return { ok: false, errors: ['options.unitId is required and non-empty'] }; + } + + const asOf = + typeof agentPayload.generated_at === 'string' && agentPayload.generated_at + ? agentPayload.generated_at + : null; + if (!asOf) errors.push('generated_at missing'); + + const marketId = + pickString(agentPayload.market) || + pickString(agentPayload.input?.slug) || + pickString(agentPayload.input?.condition_id) || + ''; + if (!marketId) errors.push('market identity missing (market / input.slug / condition_id)'); + + const matrixStatus = agentPayload.matrix_status; + const freshnessStatus = mapFreshnessStatus(agentPayload); + const analysisAllowed = mapAnalysisAllowed(agentPayload.hard_gate); + const decision = mapDecision(agentPayload, freshnessStatus, analysisAllowed); + + if (freshnessStatus === 'missing' && decision !== null) { + errors.push('decision must be null when freshness.status=missing'); + } + if (!analysisAllowed && decision !== null) { + errors.push('decision must be null when analysisAllowed=false'); + } + if (decision?.eligibility === 'BET') { + errors.push('L0 projection must never emit eligibility=BET'); + } + + if (errors.length) return { ok: false, errors }; + + const partialReasons = + freshnessStatus === 'partial' + ? uniqueStrings([ + ...(Array.isArray(agentPayload.tradability_reasons) ? agentPayload.tradability_reasons : []), + matrixStatus === 'incomplete' ? 'matrix_incomplete' : null, + 'partial_surface' + ]) + : undefined; + + const evidence = buildEvidence(agentPayload); + const summary = buildSummary(agentPayload); + + const value = { + schemaId: COPILOT_SCHEMA_ID, + schemaVersion: COPILOT_SCHEMA_VERSION, + unitId, + market: { + platform: 'polymarket', + marketId, + eventId: pickString(agentPayload.event_slug) || undefined, + slug: pickString(agentPayload.market) || pickString(agentPayload.input?.slug) || undefined, + title: pickString(agentPayload.market_title) || pickString(agentPayload.event) || undefined, + url: pickString(agentPayload.input?.market_url) || undefined + }, + freshness: { + status: freshnessStatus, + asOf, + ...(partialReasons?.length ? { partialReasons } : {}) + }, + evidence, + compliance: { + analysisAllowed, + viewMode: 'summary', + reason: analysisAllowed ? undefined : `hard_gate:${String(agentPayload.hard_gate)}`, + policyVersion: 'pm-event-analyst-dual-surface-v0.1' + }, + decision, + ...(summary ? { summary } : {}), + extensions: { + [AGENT_EXTENSION_KEY]: agentPayload + } + }; + + const forbidden = Object.keys(value).filter((k) => FORBIDDEN_ROOT_KEYS.has(k)); + if (forbidden.length) { + return { ok: false, errors: [`forbidden root keys: ${forbidden.join(',')}`] }; + } + + return { ok: true, value }; +} + +function mapFreshnessStatus(payload) { + const matrix = payload.matrix_status; + const prices = payload.current_price; + const hasPrice = + prices && + typeof prices === 'object' && + Object.values(prices).some((v) => typeof v === 'number' && Number.isFinite(v)); + + if (!hasPrice && (!Array.isArray(payload.event_matrix) || payload.event_matrix.length === 0)) { + return 'missing'; + } + if (matrix === 'complete') return 'complete'; + if (matrix === 'incomplete' || payload.tradability === 'weak') return 'partial'; + if (matrix == null && hasPrice) return 'partial'; + return hasPrice ? 'complete' : 'missing'; +} + +function mapAnalysisAllowed(hardGate) { + if (hardGate == null || hardGate === 'no_orders_no_account_mutation') return true; + return false; +} + +function mapDecision(payload, freshnessStatus, analysisAllowed) { + if (!analysisAllowed || freshnessStatus === 'missing') return null; + + const t = payload.tradability; + if (t === 'high') { + return { + eligibility: 'OBSERVE', + coverage_score: clamp01(evidenceCoverage(payload)), + liquidity_score: clamp01(evidenceLiquidity(payload)), + rules_clarity: 'MED', + confidence: 0.7, + edge: null + }; + } + if (t === 'medium') { + return { + eligibility: 'OBSERVE', + coverage_score: clamp01(evidenceCoverage(payload)), + liquidity_score: clamp01(evidenceLiquidity(payload)), + rules_clarity: 'MED', + confidence: 0.5, + edge: null + }; + } + // weak | low | unknown — never BET from L0 + return { + eligibility: 'AVOID', + coverage_score: clamp01(evidenceCoverage(payload)), + liquidity_score: clamp01(evidenceLiquidity(payload)), + rules_clarity: freshnessStatus === 'partial' ? 'LOW' : 'UNKNOWN', + confidence: 0.3, + edge: null + }; +} + +function buildEvidence(payload) { + const sources = Array.isArray(payload.sources_read) ? payload.sources_read : []; + const matrix = Array.isArray(payload.event_matrix) ? payload.event_matrix : []; + const priceObj = payload.current_price; + const hasCurrentPrice = + priceObj && + typeof priceObj === 'object' && + Object.values(priceObj).some((v) => typeof v === 'number' && Number.isFinite(v)); + const hasPrices = + matrix.some( + (row) => + row && + ((typeof row.yes_price === 'number' && Number.isFinite(row.yes_price)) || + (typeof row.price === 'number' && Number.isFinite(row.price))) + ) || hasCurrentPrice; + return { + coverageScore: clamp01(evidenceCoverage(payload)), + sourcesCount: sources.length, + hasRules: Boolean(payload.fixture_status || payload.market_fixture_match), + hasPrices: Boolean(hasPrices), + hasLiquidity: evidenceLiquidity(payload) > 0 + }; +} + +function evidenceCoverage(payload) { + const matrix = Array.isArray(payload.event_matrix) ? payload.event_matrix : []; + if (payload.matrix_status === 'complete' && matrix.length >= 2) return 0.85; + if (matrix.length >= 1) return 0.55; + return 0.2; +} + +function evidenceLiquidity(payload) { + const matrix = Array.isArray(payload.event_matrix) ? payload.event_matrix : []; + const withLiq = matrix.filter((row) => typeof row?.liquidity === 'number' && row.liquidity > 0); + if (withLiq.length >= 2) return 0.7; + if (withLiq.length === 1) return 0.4; + return payload.tradability === 'high' ? 0.5 : 0.15; +} + +function buildSummary(payload) { + const parts = [pickString(payload.base_case), pickString(payload.market_implied_view)].filter(Boolean); + if (!parts.length) return undefined; + return { en: parts.join(' ') }; +} + +function pickString(v) { + return typeof v === 'string' && v.trim() ? v.trim() : null; +} + +function uniqueStrings(items) { + return [...new Set(items.filter((x) => typeof x === 'string' && x.trim()))]; +} + +function clamp01(n) { + if (typeof n !== 'number' || !Number.isFinite(n)) return 0; + return Math.max(0, Math.min(1, n)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-gamma-market.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-gamma-market.mjs new file mode 100644 index 00000000..e63ae515 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-gamma-market.mjs @@ -0,0 +1,302 @@ +// Shared Polymarket Gamma market fetch helpers for PM services. + +export const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +export const FETCH_TIMEOUT_MS = 8000; + +export function resolveMarketRef(input) { + const conditionId = String(input.condition_id ?? '').trim(); + const slug = String(input.slug ?? '').trim(); + const fromUrl = extractFromMarketUrl(input.market_url); + return { + condition_id: conditionId || fromUrl.condition_id || null, + slug: slug || fromUrl.slug || null + }; +} + +export function extractFromMarketUrl(marketUrl) { + const raw = String(marketUrl ?? '').trim(); + if (!raw) return { slug: null, condition_id: null }; + + const conditionMatch = raw.match(/\b(0x[a-fA-F0-9]{64})\b/); + if (conditionMatch) { + return { slug: null, condition_id: conditionMatch[1] }; + } + + try { + const url = new URL(raw); + const parts = url.pathname.split('/').filter(Boolean); + const idx = parts.findIndex((part) => part === 'event' || part === 'market'); + if (idx >= 0 && parts[idx + 1]) { + return { slug: decodeURIComponent(parts[idx + 1]), condition_id: null }; + } + } catch { + if (/^[a-z0-9-]+$/i.test(raw)) { + return { slug: raw, condition_id: null }; + } + } + + return { slug: null, condition_id: null }; +} + +export async function fetchMarket(fetchImpl, ref) { + if (ref.condition_id) { + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?condition_ids=${encodeURIComponent(ref.condition_id)}` + ); + const market = pickMarketRow(rows); + if (market) return normalizeGammaMarket(market); + } + + if (ref.slug) { + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?slug=${encodeURIComponent(ref.slug)}` + ); + const market = pickMarketRow(rows); + if (market) return normalizeGammaMarket(market); + } + + return null; +} + +function pickMarketRow(rows) { + const list = Array.isArray(rows) ? rows : []; + return list.find((row) => row?.conditionId) ?? null; +} + +export function normalizeGammaMarket(market) { + const outcomes = parseJsonArray(market.outcomes); + const outcome_prices = parseJsonArray(market.outcomePrices).map((value) => Number(value)); + const bestBid = toNumber(market.bestBid); + const bestAsk = toNumber(market.bestAsk); + const spread = bestAsk > 0 && bestBid > 0 ? Math.max(0, bestAsk - bestBid) : null; + const oneDay = Number(market.oneDayPriceChange); + const events = Array.isArray(market.events) + ? market.events.map((event) => ({ + id: event?.id ?? null, + slug: event?.slug ?? event?.ticker ?? null, + title: event?.title ?? null, + description: event?.description ?? null + })) + : []; + + return { + condition_id: market.conditionId, + slug: market.slug ?? market.conditionId, + title: market.question ?? market.slug ?? market.conditionId, + description: market.description ?? null, + group_item_title: market.groupItemTitle ?? null, + sports_market_type: market.sportsMarketType ?? null, + events, + active: market.active !== false, + closed: Boolean(market.closed), + volume_24hr: toNumber(market.volume24hr ?? market.volumeNum ?? market.volume), + volume_total: toNumber(market.volumeNum ?? market.volume), + outcomes, + outcome_prices, + best_bid: bestBid || null, + best_ask: bestAsk || null, + spread, + end_date: market.endDate ?? market.endDateIso ?? null, + start_time: market.eventStartTime ?? market.startDate ?? market.startDateIso ?? null, + one_day_price_change: Number.isFinite(oneDay) ? oneDay : null, + updated_at: market.updatedAt ?? null + }; +} + +/** Parent event slug from a normalized market (Gamma embeds events[] on market rows). */ +export function eventSlugFromMarket(market) { + const slug = market?.events?.[0]?.slug; + return typeof slug === 'string' && slug.trim() ? slug.trim() : null; +} + +/** Fetch a Gamma event by slug; returns { slug, title, markets[] } or null. */ +export async function fetchEventBySlug(fetchImpl, eventSlug) { + if (!eventSlug) return null; + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?slug=${encodeURIComponent(eventSlug)}` + ); + const list = Array.isArray(rows) ? rows : []; + const event = list.find((row) => row?.slug === eventSlug) ?? list[0] ?? null; + return normalizeEventRow(event, eventSlug); +} + +/** + * Fetch child events under a Gamma parent_event_id. + * Same discovery path as pm-manual-trading-lab event_market_matrix_monitor.py + * (snake_case parent_event_id — camelCase silently returns unrelated events). + */ +export async function fetchChildEventsByParentId(fetchImpl, parentEventId, limit = 100) { + if (parentEventId == null || parentEventId === '') return []; + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?parent_event_id=${encodeURIComponent(parentEventId)}&limit=${limit}` + ); + return Array.isArray(rows) ? rows : []; +} + +function normalizeEventRow(event, eventSlugFallback = null) { + if (!event) return null; + const markets = (Array.isArray(event.markets) ? event.markets : []) + .map((row) => normalizeGammaMarket(row)) + .filter((market) => market.condition_id); + return { + id: event.id ?? null, + slug: event.slug ?? eventSlugFallback, + title: event.title ?? event.slug ?? eventSlugFallback, + description: event.description ?? null, + start_time: event.startTime ?? event.startDate ?? null, + end_date: event.endDate ?? null, + parent_event_id: event.parentEventId ?? event.parent_event_id ?? null, + markets + }; +} + +/** + * Football/tennis Polymarket splits one match across parent + child events. + * Prefer Gamma parent_event_id children (skill-aligned). Fall back to known + * sibling slug heuristics when parent id is missing. + */ +export async function fetchEventBundleWithSiblings(fetchImpl, primaryEventSlug) { + let primary = await fetchEventBySlug(fetchImpl, primaryEventSlug); + if (!primary) return null; + + // If caller hit a child event (more-markets / halftime / …), climb to parent + // so sibling discovery matches the local matrix monitor. + if (primary.parent_event_id != null) { + try { + const parentRows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?id=${encodeURIComponent(primary.parent_event_id)}` + ); + const parentList = Array.isArray(parentRows) ? parentRows : []; + const parentRaw = parentList[0] ?? null; + const parent = normalizeEventRow(parentRaw); + if (parent?.markets?.length) { + primary = { + ...parent, + // keep the caller's slug as entrypoint evidence + entry_event_slug: primary.slug + }; + } + } catch { + // stay on child primary + } + } + + const siblingBundles = []; + const seenSlugs = new Set([primary.slug].filter(Boolean)); + + if (primary.id != null) { + try { + const children = await fetchChildEventsByParentId(fetchImpl, primary.id); + for (const child of children) { + const bundle = normalizeEventRow(child); + if (!bundle?.slug || seenSlugs.has(bundle.slug) || !bundle.markets?.length) continue; + seenSlugs.add(bundle.slug); + siblingBundles.push(bundle); + } + } catch { + // parent_event_id path optional; fall through to heuristics + } + } + + if (!siblingBundles.length) { + const base = String(primaryEventSlug) + .replace(/-more-markets$/, '') + .replace(/-halftime-result$/, '') + .replace(/-second-half-result$/, '') + .replace(/-exact-score$/, '') + .replace(/-player-props$/, '') + .replace(/-total-corners$/, '') + .replace(/-first-to-score$/, ''); + const heuristicSlugs = [ + `${base}-more-markets`, + `${base}-team-to-advance`, + `${base}-exact-score`, + `${base}-halftime-result`, + `${base}-second-half-result`, + `${base}-first-to-score`, + `${base}-total-corners`, + `${base}-player-props` + ].filter((slug) => slug !== primaryEventSlug && !seenSlugs.has(slug)); + + for (const slug of heuristicSlugs) { + try { + const bundle = await fetchEventBySlug(fetchImpl, slug); + if (bundle?.markets?.length) { + seenSlugs.add(bundle.slug); + siblingBundles.push(bundle); + } + } catch { + // sibling optional + } + } + } + + const byCondition = new Map(); + for (const market of primary.markets) { + byCondition.set(market.condition_id, market); + } + for (const bundle of siblingBundles) { + for (const market of bundle.markets) { + if (!byCondition.has(market.condition_id)) { + byCondition.set(market.condition_id, market); + } + } + } + + return { + ...primary, + markets: [...byCondition.values()], + sibling_event_slugs: siblingBundles.map((b) => b.slug), + linked_event_count: 1 + siblingBundles.length, + discovery: siblingBundles.length && primary.id != null + ? 'parent_event_id' + : (siblingBundles.length ? 'heuristic_slugs' : 'primary_only'), + primary_event_slug: primary.slug + }; +} + +export function parseJsonArray(value) { + if (Array.isArray(value)) return value; + if (typeof value !== 'string' || !value.trim()) return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) { + throw new Error(`Upstream ${response.status} for ${url}`); + } + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +export function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +export function round2(value) { + return Math.round(value * 100) / 100; +} + +export function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-health.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-health.mjs new file mode 100644 index 00000000..7bbc444a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-health.mjs @@ -0,0 +1,200 @@ +// PM Market Health — spread / depth / overround for one market or event. +// Lineage: polymarket-toolkit markets surface + prediction-trader overround idea (read-only). +// No keys, no orders. + +import { resolveMarketRef } from './pm-gamma-market.mjs'; + +const SERVICE_ID = 'pm_market_health'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; + +const STANDARD_CAVEATS = [ + 'Read-only book/overround snapshot. Not a buy tip.', + 'Overround = sum of outcome yes-prices; >1 means vig/overlap, <1 may mean incomplete quotes.', + 'No orders, no wallet custody.', + 'OSS lineage: polymarket-toolkit markets + public Gamma fields.' +]; + +/** + * @param {object} input + * @param {string} [input.market_url] + * @param {string} [input.slug] + * @param {string} [input.condition_id] + * @param {string} [input.event_slug] + */ +export async function assessPmMarketHealthLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const ref = resolveMarketRef(input); + const eventSlug = String(input.event_slug ?? input.eventSlug ?? '').trim(); + + if (!ref.slug && !ref.condition_id && !eventSlug) { + throw new Error('pm-market-health requires market_url, slug, condition_id, or event_slug'); + } + + let markets = []; + let event_title = null; + let resolved_via = null; + + if (eventSlug) { + const events = await fetchJson(fetchImpl, `${GAMMA_BASE}/events?slug=${encodeURIComponent(eventSlug)}`).catch(() => []); + const event = Array.isArray(events) ? events[0] : null; + if (!event) throw new Error(`No Gamma event for slug: ${eventSlug}`); + event_title = event.title ?? eventSlug; + markets = event.markets || []; + resolved_via = 'event_slug'; + } else if (ref.slug) { + const rows = await fetchJson(fetchImpl, `${GAMMA_BASE}/markets?slug=${encodeURIComponent(ref.slug)}`).catch(() => []); + markets = Array.isArray(rows) ? rows : []; + resolved_via = 'market_slug'; + event_title = markets[0]?.events?.[0]?.title ?? null; + } else { + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?condition_ids=${encodeURIComponent(ref.condition_id)}` + ).catch(() => []); + markets = Array.isArray(rows) ? rows : []; + resolved_via = 'condition_id'; + } + + if (!markets.length) throw new Error('No markets found for health check'); + + const assessed = markets.slice(0, 40).map(assessOneMarket); + const primary = assessed[0]; + const overround_event = round4(assessed.reduce((sum, m) => sum + (m.yes_price ?? 0), 0)); + const verdict = classifyHealth(primary, assessed.length > 1 ? overround_event : primary.overround); + + const generated_at = new Date().toISOString(); + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at, + input: { + market_url: input.market_url ?? null, + slug: ref.slug, + condition_id: ref.condition_id, + event_slug: eventSlug || null, + resolved_via + }, + event_title, + primary, + related_markets: assessed.slice(0, 12), + event_yes_sum: assessed.length > 1 ? overround_event : null, + health_verdict: verdict, + buyer_summary_zh: buildZh(primary, verdict, assessed.length), + buyer_summary_en: buildEn(primary, verdict, assessed.length), + value_loop: { + why_pay_again: 'Spread and overround move with the book; re-check before size.', + stale_after_minutes: 5, + paid_value_tier: 'toolkit_market_health', + oss_lineage: 'polymarket-toolkit markets · overround identity', + llm_api_key_required: false + }, + hard_gate: 'no_orders_no_signing', + caveats: STANDARD_CAVEATS, + next_gate: verdict === 'avoid_thin_or_incoherent' + ? 'Do_not_size_until_book_improves' + : 'Optional_pm_trade_preflight_or_decision_card', + source: { provider: 'polymarket_gamma_public_api', method: 'market_health_overround' } + }; +} + +export function buildPmMarketHealthFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + health_verdict: 'unknown', + buyer_summary_zh: '盘口健康回退:上游不可用。', + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + source: { provider: 'static_fallback' }, + input + }; +} + +function assessOneMarket(m) { + const prices = parsePrices(m.outcomePrices); + const yes = prices[0] ?? null; + const no = prices[1] ?? null; + const overround = yes != null && no != null ? round4(yes + no) : null; + const spread = numOrNull(m.spread); + const bestBid = numOrNull(m.bestBid); + const bestAsk = numOrNull(m.bestAsk); + const volume24hr = toNumber(m.volume24hr ?? m.volumeNum); + const liquidity = toNumber(m.liquidityNum ?? m.liquidity); + + return { + slug: m.slug ?? null, + question: m.question ?? m.groupItemTitle ?? null, + yes_price: yes, + no_price: no, + overround, + spread, + best_bid: bestBid, + best_ask: bestAsk, + volume_24h_usd: volume24hr, + liquidity_usd: liquidity, + active: m.active !== false && m.closed !== true, + accepting_orders: m.acceptingOrders !== false + }; +} + +function classifyHealth(primary, overround) { + if (!primary?.active) return 'closed_or_inactive'; + if (primary.spread != null && primary.spread > 0.08) return 'wide_spread'; + if (overround != null && (overround > 1.08 || overround < 0.92)) return 'avoid_thin_or_incoherent'; + if ((primary.volume_24h_usd || 0) < 500) return 'low_volume'; + if (primary.spread != null && primary.spread <= 0.03 && overround != null && overround >= 0.98 && overround <= 1.05) { + return 'ok_tight'; + } + return 'ok_usable'; +} + +function buildZh(primary, verdict, n) { + return `盘口健康:verdict=${verdict};yes≈${primary?.yes_price ?? 'n/a'} no≈${primary?.no_price ?? 'n/a'} overround=${primary?.overround ?? 'n/a'} spread=${primary?.spread ?? 'n/a'};关联盘 ${n}。非买点。`; +} + +function buildEn(primary, verdict, n) { + return `Market health: verdict=${verdict}; yes≈${primary?.yes_price ?? 'n/a'} overround=${primary?.overround ?? 'n/a'} spread=${primary?.spread ?? 'n/a'}; related=${n}. Not a buy tip.`; +} + +function parsePrices(raw) { + if (Array.isArray(raw)) return raw.map((x) => Number(x)).filter((n) => Number.isFinite(n)); + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) return parsed.map((x) => Number(x)).filter((n) => Number.isFinite(n)); + } catch { + return []; + } + } + return []; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function toNumber(value) { + const n = Number(value ?? 0); + return Number.isFinite(n) ? n : 0; +} + +function numOrNull(value) { + if (value == null || value === '') return null; + const n = Number(value); + return Number.isFinite(n) ? n : null; +} + +function round4(value) { + return Math.round(value * 10000) / 10000; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-scan.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-scan.mjs new file mode 100644 index 00000000..5644a0b3 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-market-scan.mjs @@ -0,0 +1,146 @@ +// PM Market Scan — productizes polymarket-toolkit `pm scan` (volume + spread). +// Read-only Gamma. No keys, no orders. + +const SERVICE_ID = 'pm_market_scan'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; + +const STANDARD_CAVEATS = [ + 'Read-only Gamma scanner from polymarket-toolkit lineage (pm scan).', + 'Spread/volume are platform fields — not a trading signal.', + 'No orders, no wallet custody.' +]; + +/** + * @param {object} input + * @param {number} [input.limit=10] + * @param {number} [input.min_volume=1000] + * @param {string} [input.query] + */ +export async function assessPmMarketScanLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInt(input.limit, 1, 30, 10); + const minVolume = Number(input.min_volume ?? input.minVolume ?? 1000) || 1000; + const query = String(input.query ?? input.q ?? '').trim(); + + let markets = []; + if (query) { + const search = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(query)}&events_status=active&limit_per_type=20` + ).catch(() => ({})); + for (const event of search?.events || []) { + for (const m of event.markets || []) { + markets.push({ ...m, _event_title: event.title }); + } + } + } else { + markets = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?active=true&closed=false&limit=80&order=volume24hr&ascending=false` + ).catch(() => []); + } + + const rows = rankMarketsForScan(Array.isArray(markets) ? markets : [], { minVolume24hr: minVolume, limit }); + const generated_at = new Date().toISOString(); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at, + input: { limit, min_volume: minVolume, query: query || null }, + markets: rows, + market_count: rows.length, + buyer_summary_zh: rows.length + ? `市场扫描:${rows.length} 个活跃盘(min_vol=${minVolume})。头名 ${rows[0].slug} · 24h≈$${Math.round(rows[0].volume24hr)} · spread=${fmtSpread(rows[0].spread)}。来自 polymarket-toolkit pm scan。` + : '市场扫描:无满足成交量门槛的活跃盘。', + buyer_summary_en: rows.length + ? `Market scan: ${rows.length} active markets (min_vol=${minVolume}). Top ${rows[0].slug}.` + : 'Market scan: no active markets above volume floor.', + value_loop: { + why_pay_again: '24h volume and spreads move; re-scan before picking a book.', + stale_after_minutes: 10, + paid_value_tier: 'toolkit_scanner', + oss_lineage: 'polymarket-toolkit src/scanner.ts · pm scan', + llm_api_key_required: false + }, + caveats: STANDARD_CAVEATS, + next_gate: 'Pick a slug → /pm-market-health or /pm-trade-preflight', + source: { provider: 'polymarket_gamma_public_api', method: 'toolkit_pm_scan' } + }; +} + +export function buildPmMarketScanFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + markets: [], + market_count: 0, + buyer_summary_zh: '市场扫描回退:上游不可用。', + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + source: { provider: 'static_fallback' }, + input + }; +} + +/** Pure ranker — ported from polymarket-toolkit/src/scanner.ts */ +export function rankMarketsForScan(markets, options = {}) { + const minVol = options.minVolume24hr ?? 0; + const limit = options.limit ?? 20; + const rows = []; + for (const m of markets) { + if (m.closed === true || m.active === false) continue; + const volume24hr = toNumber(m.volume24hrClob ?? m.volume24hr ?? m.volumeNum); + if (volume24hr < minVol) continue; + rows.push({ + slug: m.slug ?? 'unknown', + question: String(m.question ?? m.slug ?? 'unknown').slice(0, 120), + volume24hr, + spread: numOrNull(m.spread), + liquidity: toNumber(m.liquidityNum ?? m.liquidity), + best_bid: numOrNull(m.bestBid), + best_ask: numOrNull(m.bestAsk), + accepting_orders: m.acceptingOrders !== false, + event_title: m._event_title ?? null + }); + } + rows.sort((a, b) => b.volume24hr - a.volume24hr || (a.spread ?? 999) - (b.spread ?? 999)); + return rows.slice(0, limit); +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} + +function toNumber(value) { + const n = Number(value ?? 0); + return Number.isFinite(n) ? n : 0; +} + +function numOrNull(value) { + if (value == null || value === '') return null; + const n = Number(value); + return Number.isFinite(n) ? n : null; +} + +function fmtSpread(spread) { + if (spread == null) return 'n/a'; + return `${(spread * 100).toFixed(2)}%`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-pnl-audit.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-pnl-audit.mjs new file mode 100644 index 00000000..c9f8c836 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-pnl-audit.mjs @@ -0,0 +1,693 @@ +// PM PnL Audit — quick trust gate + Worker-safe full cashflow replay. +// Ports polymarket-toolkit / polymarket-pnl fee-inclusive cashflow method. +// Read-only public APIs only. No wallet custody, signing, or orders. + +const SERVICE_ID = 'pm_pnl_audit'; +const LB_BASE = 'https://lb-api.polymarket.com'; +const DATA_BASE = 'https://data-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; +const EVM = /^0x[a-fA-F0-9]{40}$/; +const AUDIT_PASS_DELTA_USD = 10; +const PAGE_LIMIT = 500; +const ACTIVITY_OFFSET_CAP = 9500; +const POSITIONS_OFFSET_CAP = 9500; +/** Wall-clock budget for full pagination inside Worker. */ +const FULL_BUDGET_MS = 9000; +const FULL_MAX_PAGES_PER_TYPE = 24; + +const ACTIVITY_TYPES_FULL = [ + 'TRADE', + 'REDEEM', + 'MERGE', + 'SPLIT', + 'MAKER_REBATE', + 'REWARD', + 'REFERRAL_REWARD', + 'CONVERSION' +]; + +const STANDARD_CAVEATS = [ + 'Quick mode compares LB all-time profit with position-level cashPnL and activity first-page hints.', + 'Full mode paginates Data API /activity cashflows under a Worker time budget; pagination_incomplete means audit is not complete.', + 'Position cashPnL is approximate and can diverge from fee-inclusive wallet truth due to fees, rebates, redemptions, merges/splits, and rounding.', + 'Read-only public APIs only: no wallet custody, no signing, no order routing.' +]; + +/** + * @param {object} input + * @param {string} [input.address] + * @param {string} [input.wallet] + * @param {string} [input.username] + * @param {string} [input.query] + * @param {'quick'|'full'} [input.mode='quick'] + * @param {number} [input.positions_limit=100] + */ +export async function assessPmPnlAuditLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const raw = String(input.address ?? input.wallet ?? input.username ?? input.query ?? '').trim(); + if (!raw) throw new Error('pm-pnl-audit requires address or username.'); + + const requestedMode = normalizeMode(input.mode); + const resolved = await resolveAddress(fetchImpl, raw); + if (!resolved.address) { + throw new Error( + `Could not resolve wallet for "${raw}" via leaderboard username search ` + + `(scanned ~top LB pages only). Pass a 0x proxy address for certainty.` + ); + } + + const limit = clampInt(input.positions_limit ?? input.limit, 20, 500, 100); + const deadline = Date.now() + (options.fullBudgetMs ?? FULL_BUDGET_MS); + + const [lbRows, positionsQuick] = await Promise.all([ + fetchJson(fetchImpl, `${LB_BASE}/profit?address=${resolved.address}&window=all`).catch(() => []), + fetchJson(fetchImpl, `${DATA_BASE}/positions?user=${resolved.address}&limit=${limit}&sizeThreshold=0`).catch(() => []) + ]); + + const leaderboard_profit = summarizeLeaderboard(lbRows, resolved.address); + const positions_cash_pnl = summarizePositions(positionsQuick, limit); + + let activity_hint = null; + let cashflow_replay = null; + + if (requestedMode === 'full') { + cashflow_replay = await runFullCashflowReplay(fetchImpl, resolved.address, { + deadline, + maxPagesPerType: options.fullMaxPagesPerType ?? FULL_MAX_PAGES_PER_TYPE + }); + activity_hint = { + trade_rows: cashflow_replay.counts?.trade_count ?? 0, + pagination: cashflow_replay.pagination_incomplete ? 'incomplete_or_budget_capped' : 'paged', + caveat: cashflow_replay.complete + ? 'Full cashflow replay finished within Worker budget.' + : 'Full replay marked incomplete — do not treat as audit-grade truth.' + }; + } else { + const [tradePage, rebatePage] = await Promise.all([ + fetchJson(fetchImpl, `${DATA_BASE}/activity?user=${resolved.address}&type=TRADE&limit=500`).catch(() => []), + fetchJson(fetchImpl, `${DATA_BASE}/activity?user=${resolved.address}&type=MAKER_REBATE&limit=200`).catch(() => []) + ]); + activity_hint = { + trade_rows_first_page: Array.isArray(tradePage) ? tradePage.length : 0, + maker_rebate_rows_first_page: Array.isArray(rebatePage) ? rebatePage.length : 0, + pagination: 'first_page_only', + caveat: 'Quick mode does not page through full activity history.' + }; + } + + const divergence = requestedMode === 'full' && cashflow_replay + ? computeReplayDivergence(leaderboard_profit, cashflow_replay) + : computeDivergence(leaderboard_profit, positions_cash_pnl); + + const action = chooseAction(divergence, { + positions_truncated: positions_cash_pnl.positions_truncated, + requestedMode, + replayComplete: cashflow_replay?.complete === true + }); + + const generated_at = new Date().toISOString(); + const stale_after_minutes = requestedMode === 'full' ? 30 : 15; + const stale_at = new Date(Date.parse(generated_at) + stale_after_minutes * 60_000).toISOString(); + const confidence_gaps = [ + ...(leaderboard_profit.amount_usd === null ? ['leaderboard_profit_missing'] : []), + ...(positions_cash_pnl.positions_sampled === 0 ? ['positions_cash_pnl_missing'] : []), + ...(positions_cash_pnl.positions_truncated ? ['positions_page_may_be_truncated'] : []), + ...(requestedMode === 'quick' + ? ['cashflow_replay_not_run_in_quick_mode', 'activity_first_page_only'] + : []), + ...(cashflow_replay && !cashflow_replay.complete + ? ['cashflow_replay_incomplete', ...(cashflow_replay.pagination_incomplete_types || []).map((t) => `pagination_${t}`)] + : []) + ]; + + return { + schema_version: '0.2', + service_id: SERVICE_ID, + mode: requestedMode === 'full' ? 'live_full' : 'live_quick', + generated_at, + input: { + query: raw, + address: resolved.address, + resolved_via: resolved.resolved_via, + mode: requestedMode, + positions_limit: limit + }, + layers: { + leaderboard_profit, + positions_cash_pnl, + cashflow_replay + }, + leaderboard_profit, + positions_cash_pnl, + activity_hint, + cashflow_replay, + divergence_verdict: divergence.verdict, + divergence, + action, + buyer_summary_zh: buildBuyerSummaryZh({ + resolved, leaderboard_profit, positions_cash_pnl, divergence, action, cashflow_replay, requestedMode + }), + buyer_summary_en: buildBuyerSummaryEn({ + resolved, leaderboard_profit, positions_cash_pnl, divergence, action, cashflow_replay, requestedMode + }), + value_loop: { + why_pay_again: 'Wallet PnL, open positions and rebates move; re-run before copying a trader or trusting a PnL claim.', + stale_after_minutes, + stale_at, + best_used_in: 'copy_trading_due_diligence_or_claim_verification', + not_a_subscription_to: 'live_wallet_alerts', + paid_value_tier: 'A_tier_audit', + fulfillment: requestedMode === 'full' + ? (cashflow_replay?.complete ? 'full_cashflow_replay_on_demand' : 'full_cashflow_replay_incomplete_honest') + : 'quick_audit_on_demand', + operator_always_online: false, + llm_api_key_required: false + }, + confidence_gaps, + caveats: [ + ...STANDARD_CAVEATS, + ...(requestedMode === 'full' && cashflow_replay && !cashflow_replay.complete + ? ['Full replay incomplete under Worker budget or API pagination boundary — verify manually or re-run offline polymarket-pnl.'] + : []) + ], + next_gate: requestedMode === 'full' + ? (cashflow_replay?.complete + ? 'If abs delta vs LB > $10, distrust headline claims for copy-trading.' + : 'Re-run full offline polymarket-pnl or shrink wallet history before relying on audit-grade PnL.') + : 'If divergence matters, call mode=full for cashflow replay.', + source: { + provider: 'polymarket_public_api', + oss_lineage: 'polymarket-toolkit fee-inclusive-pnl.md + pm pnl-check + polymarket-pnl skill', + layers: requestedMode === 'full' + ? ['lb-api /profit window=all', 'data-api /positions', 'data-api /activity cashflow replay'] + : ['lb-api /profit window=all', 'data-api /positions cashPnl', 'data-api /activity first page hints'], + audit_threshold_usd: AUDIT_PASS_DELTA_USD, + activity_types_full: ACTIVITY_TYPES_FULL + } + }; +} + +export function buildPmPnlAuditFallback(input = {}) { + const generated_at = new Date().toISOString(); + return { + schema_version: '0.2', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at, + input: { + query: input.address ?? input.wallet ?? input.username ?? input.query ?? null, + address: null, + resolved_via: null, + mode: normalizeMode(input.mode) + }, + layers: { + leaderboard_profit: { amount_usd: null, source: 'lb-api /profit window=all', row_found: false }, + positions_cash_pnl: { total_cash_pnl_usd: null, positions_sampled: 0, positions_truncated: false }, + cashflow_replay: null + }, + divergence_verdict: 'unknown', + action: 'verify_manually', + buyer_summary_zh: 'PnL 审计回退:实时公开 API 不可用,无法判断;请手动核对。', + buyer_summary_en: 'PnL audit fallback: live public APIs unavailable; verify manually.', + confidence_gaps: ['live_data_unavailable'], + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Retry live quick or full audit.', + source: { provider: 'static_fallback' } + }; +} + +async function runFullCashflowReplay(fetchImpl, address, { deadline, maxPagesPerType }) { + const incompleteTypes = []; + const budgetHitTypes = []; + const counts = { + buy_count: 0, + sell_count: 0, + trade_count: 0, + redeem_count: 0, + merge_count: 0, + split_count: 0, + rebate_count: 0, + reward_count: 0, + referral_count: 0, + conversion_count: 0 + }; + + let total_buy = 0; + let total_sell = 0; + let total_redeem = 0; + let total_merge = 0; + let total_split = 0; + let total_rebate = 0; + let total_reward = 0; + let total_referral = 0; + let total_conversion = 0; + let pages_fetched = 0; + + for (const activityType of ACTIVITY_TYPES_FULL) { + if (Date.now() >= deadline) { + budgetHitTypes.push(activityType); + incompleteTypes.push(activityType); + // Remaining types not started + for (const rest of ACTIVITY_TYPES_FULL.slice(ACTIVITY_TYPES_FULL.indexOf(activityType) + 1)) { + if (!incompleteTypes.includes(rest)) incompleteTypes.push(rest); + if (!budgetHitTypes.includes(rest)) budgetHitTypes.push(rest); + } + break; + } + + const pageResult = activityType === 'TRADE' + ? await fetchActivityTimestamp(fetchImpl, address, activityType, { deadline, maxPagesPerType }) + : await fetchActivityOffset(fetchImpl, address, activityType, { deadline, maxPagesPerType }); + + pages_fetched += pageResult.pages; + if (pageResult.pagination_incomplete) incompleteTypes.push(activityType); + if (pageResult.budget_hit) budgetHitTypes.push(activityType); + + if (activityType === 'TRADE') { + for (const row of pageResult.items) { + const usdc = toNumber(row.usdcSize); + if (row.side === 'BUY') { + total_buy += usdc; + counts.buy_count += 1; + } else if (row.side === 'SELL') { + total_sell += usdc; + counts.sell_count += 1; + } + counts.trade_count += 1; + } + } else if (activityType === 'REDEEM') { + total_redeem = sumUsdc(pageResult.items); + counts.redeem_count = pageResult.items.length; + } else if (activityType === 'MERGE') { + total_merge = sumUsdc(pageResult.items); + counts.merge_count = pageResult.items.length; + } else if (activityType === 'SPLIT') { + total_split = sumUsdc(pageResult.items); + counts.split_count = pageResult.items.length; + } else if (activityType === 'MAKER_REBATE') { + total_rebate = sumUsdc(pageResult.items); + counts.rebate_count = pageResult.items.length; + } else if (activityType === 'REWARD') { + total_reward = sumUsdc(pageResult.items); + counts.reward_count = pageResult.items.length; + } else if (activityType === 'REFERRAL_REWARD') { + total_referral = sumUsdc(pageResult.items); + counts.referral_count = pageResult.items.length; + } else if (activityType === 'CONVERSION') { + total_conversion = sumUsdc(pageResult.items); + counts.conversion_count = pageResult.items.length; + } + } + + const positionsResult = Date.now() < deadline + ? await fetchAllPositions(fetchImpl, address, { deadline }) + : { items: [], truncated: true, budget_hit: true }; + if (positionsResult.budget_hit && !budgetHitTypes.includes('POSITIONS')) { + budgetHitTypes.push('POSITIONS'); + } + + let unrealized = 0; + let open_positions = 0; + for (const p of positionsResult.items) { + const size = toNumber(p.size); + const curPrice = toNumber(p.curPrice); + if (size > 0) { + unrealized += size * curPrice; + open_positions += 1; + } + } + + const pnl_trading = round2( + total_sell + total_redeem + total_merge + total_rebate - total_buy - total_split + unrealized + ); + const pnl_inclusive = round2(pnl_trading + total_reward + total_referral + total_conversion); + const pagination_incomplete = incompleteTypes.length > 0 || positionsResult.truncated || budgetHitTypes.length > 0; + const complete = !pagination_incomplete; + + return { + status: complete ? 'complete' : 'incomplete', + complete, + pagination_incomplete, + pagination_incomplete_types: incompleteTypes.length ? incompleteTypes : null, + budget_hit_types: budgetHitTypes.length ? budgetHitTypes : null, + positions_truncated: positionsResult.truncated, + pages_fetched, + formula: 'SELL+REDEEM+MERGE+REBATE - BUY - SPLIT + unrealized; inclusive += REWARD+REFERRAL+CONVERSION', + totals: { + total_buy: round2(total_buy), + total_sell: round2(total_sell), + total_redeem: round2(total_redeem), + total_merge: round2(total_merge), + total_split: round2(total_split), + total_rebate: round2(total_rebate), + total_reward: round2(total_reward), + total_referral: round2(total_referral), + total_conversion: round2(total_conversion), + unrealized: round2(unrealized) + }, + counts: { ...counts, open_positions }, + pnl_trading_usd: pnl_trading, + pnl_inclusive_usd: pnl_inclusive, + open_positions, + source: 'data-api.polymarket.com/activity + /positions' + }; +} + +async function fetchActivityOffset(fetchImpl, address, activityType, { deadline, maxPagesPerType }) { + const items = []; + let offset = 0; + let pages = 0; + let pagination_incomplete = false; + let budget_hit = false; + + while (pages < maxPagesPerType) { + if (Date.now() >= deadline) { + budget_hit = true; + pagination_incomplete = true; + break; + } + const url = `${DATA_BASE}/activity?user=${address}&type=${activityType}&limit=${PAGE_LIMIT}&offset=${offset}&sortDirection=ASC`; + const records = await fetchJson(fetchImpl, url).catch(() => []); + pages += 1; + if (!Array.isArray(records) || !records.length) break; + items.push(...records); + if (records.length < PAGE_LIMIT) break; + offset += records.length; + if (offset >= ACTIVITY_OFFSET_CAP) { + pagination_incomplete = true; + break; + } + } + if (pages >= maxPagesPerType && items.length >= PAGE_LIMIT * maxPagesPerType) { + pagination_incomplete = true; + } + return { items, pages, pagination_incomplete, budget_hit }; +} + +async function fetchActivityTimestamp(fetchImpl, address, activityType, { deadline, maxPagesPerType }) { + const items = []; + let end = null; + let pages = 0; + let pagination_incomplete = false; + let budget_hit = false; + + while (pages < maxPagesPerType) { + if (Date.now() >= deadline) { + budget_hit = true; + pagination_incomplete = true; + break; + } + const endParam = end == null ? '' : `&end=${end}`; + const url = `${DATA_BASE}/activity?user=${address}&type=${activityType}&limit=${PAGE_LIMIT}${endParam}`; + const records = await fetchJson(fetchImpl, url).catch(() => []); + pages += 1; + if (!Array.isArray(records) || !records.length) break; + items.push(...records); + + const timestamps = records.map((r) => Number(r.timestamp)).filter(Number.isFinite); + if (!timestamps.length) break; + const oldest = Math.min(...timestamps); + const oldestCount = timestamps.filter((t) => t === oldest).length; + + // Same-second full-page boundary can lose rows; mark incomplete (Worker skips exact-second backfill). + if (records.length === PAGE_LIMIT && oldestCount > 0) { + pagination_incomplete = true; + } + + const nextEnd = oldest - 1; + if (nextEnd < 0 || records.length < PAGE_LIMIT) break; + end = nextEnd; + } + + if (pages >= maxPagesPerType) pagination_incomplete = true; + return { items, pages, pagination_incomplete, budget_hit }; +} + +async function fetchAllPositions(fetchImpl, address, { deadline }) { + const items = []; + let offset = 0; + let truncated = false; + let budget_hit = false; + + while (true) { + if (Date.now() >= deadline) { + budget_hit = true; + truncated = true; + break; + } + const url = `${DATA_BASE}/positions?user=${address}&sizeThreshold=0&limit=${PAGE_LIMIT}&offset=${offset}`; + const records = await fetchJson(fetchImpl, url).catch(() => []); + if (!Array.isArray(records) || !records.length) break; + items.push(...records); + if (records.length < PAGE_LIMIT) break; + offset += records.length; + if (offset >= POSITIONS_OFFSET_CAP) { + if (records.length === PAGE_LIMIT) truncated = true; + break; + } + } + return { items, truncated, budget_hit }; +} + +function sumUsdc(rows) { + return (rows || []).reduce((sum, row) => sum + toNumber(row.usdcSize), 0); +} + +async function resolveAddress(fetchImpl, raw) { + if (EVM.test(raw)) { + return { address: raw.toLowerCase(), resolved_via: 'evm_address', display_name: null }; + } + const needle = raw.toLowerCase(); + // Leaderboard username search is best-effort (paged LB). Prefer 0x address for certainty. + const maxPages = 6; // ~3000 rows; beyond this return unresolved (caller should pass 0x) + for (let page = 0; page < maxPages; page++) { + const rows = await fetchJson( + fetchImpl, + `${LB_BASE}/profit?window=all&limit=500&offset=${page * 500}` + ).catch(() => []); + if (!Array.isArray(rows) || !rows.length) break; + for (const row of rows) { + const name = String(row.name ?? '').toLowerCase(); + const pseudonym = String(row.pseudonym ?? '').toLowerCase(); + if (name === needle || pseudonym === needle) { + const address = String(row.proxyWallet ?? '').toLowerCase(); + return { + address: EVM.test(address) ? address : null, + resolved_via: 'leaderboard_username', + display_name: row.name ?? row.pseudonym ?? raw + }; + } + } + if (rows.length < 500) break; + } + return { address: null, resolved_via: 'username_not_in_scanned_leaderboard', display_name: null }; +} + +function summarizeLeaderboard(rows, address) { + const row = Array.isArray(rows) ? rows[0] : null; + return { + amount_usd: row?.amount != null ? round2(Number(row.amount)) : null, + name: row?.name ?? row?.pseudonym ?? null, + proxy_wallet: row?.proxyWallet ?? address, + window: 'all', + source: 'lb-api.polymarket.com/profit?window=all', + row_found: Boolean(row) + }; +} + +function summarizePositions(rows, limit) { + const list = Array.isArray(rows) ? rows : []; + const normalized = list.map((p) => ({ + title: p.title ?? p.slug ?? null, + outcome: p.outcome ?? null, + size: toNumber(p.size), + avg_price: toNumber(p.avgPrice), + cash_pnl: toNumber(p.cashPnl), + current_value: toNumber(p.currentValue), + condition_id: p.conditionId ?? null + })); + const total = normalized.reduce((sum, p) => sum + (p.cash_pnl || 0), 0); + return { + total_cash_pnl_usd: round2(total), + positions_sampled: normalized.length, + open_positions_sampled: normalized.filter((p) => Math.abs(p.size || 0) > 0).length, + positions_limit: limit, + positions_truncated: normalized.length >= limit, + source: 'data-api.polymarket.com/positions cashPnl', + sample: normalized.slice(0, 10) + }; +} + +function computeDivergence(leaderboard, positions) { + const lb = leaderboard.amount_usd; + const pos = positions.total_cash_pnl_usd; + if (!Number.isFinite(lb) || !Number.isFinite(pos)) { + return { + verdict: 'unknown', + delta_usd: null, + abs_delta_usd: null, + threshold_usd: AUDIT_PASS_DELTA_USD, + interpretation: 'Missing LB or position cashPnL layer.', + compared_layer: 'positions_cash_pnl' + }; + } + const delta = round2(lb - pos); + const absDelta = Math.abs(delta); + let verdict = 'aligned'; + if (absDelta > AUDIT_PASS_DELTA_USD) { + verdict = delta > 0 ? 'lb_optimistic' : 'replay_higher'; + } + return { + verdict, + delta_usd: delta, + abs_delta_usd: round2(absDelta), + threshold_usd: AUDIT_PASS_DELTA_USD, + compared_layer: 'positions_cash_pnl', + interpretation: verdict === 'aligned' + ? 'LB all-time and sampled position cashPnL are within the quick-audit threshold.' + : (verdict === 'lb_optimistic' + ? 'Leaderboard profit is materially higher than sampled position cashPnL; do not trust headline PnL without full replay.' + : 'Position cashPnL proxy is materially higher than LB; full replay may exceed the headline or LB window may differ.') + }; +} + +function computeReplayDivergence(leaderboard, replay) { + const lb = leaderboard.amount_usd; + const replayPnl = replay?.pnl_trading_usd; + if (!Number.isFinite(lb) || !Number.isFinite(replayPnl)) { + return { + verdict: 'unknown', + delta_usd: null, + abs_delta_usd: null, + threshold_usd: AUDIT_PASS_DELTA_USD, + compared_layer: 'cashflow_replay', + interpretation: 'Missing LB or cashflow replay PnL.' + }; + } + const delta = round2(lb - replayPnl); + const absDelta = Math.abs(delta); + let verdict = 'aligned'; + if (absDelta > AUDIT_PASS_DELTA_USD) { + verdict = delta > 0 ? 'lb_optimistic' : 'replay_higher'; + } + if (!replay.complete && verdict === 'aligned') { + return { + verdict: 'unknown', + delta_usd: delta, + abs_delta_usd: round2(absDelta), + threshold_usd: AUDIT_PASS_DELTA_USD, + compared_layer: 'cashflow_replay', + interpretation: 'Replay incomplete; do not treat near-match as audit pass.' + }; + } + return { + verdict, + delta_usd: delta, + abs_delta_usd: round2(absDelta), + threshold_usd: AUDIT_PASS_DELTA_USD, + compared_layer: 'cashflow_replay', + pnl_inclusive_usd: replay.pnl_inclusive_usd, + interpretation: verdict === 'aligned' + ? 'LB all-time and cashflow-replay trading PnL are within the audit threshold.' + : (verdict === 'lb_optimistic' + ? 'Leaderboard profit is materially higher than cashflow replay; distrust headline claims for copy-trading.' + : 'Cashflow replay exceeds LB; LB window/credits may differ — verify manually before trusting either.') + }; +} + +function chooseAction(divergence, { positions_truncated, requestedMode, replayComplete }) { + if (requestedMode === 'full') { + if (!replayComplete) return 'verify_manually'; + if (divergence.verdict === 'aligned') return 'trust_for_copy'; + if (divergence.verdict === 'lb_optimistic' && (divergence.abs_delta_usd ?? 0) >= 100) { + return 'distrust_claims'; + } + return 'verify_manually'; + } + // Quick is triage only: LB vs sampled position cashPnL. Never emit trust_for_copy + // without a complete cashflow replay (mode=full). + if (divergence.verdict === 'lb_optimistic' && (divergence.abs_delta_usd ?? 0) >= 100) { + return 'distrust_claims'; + } + if (divergence.verdict === 'aligned' && !positions_truncated) return 'quick_triage_ok'; + return 'verify_manually'; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function normalizeMode(value) { + const raw = String(value ?? 'quick').trim().toLowerCase(); + return raw === 'full' ? 'full' : 'quick'; +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} + +function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function round2(value) { + return Math.round(value * 100) / 100; +} + +function buildBuyerSummaryZh({ + resolved, leaderboard_profit, positions_cash_pnl, divergence, action, cashflow_replay, requestedMode +}) { + const who = leaderboard_profit.name || resolved.display_name || shorten(resolved.address); + const lb = leaderboard_profit.amount_usd == null ? 'LB 无记录' : `LB all-time ${formatUsd(leaderboard_profit.amount_usd)}`; + if (requestedMode === 'full' && cashflow_replay) { + const replay = `回放 trading ${formatUsd(cashflow_replay.pnl_trading_usd)}` + + (cashflow_replay.complete ? '' : '(incomplete)'); + return `${who}:${lb};${replay};verdict=${divergence.verdict},action=${action}。`; + } + const pos = positions_cash_pnl.total_cash_pnl_usd == null + ? '持仓 PnL 无记录' + : `持仓 cashPnL ${formatUsd(positions_cash_pnl.total_cash_pnl_usd)}`; + return `${who}:${lb};${pos};verdict=${divergence.verdict},action=${action}。quick 不是完整流水审计。`; +} + +function buildBuyerSummaryEn({ + resolved, leaderboard_profit, positions_cash_pnl, divergence, action, cashflow_replay, requestedMode +}) { + const who = leaderboard_profit.name || resolved.display_name || shorten(resolved.address); + const lb = leaderboard_profit.amount_usd == null ? 'LB missing' : `LB all-time ${formatUsd(leaderboard_profit.amount_usd)}`; + if (requestedMode === 'full' && cashflow_replay) { + const replay = `replay trading ${formatUsd(cashflow_replay.pnl_trading_usd)}` + + (cashflow_replay.complete ? '' : ' (incomplete)'); + return `${who}: ${lb}; ${replay}; verdict=${divergence.verdict}, action=${action}.`; + } + const pos = positions_cash_pnl.total_cash_pnl_usd == null + ? 'position cashPnL missing' + : `position cashPnL ${formatUsd(positions_cash_pnl.total_cash_pnl_usd)}`; + return `${who}: ${lb}; ${pos}; verdict=${divergence.verdict}, action=${action}. Quick mode is not full cashflow replay.`; +} + +function formatUsd(value) { + const n = Number(value); + if (!Number.isFinite(n)) return 'n/a'; + return `${n >= 0 ? '+' : '-'}$${Math.abs(n).toFixed(2)}`; +} + +function shorten(address) { + const value = String(address ?? ''); + if (!value.startsWith('0x') || value.length < 12) return value || 'wallet'; + return `${value.slice(0, 6)}...${value.slice(-4)}`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-profile.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-profile.mjs new file mode 100644 index 00000000..4f9e090e --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-profile.mjs @@ -0,0 +1,183 @@ +// PM Profile — read-only Polymarket wallet/profile snapshot. +// Productizes public polymarket-toolkit profile surface (LB PnL + positions sample). +// No keys, no signing, no order routing. + +const SERVICE_ID = 'pm_profile'; +const LB_BASE = 'https://lb-api.polymarket.com'; +const DATA_BASE = 'https://data-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; +const EVM = /^0x[a-fA-F0-9]{40}$/; + +const STANDARD_CAVEATS = [ + 'Read-only public Polymarket snapshot. Not investment advice.', + 'Leaderboard PnL is a platform snapshot — not audit-grade fee-inclusive PnL.', + 'Positions page is capped; large wallets may be truncated.', + 'No wallet custody, no trade execution, no order routing.' +]; + +/** + * @param {object} input + * @param {string} [input.address] + * @param {string} [input.wallet] + * @param {string} [input.username] + * @param {string} [input.query] + */ +export async function assessPmProfileLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const raw = String(input.address ?? input.wallet ?? input.username ?? input.query ?? '').trim(); + if (!raw) { + throw new Error('address (0x…) or username is required'); + } + + let address = null; + let resolved_via = null; + if (EVM.test(raw)) { + address = raw.toLowerCase(); + resolved_via = 'evm_address'; + } else { + address = await resolveUsername(fetchImpl, raw); + resolved_via = address ? 'leaderboard_username' : null; + } + + if (!address) { + throw new Error(`Could not resolve wallet for "${raw}" via leaderboard username search`); + } + + const [pnlRows, positions] = await Promise.all([ + fetchJson(fetchImpl, `${LB_BASE}/profit?address=${address}&window=7d`).catch(() => []), + fetchJson(fetchImpl, `${DATA_BASE}/positions?user=${address}&limit=50`).catch(() => []) + ]); + + const pnl7d = Array.isArray(pnlRows) && pnlRows[0] + ? { + amount: toNumber(pnlRows[0].amount), + name: pnlRows[0].name ?? null, + pseudonym: pnlRows[0].pseudonym ?? null, + proxy_wallet: pnlRows[0].proxyWallet ?? address + } + : null; + + const posList = (Array.isArray(positions) ? positions : []).map((p) => ({ + title: p.title ?? p.slug ?? null, + outcome: p.outcome ?? null, + size: toNumber(p.size), + avg_price: toNumber(p.avgPrice), + cash_pnl: toNumber(p.cashPnl), + cur_price: toNumber(p.curPrice), + condition_id: p.conditionId ?? null + })); + + const openCount = posList.filter((p) => Math.abs(p.size) > 0).length; + const approxPosPnl = posList.reduce((sum, p) => sum + (p.cash_pnl || 0), 0); + + const displayName = pnl7d?.name ?? pnl7d?.pseudonym ?? null; + const pnl7dAmt = pnl7d?.amount ?? null; + const approxPnl = Math.round(approxPosPnl * 100) / 100; + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + query: raw, + address, + resolved_via + }, + profile: { + address, + display_name: displayName, + pnl_7d_usdt: pnl7dAmt, + open_positions_sampled: openCount, + approx_positions_cash_pnl: approxPnl, + positions_sample: posList.slice(0, 20) + }, + buyer_summary_zh: buildBuyerSummaryZh({ displayName, address, pnl7dAmt, openCount, approxPnl }), + confidence_gaps: [ + ...(pnl7d ? [] : ['not_on_7d_leaderboard']), + 'positions_page_capped', + 'not_fee_inclusive_audit_pnl' + ], + caveats: [...STANDARD_CAVEATS], + next_gate: 'Use_polymarket-toolkit_pnl_skill_for_audit_grade', + source: { + provider: 'polymarket_public_api', + oss_lineage: 'polymarket-toolkit pm profile (read-only public APIs)', + windows: { pnl: '7d', positions_limit: 50 } + } + }; +} + +export function buildPmProfileFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { query: input.address ?? input.username ?? null, address: null, resolved_via: null }, + profile: { + address: null, + display_name: null, + pnl_7d_usdt: null, + open_positions_sampled: 0, + approx_positions_cash_pnl: 0, + positions_sample: [] + }, + confidence_gaps: ['live_data_unavailable'], + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Use_polymarket-toolkit_pnl_skill_for_audit_grade', + source: { provider: 'static_fallback' } + }; +} + +async function resolveUsername(fetchImpl, username) { + const needle = username.trim().toLowerCase(); + for (let page = 0; page < 3; page++) { + const rows = await fetchJson( + fetchImpl, + `${LB_BASE}/profit?window=all&limit=500&offset=${page * 500}` + ).catch(() => []); + if (!Array.isArray(rows) || !rows.length) break; + for (const row of rows) { + const n = String(row.name ?? '').toLowerCase(); + const p = String(row.pseudonym ?? '').toLowerCase(); + if (n === needle || p === needle) return String(row.proxyWallet ?? '').toLowerCase() || null; + } + if (rows.length < 500) break; + } + return null; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function buildBuyerSummaryZh({ displayName, address, pnl7dAmt, openCount, approxPnl }) { + const who = displayName || shorten(address); + const pnlBit = pnl7dAmt == null + ? '7日榜无记录' + : `7日榜 PnL ${pnl7dAmt >= 0 ? '+' : ''}${pnl7dAmt} USDT`; + return `${who}:${pnlBit};抽样持仓 ${openCount} 个,持仓现金盈亏约 ${approxPnl} USDT。只读画像,非下单建议。`; +} + +function shorten(address) { + const value = String(address ?? ''); + if (!value.startsWith('0x') || value.length < 12) return value || '钱包'; + return `${value.slice(0, 6)}…${value.slice(-4)}`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-scenario-skus.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-scenario-skus.mjs new file mode 100644 index 00000000..762ea9c2 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-scenario-skus.mjs @@ -0,0 +1,694 @@ +// Scenario SKUs — productize category plugins as searchable ASP surfaces. +// Each wraps pm-event-readout with a fixed scenario + optional query discovery. + +import { + assessPmEventReadoutLive, + buildPmEventReadoutFallback +} from './pm-event-readout.mjs'; +import { + GAMMA_BASE, + resolveMarketRef, + fetchJson +} from './pm-gamma-market.mjs'; +import { + queryRequiresEntityMatch, + scoreSemanticMatch +} from './pm-semantic-match.mjs'; + +const STRONG_ENTITY_SEMANTIC_THRESHOLD = 4.5; + +const SCENARIOS = { + weather_event_readout: { + service_id: 'weather_event_readout', + expected: ['weather'], + default_query: 'temperature high', + query_variants: ['temperature high', 'high temperature', 'weather', 'temperature'], + tag_slugs: ['weather'], + category_keywords: ['weather', 'temperature', 'high temperature'], + zh_name: '天气温度阶梯', + sample: { + query: 'temperature', + weather: { city: 'NYC', snapshot_time: '2026-07-24T12:00:00Z', observed_temp_f: 84 } + } + }, + politics_event_readout: { + service_id: 'politics_event_readout', + expected: ['politics'], + default_query: 'presidential election', + query_variants: ['presidential election', 'election', 'politics', 'senate', 'governor'], + tag_slugs: ['politics', 'elections'], + category_keywords: ['politics', 'election', 'presidential election', 'senate'], + zh_name: '政治选举盘口', + sample: { query: 'president' } + }, + macro_fed_readout: { + service_id: 'macro_fed_readout', + expected: ['macro_fed'], + default_query: 'fed interest rates', + query_variants: ['fed interest rates', 'fed decision', 'fomc', 'interest rates', 'rate cut'], + tag_slugs: ['fed', 'federal-reserve', 'economics', 'macro'], + category_keywords: ['fed', 'fomc', 'interest rates', 'rate cut', 'rate hike'], + zh_name: '美联储利率宏观', + sample: { query: 'fed rates' } + }, + football_match_card: { + service_id: 'football_match_card', + expected: ['football'], + default_query: 'premier league', + query_variants: ['premier league', 'football', 'soccer', 'champions league', 'epl'], + tag_slugs: ['soccer', 'football', 'epl', 'premier-league', 'ucl', 'champions-league'], + category_keywords: ['football', 'soccer', 'premier league', 'uefa', 'fifa'], + zh_name: '足球比赛卡', + sample: { + query: 'premier league', + football: { verified: false } + } + }, + tennis_match_card: { + service_id: 'tennis_match_card', + expected: ['tennis'], + default_query: 'atp tennis', + // Prefer multi-token tennis queries; bare "atp" alone historically mis-hit esports names like "Atputies". + query_variants: ['atp tennis', 'wta tennis', 'tennis match', 'wimbledon tennis', 'tennis'], + tag_slugs: ['tennis', 'atp', 'wta'], + category_keywords: ['tennis', 'atp', 'wta', 'wimbledon'], + zh_name: '网球比赛卡', + sample: { + query: 'atp', + tennis: { verified: false } + } + }, + nba_match_card: { + service_id: 'nba_match_card', + expected: ['nba'], + default_query: 'nba', + query_variants: ['nba', 'basketball', 'nba finals', 'lakers', 'celtics'], + tag_slugs: ['nba', 'basketball'], + category_keywords: ['nba', 'basketball', 'wnba'], + zh_name: 'NBA 比赛卡', + sample: { + query: 'nba', + nba: { verified: false } + } + } +}; + +export function listScenarioSkuIds() { + return Object.keys(SCENARIOS); +} + +export function samplePayloadForScenario(serviceId) { + return SCENARIOS[serviceId]?.sample ?? { query: 'all' }; +} + +export async function assessWeatherEventReadoutLive(input = {}, options = {}) { + return assessScenarioSkuLive('weather_event_readout', input, options); +} +export async function assessPoliticsEventReadoutLive(input = {}, options = {}) { + return assessScenarioSkuLive('politics_event_readout', input, options); +} +export async function assessMacroFedReadoutLive(input = {}, options = {}) { + return assessScenarioSkuLive('macro_fed_readout', input, options); +} +export async function assessFootballMatchCardLive(input = {}, options = {}) { + return assessScenarioSkuLive('football_match_card', input, options); +} +export async function assessTennisMatchCardLive(input = {}, options = {}) { + return assessScenarioSkuLive('tennis_match_card', input, options); +} +export async function assessNbaMatchCardLive(input = {}, options = {}) { + return assessScenarioSkuLive('nba_match_card', input, options); +} + +export function buildWeatherEventReadoutFallback(input = {}) { + return buildScenarioFallback('weather_event_readout', input); +} +export function buildPoliticsEventReadoutFallback(input = {}) { + return buildScenarioFallback('politics_event_readout', input); +} +export function buildMacroFedReadoutFallback(input = {}) { + return buildScenarioFallback('macro_fed_readout', input); +} +export function buildFootballMatchCardFallback(input = {}) { + return buildScenarioFallback('football_match_card', input); +} +export function buildTennisMatchCardFallback(input = {}) { + return buildScenarioFallback('tennis_match_card', input); +} +export function buildNbaMatchCardFallback(input = {}) { + return buildScenarioFallback('nba_match_card', input); +} + +async function assessScenarioSkuLive(scenarioKey, input = {}, options = {}) { + const spec = SCENARIOS[scenarioKey]; + if (!spec) throw new Error(`Unknown scenario ${scenarioKey}`); + const fetchImpl = options.fetchImpl ?? fetch; + + const resolvedInput = await resolveScenarioInput(input, spec, fetchImpl); + if (resolvedInput._unavailable) { + return buildScenarioUnavailable(spec, input, resolvedInput); + } + const base = await assessPmEventReadoutLive(resolvedInput, { + ...options, + fetchImpl, + enrichCategory: true + }); + + const category = base.category || base.category_plugin?.category || 'generic'; + const expected_ok = spec.expected.includes(category); + const caveats = [ + ...(base.caveats || []), + `Scenario SKU ${spec.service_id}: expects ${spec.expected.join('|')}; detected ${category}.` + ]; + if (!expected_ok) { + caveats.push( + `Category mismatch: this SKU is for ${spec.zh_name}; detected "${category}". Results may be core_only — pick a matching market or use /pm-event-readout.` + ); + } + caveats.push( + 'Paid-value note: this is a scenario entry SKU over /pm-event-readout + category plugin — pay again when the event/market changes, not for a different wrapper of the same frozen card.' + ); + + return { + ...base, + schema_version: '0.1', + service_id: spec.service_id, + scenario: { + id: spec.service_id, + zh_name: spec.zh_name, + expected_categories: spec.expected, + detected_category: category, + expected_ok, + resolved_via: resolvedInput._resolved_via || 'caller_ref', + query: resolvedInput._query || null + }, + buyer_summary_zh: buildScenarioBuyerSummaryZh(spec, base, expected_ok), + caveats, + next_gate: 'Use_pm_trade_preflight_before_orders', + source: { + ...(base.source || {}), + scenario_sku: spec.service_id, + method: 'pm_event_readout_scenario_wrapper' + } + }; +} + +function buildScenarioFallback(scenarioKey, input = {}) { + const spec = SCENARIOS[scenarioKey]; + const base = buildPmEventReadoutFallback(input); + return { + ...base, + service_id: spec.service_id, + scenario: { + id: spec.service_id, + zh_name: spec.zh_name, + expected_categories: spec.expected, + detected_category: null, + expected_ok: false, + resolved_via: 'fallback' + }, + buyer_summary_zh: `演示回退:${spec.zh_name} 实时行情不可用。请传 market_url/slug 或 query。`, + mode: 'public_safe_demo' + }; +} + +async function resolveScenarioInput(input, spec, fetchImpl) { + const ref = resolveMarketRef(input); + if (ref.slug || ref.condition_id) { + return { ...input, _resolved_via: 'caller_ref' }; + } + + const query = String(input.query ?? input.market ?? input.topic ?? spec.default_query).trim(); + const preferredSurface = preferredSurfaceForScenario(spec.service_id); + const queryDiscovery = await discoverSlug(fetchImpl, buildQueryVariants(query, spec), spec.expected, { + originalQuery: query, + preferredSurface + }); + if (queryDiscovery.slug) { + return { + ...input, + slug: queryDiscovery.slug, + _resolved_via: queryDiscovery.query === query ? 'public_search' : 'query_variant', + _query: query, + _discovery: queryDiscovery.discovery + }; + } + + const categoryDiscovery = await discoverCategoryDefault(fetchImpl, spec, { + originalQuery: query, + preferredSurface + }); + if (categoryDiscovery.slug) { + return { + ...input, + slug: categoryDiscovery.slug, + _resolved_via: 'category_default', + _query: query || spec.default_query, + _discovery: { + query_attempts: queryDiscovery.discovery, + category_default: categoryDiscovery.discovery + } + }; + } + + if (!queryDiscovery.hadSuccessfulFetch && !categoryDiscovery.hadSuccessfulFetch) { + const detail = queryDiscovery.errors[0] || categoryDiscovery.errors[0] || 'unknown upstream failure'; + throw new Error(`Scenario discovery upstream unavailable for ${spec.service_id}: ${detail}`); + } + + return { + ...input, + _unavailable: true, + _resolved_via: 'no_active_markets', + _query: query || spec.default_query, + _discovery: { + query_attempts: queryDiscovery.discovery, + category_default: categoryDiscovery.discovery + } + }; +} + +async function discoverSlug(fetchImpl, queryVariants, expectedCategories, options = {}) { + const originalQuery = String(options.originalQuery ?? queryVariants?.[0] ?? '').trim(); + const preferredSurface = options.preferredSurface || null; + const requiresEntity = queryRequiresEntityMatch(originalQuery); + const discovery = { + method: null, + query_variants: [], + candidates_seen: 0, + preferred_surface: preferredSurface, + semantic_entity_required: requiresEntity, + semantic_threshold: requiresEntity ? STRONG_ENTITY_SEMANTIC_THRESHOLD : null + }; + const errors = []; + let hadSuccessfulFetch = false; + + for (const query of queryVariants) { + discovery.query_variants.push(query); + try { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(query)}&events_status=active&limit_per_type=12` + ); + hadSuccessfulFetch = true; + const candidates = collectEventMarketCandidates( + Array.isArray(result?.events) ? result.events : [], + expectedCategories, + originalQuery, + preferredSurface + ); + discovery.candidates_seen += candidates.length; + const best = pickBestCandidate(candidates, { requiresEntity, preferredSurface }); + if (best) { + return { + slug: best.slug, + query, + discovery: { + ...discovery, + method: `public-search:${query}`, + selected: best.slug, + selected_semantic_score: best.semantic_score, + selected_surface: best.surface + }, + hadSuccessfulFetch, + errors + }; + } + } catch (error) { + errors.push(error instanceof Error ? error.message : String(error)); + } + } + + return { slug: null, query: null, discovery, hadSuccessfulFetch, errors }; +} + +async function discoverCategoryDefault(fetchImpl, spec, options = {}) { + const originalQuery = String(options.originalQuery ?? '').trim(); + const preferredSurface = options.preferredSurface || null; + const requiresEntity = queryRequiresEntityMatch(originalQuery); + const discovery = { + method: null, + tag_slugs: [], + category_keywords: [], + candidates_seen: 0, + preferred_surface: preferredSurface, + semantic_entity_required: requiresEntity, + semantic_threshold: requiresEntity ? STRONG_ENTITY_SEMANTIC_THRESHOLD : null + }; + const errors = []; + let hadSuccessfulFetch = false; + + for (const tag of spec.tag_slugs || []) { + discovery.tag_slugs.push(tag); + try { + const events = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?closed=false&active=true&limit=25&order=volume24hr&ascending=false&tag_slug=${encodeURIComponent(tag)}` + ); + hadSuccessfulFetch = true; + const candidates = collectEventMarketCandidates( + Array.isArray(events) ? events : [], + spec.expected, + originalQuery, + preferredSurface + ); + discovery.candidates_seen += candidates.length; + const best = pickBestCandidate(candidates, { requiresEntity, preferredSurface }); + if (best) { + return { + slug: best.slug, + discovery: { + ...discovery, + method: `tag_slug:${tag}`, + selected: best.slug, + selected_semantic_score: best.semantic_score, + selected_surface: best.surface + }, + hadSuccessfulFetch, + errors + }; + } + } catch (error) { + errors.push(error instanceof Error ? error.message : String(error)); + } + } + + for (const keyword of spec.category_keywords || []) { + discovery.category_keywords.push(keyword); + try { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(keyword)}&events_status=active&limit_per_type=12` + ); + hadSuccessfulFetch = true; + const candidates = collectEventMarketCandidates( + Array.isArray(result?.events) ? result.events : [], + spec.expected, + originalQuery, + preferredSurface + ); + discovery.candidates_seen += candidates.length; + const best = pickBestCandidate(candidates, { requiresEntity, preferredSurface }); + if (best) { + return { + slug: best.slug, + discovery: { + ...discovery, + method: `category_keyword:${keyword}`, + selected: best.slug, + selected_semantic_score: best.semantic_score, + selected_surface: best.surface + }, + hadSuccessfulFetch, + errors + }; + } + } catch (error) { + errors.push(error instanceof Error ? error.message : String(error)); + } + } + + try { + const rows = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?closed=false&active=true&limit=50&order=volume24hr&ascending=false` + ); + hadSuccessfulFetch = true; + const candidates = collectMarketCandidates( + Array.isArray(rows) ? rows : [], + spec.expected, + originalQuery, + preferredSurface + ); + discovery.candidates_seen += candidates.length; + const best = pickBestCandidate(candidates, { requiresEntity, preferredSurface }); + if (best) { + return { + slug: best.slug, + discovery: { + ...discovery, + method: 'top_volume_category_filter', + selected: best.slug, + selected_semantic_score: best.semantic_score, + selected_surface: best.surface + }, + hadSuccessfulFetch, + errors + }; + } + } catch (error) { + errors.push(error instanceof Error ? error.message : String(error)); + } + + return { slug: null, discovery, hadSuccessfulFetch, errors }; +} + +function preferredSurfaceForScenario(serviceId) { + if (serviceId === 'football_match_card' || serviceId === 'tennis_match_card' || serviceId === 'nba_match_card') { + return 'match'; + } + if (serviceId === 'macro_fed_readout' || serviceId === 'politics_event_readout') { + return 'ladder'; + } + return null; +} + +function buildQueryVariants(query, spec) { + const q = String(query || '').trim(); + const extras = []; + if (queryRequiresEntityMatch(q) && preferredSurfaceForScenario(spec.service_id) === 'match') { + extras.push(`${q} vs`, `${q} versus`, `${q} match`); + } + return uniqueStrings([ + q, + ...extras, + ...(spec.query_variants || []), + spec.default_query, + ...(spec.expected || []) + ]); +} + +function collectEventMarketCandidates(events, expectedCategories, query = '', preferredSurface = null) { + const candidates = []; + for (const event of events) { + if (event?.closed) continue; + const eventBlob = `${event.title || ''} ${event.slug || ''}`.toLowerCase(); + for (const market of event.markets || []) { + if (!market?.slug || market.closed || market.active === false) continue; + const title = market.question || market.title || ''; + const blob = `${eventBlob} ${title} ${market.slug || ''}`.toLowerCase(); + const categoryScore = scoreCategoryMatch(blob, expectedCategories); + if (categoryScore <= 0) continue; + const volume = toNumber(market.volume24hr ?? market.volumeNum ?? market.volume ?? event.volume24hr); + const semanticScore = query + ? scoreSemanticMatch({ + query, + title, + slug: market.slug, + eventTitle: event.title || event.slug + }) + : 0; + const surface = classifyMarketSurface(blob); + const surfaceBoost = scoreSurfacePreference(surface, preferredSurface); + candidates.push({ + slug: market.slug, + score: categoryScore + semanticScore * 2 + surfaceBoost + Math.min(volume / 100000, 3), + category_score: categoryScore, + semantic_score: semanticScore, + surface, + volume + }); + } + } + return candidates; +} + +function collectMarketCandidates(markets, expectedCategories, query = '', preferredSurface = null) { + const candidates = []; + for (const market of markets) { + if (!market?.slug || market.closed || market.active === false) continue; + const title = market.question || market.title || market.description || ''; + const blob = `${title} ${market.slug || ''}`.toLowerCase(); + const categoryScore = scoreCategoryMatch(blob, expectedCategories); + if (categoryScore <= 0) continue; + const volume = toNumber(market.volume24hr ?? market.volumeNum ?? market.volume); + const semanticScore = query + ? scoreSemanticMatch({ + query, + title, + slug: market.slug, + eventTitle: market.events?.[0]?.title || market.eventTitle + }) + : 0; + const surface = classifyMarketSurface(blob); + const surfaceBoost = scoreSurfacePreference(surface, preferredSurface); + candidates.push({ + slug: market.slug, + score: categoryScore + semanticScore * 2 + surfaceBoost + Math.min(volume / 100000, 3), + category_score: categoryScore, + semantic_score: semanticScore, + surface, + volume + }); + } + return candidates; +} + +function classifyMarketSurface(blob) { + const text = String(blob || '').toLowerCase(); + if (/\bvs\.?\b|\bv\b|versus|moneyline|spread|o\/u|over\/under|90m|tip-?off|kickoff/.test(text)) { + return 'match'; + } + if (/championship|title|finals|outright|season winner|to win the|league winner|cup winner|fed|fomc|election|nominee|temperature|high temp/.test(text)) { + return 'ladder_or_outright'; + } + return 'other'; +} + +function scoreSurfacePreference(surface, preferredSurface) { + if (!preferredSurface) return 0; + if (preferredSurface === 'match') { + if (surface === 'match') return 4; + if (surface === 'ladder_or_outright') return -3; + } + if (preferredSurface === 'ladder') { + if (surface === 'ladder_or_outright') return 2; + } + return 0; +} + +function pickBestCandidate(candidates, options = {}) { + const ranked = candidates + .slice() + .sort((a, b) => b.score - a.score || b.semantic_score - a.semantic_score || b.volume - a.volume); + let pool = ranked; + if (options.preferredSurface === 'match') { + // Hard gate for Match Card SKUs: never fall back to season/outright/award surfaces. + const matchOnly = ranked.filter((c) => c.surface === 'match'); + if (!matchOnly.length) return null; + pool = matchOnly; + } + const best = pool[0] ?? null; + if (!best) return null; + if (options.requiresEntity && best.semantic_score < STRONG_ENTITY_SEMANTIC_THRESHOLD) { + return null; + } + return best; +} + +function buildScenarioUnavailable(spec, input, resolvedInput) { + const query = resolvedInput._query || input.query || input.market || input.topic || spec.default_query; + return { + schema_version: '0.1', + service_id: spec.service_id, + mode: 'live', + generated_at: new Date().toISOString(), + capability_status: 'no_active_markets', + action: 'unavailable', + input: { + market_url: input.market_url ?? null, + condition_id: input.condition_id ?? null, + slug: input.slug ?? null, + query + }, + scenario: { + id: spec.service_id, + zh_name: spec.zh_name, + expected_categories: spec.expected, + detected_category: null, + expected_ok: false, + resolved_via: 'no_active_markets', + query, + discovery: resolvedInput._discovery ?? null + }, + buyer_summary_zh: `${spec.zh_name}:已实时检索 Polymarket,但没有找到仍活跃且匹配该品类的市场;本次结果不可用,不返回伪造比赛/天气/政治卡。`, + buyer_summary_en: `${spec.zh_name}: No active Polymarket markets matched this scenario after live discovery; this response is unavailable instead of a fake demo card.`, + paid_checks: { + pass_count: 2, + fail_count: 1, + checks: [ + { id: 'live_discovery_attempted', status: 'pass', detail: 'Queried Gamma public-search, tag/category events, and top-volume category filter.' }, + { id: 'category_market_match', status: 'fail', detail: `No active ${spec.expected.join('/')} market found.` }, + { id: 'fake_card_suppressed', status: 'pass', detail: 'Returned structured unavailable response instead of static fallback card.' } + ] + }, + caveats: [ + 'Data and analytics only. Not investment advice, not betting advice, and not a guarantee of future returns.', + 'No active matching market was found at request time; retry later or pass an explicit active market_url/slug.' + ], + next_gate: 'Retry_when_active_markets_exist_or_pass_explicit_slug', + source: { + provider: 'polymarket_gamma_public_api', + scenario_sku: spec.service_id, + method: 'pm_event_readout_scenario_discovery', + discovery: resolvedInput._discovery ?? null + } + }; +} + +function uniqueStrings(values) { + const seen = new Set(); + const result = []; + for (const value of values) { + const text = String(value ?? '').trim(); + const key = text.toLowerCase(); + if (!text || seen.has(key)) continue; + seen.add(key); + result.push(text); + } + return result; +} + +const ESPORTS_BLOB_RE = /\bcounter[-\s]?strike\b|\bcs:?go\b|\bcs2\b|\bdota\b|\bleague of legends\b|\bvalorant\b|\besports?\b|\bmap\s*\d\b/; + +/** + * Category match for scenario discovery. Word-boundary safe for short tokens + * like atp/wta so "Atputies" (CS) cannot score as tennis. + * Exported for unit tests. + */ +export function scoreCategoryMatch(blob, expected) { + const text = String(blob || '').toLowerCase(); + if (!text || !Array.isArray(expected) || !expected.length) return 0; + + const sportsExpected = expected.some((cat) => ['tennis', 'football', 'nba'].includes(cat)); + if (sportsExpected && ESPORTS_BLOB_RE.test(text)) return 0; + + let score = 0; + for (const cat of expected) { + if (cat === 'weather' && /\btemperature\b|\bweather\b|°f|°c|\bhigh temp\b/.test(text)) score += 5; + if (cat === 'politics' && /\bpresident\b|\belection\b|\bnominee\b|\bsenate\b|\bgovernor\b|\bparliament\b/.test(text)) { + score += 5; + } + if (cat === 'macro_fed' && /\bfed\b|\bfomc\b|\binterest rate\b|\bbps\b/.test(text)) score += 5; + if (cat === 'football' && ( + /\bfootball\b|\bsoccer\b|\bpremier league\b|\buefa\b|\bfifa\b|\bepl\b|\bucl\b|\bla liga\b|\bserie a\b|\bbundesliga\b/ + ).test(text)) { + score += 5; + } + if (cat === 'tennis' && (/\btennis\b|\batp\b|\bwta\b|\bwimbledon\b|\bus open\b|\broland garros\b/).test(text)) { + score += 5; + } + if (cat === 'nba' && (/\bnba\b|\bbasketball\b|\bwnba\b/).test(text)) score += 5; + } + return score; +} + +function buildScenarioBuyerSummaryZh(spec, base, expectedOk) { + const cat = base.category || 'unknown'; + const tradability = base.tradability || 'unknown'; + const depth = base.category_depth || 'core_only'; + const plugin = base.category_plugin; + const thesis = plugin?.central_thesis + || plugin?.market_implied_shape?.central_thesis + || base.base_case + || '无中心论题'; + if (!expectedOk) { + return `${spec.zh_name} SKU:检测到品类 ${cat}(期望 ${spec.expected.join('/')}),深度 ${depth},可交易性 ${tradability}。建议换匹配市场或改用通用 /pm-event-readout。`; + } + return `${spec.zh_name}:品类 ${cat},深度 ${depth},可交易性 ${tradability}。${String(thesis).slice(0, 120)} 非下单建议。`; +} + +function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-semantic-match.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-semantic-match.mjs new file mode 100644 index 00000000..2a3e3cd0 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-semantic-match.mjs @@ -0,0 +1,145 @@ +// Lightweight semantic entity matching for scenario discovery. +// Keeps discovery honest for queries like "Lakers" without broadening SKU count. + +const STOP_WORDS = new Set([ + 'a', 'an', 'and', 'are', 'at', 'be', 'by', 'for', 'from', 'in', 'is', 'of', + 'on', 'or', 'the', 'to', 'vs', 'v', 'will', 'win', 'winner', 'market', + 'match', 'game', 'event', 'readout', 'card', 'who', 'what', 'when' +]); + +const GENERIC_CATEGORY_TERMS = new Set([ + 'nba', 'basketball', 'wnba', + 'football', 'soccer', 'epl', 'ucl', 'premier', 'league', 'champions', + 'tennis', 'atp', 'wta', + 'fed', 'fomc', 'federal', 'reserve', 'interest', 'rates', 'rate', 'cut', 'hike', + 'politics', 'political', 'election', 'president', 'senate', 'governor', + 'weather', 'temperature', 'temp' +]); + +const ENTITY_ALIASES = [ + ['lakers', 'la lakers', 'los angeles lakers', 'lal'], + ['clippers', 'la clippers', 'los angeles clippers', 'lac'], + ['lebron', 'lebron james'], + ['celtics', 'boston celtics'], + ['knicks', 'new york knicks', 'ny knicks', 'nyk'], + ['warriors', 'golden state warriors', 'gsw'], + ['arsenal'], + ['man city', 'manchester city'], + ['man united', 'man utd', 'manchester united'], + ['chelsea'], + ['liverpool'], + ['nyc', 'new york', 'new york city'], + ['fomc', 'fed', 'federal reserve'] +]; + +const ALIAS_GROUPS = ENTITY_ALIASES.map((aliases, index) => ({ + id: `alias_${index}`, + aliases, + tokens: unique(aliases.flatMap(simpleTokens)) +})); + +export function extractSemanticTokens(text) { + const baseTokens = simpleTokens(text).filter((token) => !STOP_WORDS.has(token)); + const out = new Set(baseTokens); + const normalized = normalize(text); + for (const group of ALIAS_GROUPS) { + if (group.aliases.some((alias) => containsPhrase(normalized, alias))) { + for (const token of group.tokens) out.add(token); + out.add(group.id); + } + } + return [...out].filter((token) => token.length > 1); +} + +export function queryRequiresEntityMatch(query) { + const raw = String(query ?? '').trim(); + if (!raw) return false; + const normalized = normalize(raw); + const tokens = simpleTokens(raw).filter((token) => !STOP_WORDS.has(token)); + if (!tokens.length) return false; + + const hasAlias = ALIAS_GROUPS.some((group) => ( + // Treat bare Fed/FOMC as category-generic even though it is an alias group. + group.aliases.some((alias) => containsPhrase(normalized, alias)) + && !group.aliases.every((alias) => ['fed', 'fomc', 'federal reserve'].includes(alias)) + )); + if (hasAlias) return true; + + const nonGeneric = tokens.filter((token) => !GENERIC_CATEGORY_TERMS.has(token)); + if (!nonGeneric.length) return false; + + // Avoid treating arbitrary fallback phrases as entities. Multi-token titlecase + // inputs often are specific names ("Los Angeles", "Candidate A"). + const titlecaseWords = raw.match(/\b[A-Z][a-z]+(?:\b|$)/g) || []; + return titlecaseWords.length >= 2; +} + +export function scoreSemanticMatch({ query, title, slug, eventTitle } = {}) { + const queryTokens = extractSemanticTokens(query); + if (!queryTokens.length) return 0; + const candidateText = [title, slug, eventTitle].filter(Boolean).join(' '); + const candidateTokens = new Set(extractSemanticTokens(candidateText)); + if (!candidateTokens.size) return 0; + + let overlap = 0; + let aliasOverlap = 0; + for (const token of queryTokens) { + if (!candidateTokens.has(token)) continue; + overlap += 1; + if (token.startsWith('alias_')) aliasOverlap += 1; + } + + const meaningfulQuery = queryTokens.filter((token) => !GENERIC_CATEGORY_TERMS.has(token)); + const denominator = Math.max(1, meaningfulQuery.length || queryTokens.length); + const coverage = Math.min(1, overlap / denominator); + const exactPhraseBonus = hasExactAliasPhrase(query, candidateText) ? 2 : 0; + const aliasBonus = aliasOverlap > 0 ? 3 : 0; + + return round2(Math.min(10, coverage * 6 + aliasBonus + exactPhraseBonus)); +} + +function hasExactAliasPhrase(query, candidateText) { + const q = normalize(query); + const c = normalize(candidateText); + for (const group of ALIAS_GROUPS) { + const qHit = group.aliases.some((alias) => containsPhrase(q, alias)); + if (!qHit) continue; + if (group.aliases.some((alias) => containsPhrase(c, alias))) return true; + } + return false; +} + +function containsPhrase(normalizedText, phrase) { + const normalizedPhrase = normalize(phrase); + return new RegExp(`(?:^|\\s)${escapeRegExp(normalizedPhrase)}(?:\\s|$)`).test(normalizedText); +} + +function simpleTokens(text) { + return normalize(text) + .split(/\s+/) + .map((token) => token.trim()) + .filter(Boolean); +} + +function normalize(text) { + return String(text ?? '') + .toLowerCase() + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function unique(values) { + return [...new Set(values)]; +} + +function round2(value) { + return Math.round(value * 100) / 100; +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-trade-preflight.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-trade-preflight.mjs new file mode 100644 index 00000000..13434c83 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-trade-preflight.mjs @@ -0,0 +1,259 @@ +// PM Trade Preflight (pm_trade_preflight) — read-only eligible/watch/skip gate before +// a prediction-market order. Uses public Polymarket Gamma market metadata only. +// `eligible` means mechanical checks passed — NOT a buy/sell tip and NOT order routing. + +import { + fetchMarket, + resolveMarketRef, + clamp, + round2, + toNumber +} from './pm-gamma-market.mjs'; + +const SERVICE_ID = 'pm_trade_preflight'; + +const MIN_VOLUME_24H_USD = 5_000; +const EXTREME_PRICE_LOW = 0.08; +const EXTREME_PRICE_HIGH = 0.92; +const MAX_SPREAD = 0.06; +const SIZE_VS_VOLUME_RATIO = 0.05; + +const STANDARD_CAVEATS = [ + 'Read-only preflight gate. Not investment advice; does not place, cancel, or route orders.', + 'Heuristic checks on liquidity, price zone, and spread only — not a full event readout.', + 'Caller retains all risk limits and manual approval before any real-money action.' +]; + +export async function assessPmTradePreflightLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const side = normalizeSide(input.side); + const sizeUsd = parseOptionalUsd(input.size_usd ?? input.sizeUsd); + const marketRef = resolveMarketRef(input); + + const market = await fetchMarket(fetchImpl, marketRef); + if (!market) { + throw new Error('Market not found for the provided slug, condition_id, or market_url'); + } + + const evaluation = evaluatePreflight(market, side, sizeUsd); + const decisionLite = buildDecisionCardLite(market, side, evaluation); + + return { + schema_version: '0.2', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + market_url: input.market_url ?? null, + condition_id: marketRef.condition_id, + slug: marketRef.slug, + side, + size_usd: sizeUsd + }, + market: { + condition_id: market.condition_id, + slug: market.slug, + title: market.title, + active: market.active, + closed: market.closed, + volume_24h_usd: market.volume_24hr, + best_bid: market.best_bid, + best_ask: market.best_ask, + spread: market.spread, + outcomes: market.outcomes, + outcome_prices: market.outcome_prices + }, + action: evaluation.action, + confidence: evaluation.confidence, + reasons: evaluation.reasons, + risk_flags: evaluation.risk_flags, + side_price: evaluation.side_price, + decision_card_lite: decisionLite, + caveats: [...STANDARD_CAVEATS], + next_gate: 'Leo_manual_order_approval_required', + source: { + provider: 'polymarket_gamma_public_api', + min_volume_24h_usd: MIN_VOLUME_24H_USD, + extreme_price_band: [EXTREME_PRICE_LOW, EXTREME_PRICE_HIGH] + } + }; +} + +export function buildPmTradePreflightFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + market_url: input?.market_url ?? null, + condition_id: input?.condition_id ?? null, + slug: input?.slug ?? 'demo-market', + side: normalizeSide(input?.side), + size_usd: parseOptionalUsd(input?.size_usd) + }, + market: { + condition_id: 'demo', + slug: 'demo-market', + title: 'Demo market (live data unavailable)', + active: true, + closed: false, + volume_24h_usd: 25_000, + outcomes: ['Yes', 'No'], + outcome_prices: [0.42, 0.58] + }, + action: 'watch', + confidence: 0.4, + reasons: ['Live Polymarket lookup unavailable; demo preflight only.'], + risk_flags: ['live_data_unavailable'], + side_price: 0.42, + decision_card_lite: { + fair_prob_range: [0.35, 0.5], + max_entry: 0.45, + price_status: 'unknown_demo', + edge_after_fees_buffer: null, + best_alternative_market: null, + decision_mode: 'demo_only' + }, + caveats: [...STANDARD_CAVEATS, 'Demo mode: do not trade on this response.'], + next_gate: 'Leo_manual_order_approval_required', + source: { provider: 'static_fallback' } + }; +} + +function evaluatePreflight(market, side, sizeUsd) { + const reasons = []; + const risk_flags = []; + let action = 'eligible'; + let confidence = 0.72; + + if (market.closed || !market.active) { + return { + action: 'skip', + confidence: 0.9, + reasons: ['Market is closed or inactive.'], + risk_flags: ['market_closed_or_inactive'], + side_price: getSidePrice(market, side) + }; + } + + const sidePrice = getSidePrice(market, side); + if (sidePrice === null) { + return { + action: 'skip', + confidence: 0.85, + reasons: [`Could not resolve price for side "${side}".`], + risk_flags: ['missing_side_price'], + side_price: null + }; + } + + if (market.volume_24hr < MIN_VOLUME_24H_USD) { + risk_flags.push('low_liquidity'); + reasons.push(`24h volume $${Math.round(market.volume_24hr)} is below $${MIN_VOLUME_24H_USD} threshold.`); + action = 'watch'; + confidence -= 0.15; + } + + if (sidePrice <= EXTREME_PRICE_LOW || sidePrice >= EXTREME_PRICE_HIGH) { + risk_flags.push('extreme_implied_probability'); + reasons.push(`Side price ${round2(sidePrice)} is in an extreme zone for new entry.`); + action = 'watch'; + confidence -= 0.12; + } + + if (market.spread !== null && market.spread > MAX_SPREAD) { + risk_flags.push('wide_spread'); + reasons.push(`Bid/ask spread ~${round2(market.spread)} looks wide.`); + action = 'watch'; + confidence -= 0.1; + } + + if (sizeUsd !== null && market.volume_24hr > 0 && sizeUsd > market.volume_24hr * SIZE_VS_VOLUME_RATIO) { + risk_flags.push('size_large_vs_daily_volume'); + reasons.push(`Requested size $${sizeUsd} is large vs 24h volume $${Math.round(market.volume_24hr)}.`); + action = 'watch'; + confidence -= 0.1; + } + + if (action === 'eligible') { + reasons.push('Liquidity, price zone, and spread checks passed heuristic preflight (eligible ≠ buy tip).'); + } + + return { + action, + confidence: round2(clamp(confidence, 0.35, 0.9)), + reasons, + risk_flags, + side_price: round2(sidePrice) + }; +} + +/** + * Decision-card-lite: mechanical fair band + entry ceiling from price/liquidity + * heuristics only. Not a full pm-decision-card (no account/exposure/sizing). + */ +function buildDecisionCardLite(market, side, evaluation) { + const sidePrice = evaluation.side_price; + if (sidePrice === null || sidePrice === undefined) { + return { + fair_prob_range: null, + max_entry: null, + price_status: 'missing_price', + edge_after_fees_buffer: null, + best_alternative_market: null, + decision_mode: evaluation.action + }; + } + + // Without an external model, treat a narrow band around mid as a "watch" fair zone, + // and require a small edge buffer before considering entry. + const halfBand = 0.04; + const feeBuffer = 0.02; + const fairLow = round2(clamp(sidePrice - halfBand, 0.01, 0.99)); + const fairHigh = round2(clamp(sidePrice + halfBand, 0.01, 0.99)); + const maxEntry = round2(clamp(sidePrice - feeBuffer, 0.01, 0.99)); + + let price_status = 'at_market'; + if (evaluation.risk_flags.includes('extreme_implied_probability')) price_status = 'extreme_zone'; + else if (evaluation.action === 'eligible') price_status = 'mechanically_ok_not_a_buy'; + else if (evaluation.action === 'watch') price_status = 'watch_constraints'; + else if (evaluation.action === 'skip') price_status = 'skip'; + + const otherOutcomes = (market.outcomes || []) + .map((name, idx) => ({ + outcome: name, + price: market.outcome_prices?.[idx] ?? null + })) + .filter((row) => String(row.outcome).toLowerCase() !== normalizeSide(side)); + + return { + fair_prob_range: [fairLow, fairHigh], + max_entry: maxEntry, + price_status, + edge_after_fees_buffer: feeBuffer, + best_alternative_market: otherOutcomes[0] ?? null, + decision_mode: evaluation.action, + note: 'fair_prob_range is a mechanical band around the live price, not a model-implied fair value.' + }; +} + +function getSidePrice(market, side) { + const normalized = normalizeSide(side); + const idx = market.outcomes.findIndex((outcome) => String(outcome).toLowerCase() === normalized); + if (idx < 0) return null; + const price = market.outcome_prices[idx]; + return Number.isFinite(price) ? price : null; +} + +function normalizeSide(side) { + const raw = String(side ?? 'yes').trim().toLowerCase(); + if (raw === 'no') return 'no'; + return 'yes'; +} + +function parseOptionalUsd(value) { + if (value === null || value === undefined || value === '') return null; + const n = Number(value); + return Number.isFinite(n) && n > 0 ? round2(n) : null; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-updown-readout.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-updown-readout.mjs new file mode 100644 index 00000000..3358afc3 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-updown-readout.mjs @@ -0,0 +1,131 @@ +// PM Up/Down Readout — productizes polymarket-toolkit `pm updown`. +// Crypto up/down event surface with resolution-source pitfalls. Read-only. + +const SERVICE_ID = 'pm_updown_readout'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +const FETCH_TIMEOUT_MS = 12000; + +const STANDARD_CAVEATS = [ + 'Read-only Gamma up/down event surface from polymarket-toolkit pm updown.', + 'No universal priceToBeat — verify resolutionSource + official rules before any trade.', + 'Do not treat CLOB mid as oracle target without your own window open record.', + 'No orders, no wallet custody.' +]; + +/** + * @param {object} input + * @param {string} [input.event_slug] + * @param {string} [input.slug] + * @param {string} [input.query] discovery hint e.g. btc updown + */ +export async function assessPmUpdownReadoutLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + let slug = String(input.event_slug ?? input.slug ?? '').trim(); + const query = String(input.query ?? input.q ?? '').trim(); + + if (!slug && query) { + slug = await discoverUpdownSlug(fetchImpl, query); + } + if (!slug) { + throw new Error('pm-updown-readout requires event_slug/slug or query (e.g. "btc updown")'); + } + + const events = await fetchJson(fetchImpl, `${GAMMA_BASE}/events?slug=${encodeURIComponent(slug)}`).catch(() => []); + const event = Array.isArray(events) ? events[0] : null; + if (!event) throw new Error(`No Gamma event for slug: ${slug}`); + + const markets = (event.markets || []).map((m) => ({ + question: m.question ?? null, + slug: m.slug ?? null, + condition_id: m.conditionId ?? null, + group_item_title: m.groupItemTitle ?? null, + outcome_prices: m.outcomePrices ?? null, + best_bid: m.bestBid ?? null, + best_ask: m.bestAsk ?? null, + spread: m.spread ?? null, + resolution_source: m.resolutionSource ?? event.resolutionSource ?? null, + end_date: m.endDate ?? event.endDate ?? null + })); + + const generated_at = new Date().toISOString(); + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at, + input: { event_slug: slug, query: query || null }, + event: { + title: event.title ?? slug, + slug, + end_date: event.endDate ?? null, + resolution_source: event.resolutionSource ?? null, + market_count: markets.length + }, + markets: markets.slice(0, 16), + pitfalls: [ + 'No universal priceToBeat field — verify resolutionSource + official rules', + 'Do not use CLOB mid as oracle target without recording your own window open', + 'See polymarket-toolkit docs/crypto-updown-price-source.md' + ], + buyer_summary_zh: `涨跌盘读出:${event.title ?? slug} · ${markets.length} 个子盘 · 结算源=${String(event.resolutionSource ?? '见规则').slice(0, 60)}。先核结算定义再谈价。`, + buyer_summary_en: `Up/down readout: ${event.title ?? slug}; ${markets.length} markets; check resolutionSource before pricing.`, + value_loop: { + why_pay_again: 'Up/down windows and books roll; re-fetch each window.', + stale_after_minutes: 3, + paid_value_tier: 'toolkit_updown', + oss_lineage: 'polymarket-toolkit pm updown', + llm_api_key_required: false + }, + hard_gate: 'no_orders_verify_resolution_first', + caveats: STANDARD_CAVEATS, + next_gate: 'Human_verify_resolution_source_then_optional_trade_preflight', + source: { provider: 'polymarket_gamma_public_api', method: 'toolkit_pm_updown' } + }; +} + +export function buildPmUpdownReadoutFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + markets: [], + buyer_summary_zh: '涨跌盘读出回退:上游不可用。', + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + source: { provider: 'static_fallback' }, + input + }; +} + +async function discoverUpdownSlug(fetchImpl, query) { + const q = /updown|up-down|涨跌/i.test(query) ? query : `${query} updown`; + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(q)}&events_status=active&limit_per_type=12` + ).catch(() => ({})); + const events = Array.isArray(result?.events) ? result.events : []; + const scored = events + .map((e) => { + const blob = `${e.title || ''} ${e.slug || ''}`.toLowerCase(); + let score = 0; + if (/updown|up-down|up down/.test(blob)) score += 5; + if (/btc|bitcoin|eth|ethereum|sol/.test(blob)) score += 2; + if (/15m|1h|hourly|daily/.test(blob)) score += 1; + return { slug: e.slug, score, volume: Number(e.volume24hr || 0) }; + }) + .filter((x) => x.slug && x.score > 0) + .sort((a, b) => b.score - a.score || b.volume - a.volume); + return scored[0]?.slug ?? null; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }); + if (!response.ok) throw new Error(`Upstream ${response.status}`); + return await response.json(); + } finally { + clearTimeout(timer); + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-wallet-report.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-wallet-report.mjs new file mode 100644 index 00000000..b01fa9c5 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/pm-wallet-report.mjs @@ -0,0 +1,182 @@ +// PM Wallet Report — one-pager composing toolkit profile + brier + pnl quick audit. +// Lineage: polymarket-toolkit Drawer A (research an address). Read-only. + +import { assessPmProfileLive, buildPmProfileFallback } from './pm-profile.mjs'; +import { assessPmBrierLive, buildPmBrierFallback } from './pm-brier.mjs'; +import { assessPmPnlAuditLive, buildPmPnlAuditFallback } from './pm-pnl-audit.mjs'; + +const SERVICE_ID = 'pm_wallet_report'; + +const STANDARD_CAVEATS = [ + 'Composed read-only report from polymarket-toolkit surfaces (profile + brier + pnl quick).', + 'Quick PnL is not full cashflow replay — call /pm-pnl-audit mode=full when needed.', + 'No wallet custody, no orders.' +]; + +/** + * @param {object} input + * @param {string} [input.address] + * @param {string} [input.username] + * @param {'quick'|'full'} [input.pnl_mode='quick'] + */ +export async function assessPmWalletReportLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const raw = String(input.address ?? input.wallet ?? input.username ?? input.query ?? '').trim(); + if (!raw) throw new Error('pm-wallet-report requires address or username.'); + + const pnlMode = String(input.pnl_mode ?? input.mode ?? 'quick').toLowerCase() === 'full' ? 'full' : 'quick'; + + const [profileR, brierR, pnlR] = await Promise.allSettled([ + assessPmProfileLive(input, { fetchImpl }), + assessPmBrierLive(input, { fetchImpl }), + assessPmPnlAuditLive({ ...input, mode: pnlMode }, { fetchImpl }) + ]); + + const profile = profileR.status === 'fulfilled' ? profileR.value : null; + const brier = brierR.status === 'fulfilled' ? brierR.value : null; + const pnl = pnlR.status === 'fulfilled' ? pnlR.value : null; + + if (!profile && !brier && !pnl) { + throw new Error('All wallet report layers failed upstream'); + } + + const address = profile?.input?.address || pnl?.input?.address || null; + const action = chooseCompositeAction(pnl, brier); + const generated_at = new Date().toISOString(); + const evidenceState = buildEvidenceState({ profile, brier, pnl }); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at, + input: { + query: raw, + address, + pnl_mode: pnlMode + }, + layers: { + profile: profile + ? { + pnl_7d: profile.profile?.pnl_7d_usdt ?? profile.pnl_7d ?? profile.leaderboard_pnl_7d ?? null, + open_positions: profile.profile?.open_positions_sampled ?? profile.open_positions ?? profile.positions_sampled ?? null, + buyer_summary_zh: profile.buyer_summary_zh + } + : { error: profileR.reason?.message || 'unavailable' }, + brier: brier + ? { + brier: brier.brier ?? brier.score ?? null, + rating: brier.rating ?? null, + settled_sample: brier.settled_markets ?? brier.settled_count ?? brier.sample_size ?? null, + buyer_summary_zh: brier.buyer_summary_zh + } + : { error: brierR.reason?.message || 'unavailable' }, + pnl_audit: pnl + ? { + mode: pnl.mode, + divergence_verdict: pnl.divergence_verdict, + action: pnl.action, + leaderboard_profit: pnl.leaderboard_profit?.amount_usd ?? null, + cashflow_complete: pnl.cashflow_replay?.complete ?? null, + buyer_summary_zh: pnl.buyer_summary_zh + } + : { error: pnlR.reason?.message || 'unavailable' } + }, + composite_action: action, + evidence_state: evidenceState, + consumer_contract: buildConsumerContract({ action, evidenceState, pnlMode }), + buyer_summary_zh: buildZh({ address, profile, brier, pnl, action }), + buyer_summary_en: buildEn({ address, profile, brier, pnl, action }), + value_loop: { + why_pay_again: 'Wallet positions, LB PnL and calibration move; re-run before copying.', + stale_after_minutes: 15, + paid_value_tier: 'toolkit_wallet_report', + oss_lineage: 'polymarket-toolkit Drawer A: profile + brier + pnl', + llm_api_key_required: false + }, + caveats: STANDARD_CAVEATS, + next_gate: action === 'distrust_claims' + ? 'Do_not_copy_without_full_pnl_replay' + : 'Optional_pm_pnl_audit_mode_full', + source: { + provider: 'polymarket_public_api', + composed: ['pm_profile', 'pm_brier', 'pm_pnl_audit'] + } + }; +} + +export function buildPmWalletReportFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + layers: { + profile: buildPmProfileFallback(input), + brier: buildPmBrierFallback(input), + pnl_audit: buildPmPnlAuditFallback(input) + }, + composite_action: 'verify_manually', + buyer_summary_zh: '钱包一页纸回退:上游不可用。', + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + source: { provider: 'static_fallback' } + }; +} + +function buildEvidenceState({ profile, brier, pnl }) { + const available_layers = []; + if (profile) available_layers.push('profile'); + if (brier) available_layers.push('brier'); + if (pnl) available_layers.push('pnl_audit'); + const all = ['profile', 'brier', 'pnl_audit']; + const missing_layers = all.filter((name) => !available_layers.includes(name)); + const status = missing_layers.length === 0 ? 'live' : (available_layers.length > 0 ? 'degraded' : 'insufficient_evidence'); + return { status, available_layers, missing_layers, sufficient_for_action: status === 'live' && Boolean(pnl) }; +} + +function buildConsumerContract({ action, evidenceState, pnlMode }) { + const degraded = evidenceState.status !== 'live'; + return { + schema_version: '0.1', + decision: degraded ? 'do_not_autocopy' : action, + can_autocopy: !degraded && action === 'trust_for_copy' && pnlMode === 'full', + requires_human_review: degraded || action !== 'trust_for_copy', + readback_key: 'generated_at', + stale_after_minutes: 15, + follow_up: degraded ? 'retry_same_report_before_acting' : (action === 'distrust_claims' ? 'run_pm_pnl_audit_full_before_copying' : (pnlMode === 'quick' ? 'optional_pm_pnl_audit_full' : 'none')), + fail_closed_reason: degraded ? 'missing_layers:' + evidenceState.missing_layers.join(',') : null + }; +} + +function chooseCompositeAction(pnl, brier) { + if (pnl?.action === 'distrust_claims') return 'distrust_claims'; + // Only full cashflow trust_for_copy may lift composite; quick_triage_ok stays manual. + if (pnl?.action === 'trust_for_copy' && brier?.rating === 'good') return 'trust_for_copy'; + if (pnl?.action === 'trust_for_copy') return 'trust_with_calibration_check'; + if (pnl?.action === 'quick_triage_ok') return 'verify_manually'; + return 'verify_manually'; +} + +function buildZh({ address, profile, brier, pnl, action }) { + const who = shorten(address); + const lb = pnl?.leaderboard_profit?.amount_usd + ?? profile?.profile?.pnl_7d_usdt + ?? profile?.pnl_7d?.amount + ?? 'n/a'; + const rating = brier?.rating ?? 'n/a'; + const verdict = pnl?.divergence_verdict ?? 'n/a'; + return `钱包一页纸 ${who}:PnL层 verdict=${verdict};Brier=${rating};LB≈${lb};composite=${action}。来自 toolkit 组合,非跟单建议。`; +} + +function buildEn({ address, profile, brier, pnl, action }) { + const who = shorten(address); + const rating = brier?.rating ?? 'n/a'; + const verdict = pnl?.divergence_verdict ?? 'n/a'; + return `Wallet report ${who}: pnl verdict=${verdict}; brier=${rating}; composite=${action}. Toolkit compose; not copy advice.`; +} + +function shorten(address) { + const value = String(address ?? ''); + if (!value.startsWith('0x') || value.length < 12) return value || 'wallet'; + return `${value.slice(0, 6)}...${value.slice(-4)}`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/publish-readiness.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/publish-readiness.mjs new file mode 100644 index 00000000..cb8cc3da --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/publish-readiness.mjs @@ -0,0 +1,176 @@ +// Publish Readiness — combines Content Slop Check + Content Verify Claims into +// one pre-publish gate: ready / edit_first / block. +// Deterministic, no rewrite, no web fetch, no account mutation. + +import { assessContentSlopCheck } from './content-slop-check.mjs'; +import { assessContentVerifyClaims } from './content-verify-claims.mjs'; + +const SERVICE_ID = 'publish_readiness'; + +const STANDARD_CAVEATS = [ + 'Combines rule-based slop detection + claim/source overlap — not multi-model review.', + 'Does not publish, schedule, rewrite, or mutate any account.', + 'Caller must supply source excerpts when claims are provided; URLs alone are not fetched.' +]; + +/** + * @param {object} input + * @param {string} [input.text] + * @param {string} [input.content] + * @param {string} [input.draft] + * @param {string[]} [input.claims] + * @param {Array<{text?: string, url?: string}>} [input.sources] + * @param {number} [input.max_slop_score] default 55 + */ +export function assessPublishReadiness(input = {}) { + const text = String(input.text ?? input.content ?? input.draft ?? '').trim(); + if (!text) { + throw new Error('text (draft content) is required'); + } + + const maxSlop = clampInt(input.max_slop_score, 10, 90, 55); + const claims = normalizeClaims(input.claims ?? input.claim); + const sources = input.sources ?? input.source; + + const slop = assessContentSlopCheck({ text }); + + let verify = null; + let verify_skipped = false; + if (claims.length) { + const sourceList = Array.isArray(sources) ? sources : (sources ? [sources] : []); + const hasTextSource = sourceList.some((s) => s && String(s.text ?? '').trim()); + if (!hasTextSource) { + throw new Error('claims[] provided but sources[].text excerpts are missing (URLs alone are not fetched).'); + } + verify = assessContentVerifyClaims({ claims, sources: sourceList }); + } else { + verify_skipped = true; + } + + const blockers = []; + const edit_reasons = []; + + if (slop.verdict === 'sloppy' || slop.slop_score_0_100 >= maxSlop) { + blockers.push(`slop_score ${slop.slop_score_0_100} ≥ ${maxSlop} (or verdict=sloppy)`); + } else if (slop.verdict === 'needs_edit') { + edit_reasons.push(`slop needs_edit (score ${slop.slop_score_0_100})`); + } + + if (slop.slop_flags?.some((f) => f.id === 'as_an_ai')) { + blockers.push('model self-reference (as_an_ai) must be removed'); + } + + if (verify) { + if (verify.verdict === 'fail' || (verify.unsupported?.length ?? 0) > 0 || (verify.conflicts?.length ?? 0) > 0) { + blockers.push(`claims verify=${verify.verdict} (unsupported=${verify.unsupported?.length ?? 0}, conflicts=${verify.conflicts?.length ?? 0})`); + } else if (verify.verdict === 'needs_review') { + edit_reasons.push('claims need_review before publish'); + } + } else if (looksLikeFactHeavy(text) && verify_skipped) { + edit_reasons.push('draft looks fact-heavy but claims[]/sources[] were omitted'); + } + + let action = 'ready'; + if (blockers.length) action = 'block'; + else if (edit_reasons.length) action = 'edit_first'; + + const buyer_summary_zh = buildBuyerSummaryZh(action, slop, verify, blockers, edit_reasons); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + text_chars: text.length, + claim_count: claims.length, + verify_skipped, + max_slop_score: maxSlop + }, + action, + buyer_summary_zh, + value_loop: { + why_pay_again: 'Each draft is different; re-run before every publish attempt.', + stale_after_minutes: null, + best_used_in: 'content_publish_gate_before_post', + paid_value_tier: 'A_repeat_workflow', + fulfillment: 'edge_on_demand_no_llm' + }, + blockers, + edit_reasons, + slop: { + verdict: slop.verdict, + slop_score_0_100: slop.slop_score_0_100, + slop_flags: slop.slop_flags, + suggested_actions: slop.suggested_actions + }, + verify: verify + ? { + verdict: verify.verdict, + consensus: verify.consensus, + supported_count: verify.supported?.length ?? 0, + unsupported_count: verify.unsupported?.length ?? 0, + needs_review_count: verify.needs_review?.length ?? 0, + conflicts_count: verify.conflicts?.length ?? 0 + } + : null, + caveats: [...STANDARD_CAVEATS], + next_gate: action === 'ready' + ? 'Human_spot_check_then_publish' + : 'Fix_blockers_or_edit_reasons_then_rerun', + source: { + method: 'compose_content_slop_check_plus_content_verify_claims', + oss_lineage: 'talk-human / content-verify rules productized' + } + }; +} + +export function buildPublishReadinessFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { text_chars: 0, claim_count: 0, verify_skipped: true, max_slop_score: 55 }, + action: 'edit_first', + buyer_summary_zh: '演示回退:请提供 text(及可选 claims/sources)后再跑发布就绪闸门。', + blockers: [], + edit_reasons: ['demo_fallback'], + slop: null, + verify: null, + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Fix_blockers_or_edit_reasons_then_rerun', + source: { method: 'static_fallback' } + }; +} + +function buildBuyerSummaryZh(action, slop, verify, blockers, edit_reasons) { + const slopBit = `注水分 ${slop.slop_score_0_100}(${slop.verdict})`; + const verifyBit = verify + ? `断言核查 ${verify.verdict}(支持${verify.supported?.length ?? 0}/不支持${verify.unsupported?.length ?? 0})` + : '未提供 claims,跳过断言核查'; + if (action === 'ready') { + return `可发布(仍建议人工扫一眼)。${slopBit};${verifyBit}。`; + } + if (action === 'block') { + return `先别发:${blockers.join(';')}。${slopBit};${verifyBit}。`; + } + return `先改再发:${edit_reasons.join(';')}。${slopBit};${verifyBit}。`; +} + +function looksLikeFactHeavy(text) { + const nums = (text.match(/\d+(?:\.\d+)?%?|\b(?:USDT|USD|\$)\s?\d+/gi) || []).length; + return nums >= 2 || /根据|数据显示|官方|销量|成交/.test(text); +} + +function normalizeClaims(value) { + if (value == null) return []; + const items = Array.isArray(value) ? value : [value]; + return items.map((c) => String(c).trim()).filter(Boolean); +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-cockpit.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-cockpit.mjs new file mode 100644 index 00000000..e3e2e592 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-cockpit.mjs @@ -0,0 +1,136 @@ +// Sports Cockpit — compose Sports Smart Money + Sports Upset into one card. +// World Cup is one league scope, not the product identity. + +import { assessSportsSmartMoneyLive } from './worldcup-smart-money-live.mjs'; +import { + assessSportsUpsetAlertLive, + buildSportsUpsetAlertFallback +} from './sports-upset-alert.mjs'; +import { assessWorldCupSmartMoney } from './worldcup-smart-money.mjs'; + +const SERVICE_ID = 'sports_cockpit'; + +const STANDARD_CAVEATS = [ + 'Composed sports prediction-market card only. Not betting advice; does not place or route orders.', + 'Smart-money and upset legs share the same public Polymarket scan heuristics and can be wrong or stale.', + 'No wallet custody, no trade execution.' +]; + +export async function assessSportsCockpitLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInt(input.limit, 1, 10, 5); + const scope = { + sport: input.sport ?? 'all', + league: input.league ?? null, + tag_slug: input.tag_slug ?? null, + query: input.query ?? input.market ?? 'all', + max_prob: input.max_prob ?? input.max_implied_probability ?? 0.35, + limit + }; + + const [smartResult, upsetResult] = await Promise.allSettled([ + assessSportsSmartMoneyLive(scope, { fetchImpl }), + assessSportsUpsetAlertLive(scope, { fetchImpl }) + ]); + + const smart = smartResult.status === 'fulfilled' + ? smartResult.value + : assessWorldCupSmartMoney(scope); + const upset = upsetResult.status === 'fulfilled' + ? upsetResult.value + : buildSportsUpsetAlertFallback(scope); + + if (smartResult.status === 'rejected' && upsetResult.status === 'rejected') { + throw new Error('Both sports smart-money and upset upstream paths failed'); + } + + const signals = smart.signals || []; + const alerts = upset.upset_alerts || []; + const cohort = smart.wallet_cohort || upset.wallet_cohort || []; + const action = alerts.length + ? 'upset_watch' + : (signals.length ? 'follow_smart_money_review' : 'no_signal'); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'live', + generated_at: new Date().toISOString(), + input: scope, + action, + buyer_summary_zh: buildBuyerSummaryZh(action, scope, signals, alerts, cohort), + value_loop: { + why_pay_again: 'Large trades and upset flow change continuously; re-scan before acting on sports PM.', + stale_after_minutes: 10, + best_used_in: 'sports_pm_watchlist_or_pretrade_scan', + paid_value_tier: 'A_repeat_monitoring' + }, + smart_money: { + summary: smart.summary, + signal_count: signals.length, + signals: signals.slice(0, limit), + wallet_cohort: (cohort || []).slice(0, 5) + }, + upset: { + summary: upset.summary, + alert_count: alerts.length, + max_prob: upset.input?.max_prob ?? scope.max_prob, + upset_alerts: alerts.slice(0, limit) + }, + caveats: [ + ...STANDARD_CAVEATS, + ...(smartResult.status === 'rejected' + ? [`Smart-money leg degraded: ${smartResult.reason?.message || smartResult.reason}`] + : []), + ...(upsetResult.status === 'rejected' + ? [`Upset leg degraded: ${upsetResult.reason?.message || upsetResult.reason}`] + : []) + ], + next_gate: 'Use_pm_trade_preflight_before_any_order', + source: { + method: 'compose_sports_smart_money_plus_sports_upset', + discovery: smart.source?.discovery || upset.source?.discovery || null + } + }; +} + +export function buildSportsCockpitFallback(input = {}) { + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + sport: input.sport ?? null, + league: input.league ?? null, + query: input.query ?? 'all', + limit: clampInt(input.limit, 1, 10, 5) + }, + action: 'no_signal', + buyer_summary_zh: '演示回退:实时体育扫描不可用。', + smart_money: null, + upset: null, + caveats: [...STANDARD_CAVEATS, 'Demo fallback.'], + next_gate: 'Use_pm_trade_preflight_before_any_order', + source: { method: 'static_fallback' } + }; +} + +function buildBuyerSummaryZh(action, scope, signals, alerts, cohort) { + const actionZh = { + upset_watch: '有冷门预警,优先看低概率侧', + follow_smart_money_review: '有聪明钱信号,先复核再跟', + no_signal: '当前范围无明显信号' + }[action] || action; + const label = [scope.sport, scope.league, scope.query].filter(Boolean).join('/') || 'sports'; + const cross = (cohort || []).filter((w) => w.cross_market).length; + return `体育副驾驶:${actionZh}(范围 ${label})。聪明钱 ${signals.length} 条 / 冷门 ${alerts.length} 条` + + (cross ? ` / 跨场钱包 ${cross}` : '') + + '。非投注建议。'; +} + +function clampInt(value, min, max, fallback) { + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-upset-alert.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-upset-alert.mjs new file mode 100644 index 00000000..32aebffb --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/sports-upset-alert.mjs @@ -0,0 +1,205 @@ +// Sports Upset Alert — generic sports/PM low-probability smart-money filter. +// World Cup Upset Alert is a thin legacy wrapper (league=world_cup). + +import { + resolveSportsMarkets, + resolveWorldCupMarkets, + scanMarketsForSmartMoney +} from './worldcup-smart-money-live.mjs'; + +const SERVICE_ID = 'sports_upset_alert'; +const LEGACY_SERVICE_ID = 'world_cup_upset_alert'; + +const MAX_UPSET_PROBABILITY = 0.35; +const DEEP_UPSET_PROBABILITY = 0.2; +const SCAN_CANDIDATES = 12; + +const STANDARD_CAVEATS = [ + 'Data and analytics only. Not investment advice, not betting advice, and not a guarantee of future returns.', + 'No wallet custody, no user funds, no trade execution, no order routing.', + 'Upset alerts are heuristic reads of recent large public trades on Polymarket: a profitable wallet buying the low-probability side can also be hedging, market-making, or wrong.', + 'seven-day PnL comes from the Polymarket 7d profit leaderboard; wallets absent from it are excluded from alerts (never estimated).', + 'World Cup is one sports scope — pass sport/league for EPL/UCL/tennis/NBA/etc.' +]; + +export async function assessWorldCupUpsetAlertLive(input = {}, options = {}) { + return assessSportsUpsetAlertLive({ + ...input, + sport: input.sport ?? 'football', + league: input.league ?? 'world_cup', + tag_slug: input.tag_slug ?? 'world-cup' + }, { ...options, serviceId: LEGACY_SERVICE_ID, legacyWorldCup: true }); +} + +export async function assessSportsUpsetAlertLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInteger(input.limit, 1, 10, 5); + const serviceId = options.serviceId ?? SERVICE_ID; + const maxProb = clampProbability( + input.max_prob ?? input.max_implied_probability ?? input.max_upset_probability, + 0.05, + 0.5, + MAX_UPSET_PROBABILITY + ); + const deepProb = Math.min(DEEP_UPSET_PROBABILITY, maxProb * 0.6); + + let markets; + let usedFallback; + let discovery = null; + + if (options.legacyWorldCup) { + const marketHint = normalizeText(input.market ?? input.market_id ?? input.query ?? 'all'); + const resolved = await resolveWorldCupMarkets(fetchImpl, marketHint); + markets = resolved.markets; + usedFallback = resolved.usedFallback; + discovery = resolved.discovery; + } else { + const resolved = await resolveSportsMarkets(fetchImpl, input); + markets = resolved.markets; + usedFallback = resolved.usedFallback; + discovery = resolved.discovery; + } + + const { scanned, enriched, wallet_cohort = [] } = await scanMarketsForSmartMoney( + fetchImpl, + markets, + SCAN_CANDIDATES + ); + + const alerts = enriched + .filter((signal) => isUpsetCandidate(signal, maxProb)) + .map((signal) => buildUpsetAlert(signal, deepProb)) + .sort((a, b) => b.confidence - a.confidence) + .slice(0, limit); + + const caveats = [...STANDARD_CAVEATS]; + if (discovery?.scope_expanded) { + caveats.push(`No active ${discovery.requested_scope} markets matched; expanded live discovery to ${discovery.effective_scope} sports markets.`); + } else if (usedFallback) { + caveats.push(options.legacyWorldCup + ? 'No active World Cup markets matched; fell back to Polymarket top-volume markets site-wide.' + : 'No active scoped sports markets matched; fell back to top-volume / search.'); + } + if (maxProb !== MAX_UPSET_PROBABILITY) { + caveats.push(`Caller overridden max_implied_probability=${maxProb} (default ${MAX_UPSET_PROBABILITY}).`); + } + + return { + schema_version: '0.3', + service_id: serviceId, + mode: 'live', + generated_at: new Date().toISOString(), + input: { + sport: input.sport ?? null, + league: input.league ?? null, + tag_slug: input.tag_slug ?? null, + query: input.query ?? input.market ?? input.market_id ?? 'all', + max_prob: maxProb, + limit + }, + buyer_summary_zh: buildBuyerSummaryZh(alerts, scanned, enriched, discovery), + buyer_summary_en: buildBuyerSummaryEn(alerts, scanned, enriched, discovery), + summary: buildSummary(alerts, scanned, enriched), + upset_alerts: alerts, + wallet_cohort: wallet_cohort.filter((w) => w.cross_market).slice(0, 5), + caveats, + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + provider: 'polymarket_public_api', + pipeline: 'sports_smart_money scan + upset filter', + filter: { + wallet_7d_pnl: '> 0 (leaderboard-confirmed profitable wallets only)', + action: 'new_position or increased_position (net buying)', + max_implied_probability: maxProb, + deep_upset_probability: deepProb + }, + discovery, + markets_scanned: scanned.map((market) => ({ + market_id: market.market_id, + condition_id: market.condition_id, + title: market.title + })) + } + }; +} + +export function buildWorldCupUpsetAlertFallback(input = {}) { + return buildSportsUpsetAlertFallback({ ...input, service_id: LEGACY_SERVICE_ID }); +} + +export function buildSportsUpsetAlertFallback(input = {}) { + return { + schema_version: '0.2', + service_id: input.service_id ?? SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + sport: input.sport ?? null, + league: input.league ?? null, + query: input.query ?? 'all', + limit: clampInteger(input.limit, 1, 10, 5) + }, + summary: 'Demo fallback — live sports upset scan unavailable.', + upset_alerts: [], + caveats: [...STANDARD_CAVEATS, 'Demo mode: empty alerts.'], + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { provider: 'static_fallback' } + }; +} + +function isUpsetCandidate(signal, maxProb = MAX_UPSET_PROBABILITY) { + const price = Number(signal.last_trade_price); + const pnl = signal.seven_day_pnl_usdt; + const action = signal.action; + if (!Number.isFinite(price) || price >= maxProb) return false; + if (pnl === null || pnl === undefined || Number(pnl) <= 0) return false; + if (action !== 'new_position' && action !== 'increased_position') return false; + return true; +} + +function buildUpsetAlert(signal, deepProb = DEEP_UPSET_PROBABILITY) { + const price = Number(signal.last_trade_price); + const deep = price < deepProb; + return { + ...signal, + upset_band: deep ? 'deep_upset' : 'upset', + implied_probability: price, + confidence: Number(signal.confidence ?? 0.5) + (deep ? 0.05 : 0) + }; +} + +function buildSummary(alerts, scanned, enriched) { + return `Scanned ${scanned.length} markets / ${enriched.length} smart-money candidates → ${alerts.length} upset alert(s).`; +} + +function buildBuyerSummaryZh(alerts, scanned, enriched, discovery) { + const summary = buildSummary(alerts, scanned, enriched); + if (discovery?.scope_expanded) { + return `请求范围 ${discovery.requested_scope} 当前无活跃市场,已自动扩展到 ${discovery.effective_scope} 体育市场并返回真实 upset 扫描。${summary}`; + } + return `已扫描真实 Polymarket 体育市场的低概率大额交易。${summary}`; +} + +function buildBuyerSummaryEn(alerts, scanned, enriched, discovery) { + const summary = buildSummary(alerts, scanned, enriched); + if (discovery?.scope_expanded) { + return `Requested ${discovery.requested_scope} had no active markets, so live discovery expanded to ${discovery.effective_scope} sports markets and returned a real upset scan. ${summary}`; + } + return `Scanned real Polymarket sports markets for low-probability large-trade entries. ${summary}`; +} + +function normalizeText(value) { + return String(value ?? '').trim().toLowerCase(); +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} + +function clampProbability(value, min, max, fallback) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/token-dd-verdict.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/token-dd-verdict.mjs new file mode 100644 index 00000000..de5d7b35 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/token-dd-verdict.mjs @@ -0,0 +1,404 @@ +// Token DD Verdict (token_dd_verdict) — Quick-tier rule-based research gate. +// Maps asset-dd participation buckets to a compact API verdict. Not LLM judging; +// optional DexScreener public lookup for EVM contract addresses only. + +const SERVICE_ID = 'token_dd_verdict'; +const DEXSCREENER_BASE = 'https://api.dexscreener.com/latest/dex/tokens'; +const FETCH_TIMEOUT_MS = 8000; + +const REFERRAL_PATTERNS = [ + /\brise\.rich\/ref\//i, + /\bref=[\w-]{4,}/i, + /\baffiliate\b/i, + /\binvite\.code\b/i, + /\bairdrop\b.*\bclaim\b/i +]; + +const EVM_ADDRESS = /\b(0x[a-fA-F0-9]{40})\b/; +const TICKER_ONLY = /^[A-Za-z][A-Za-z0-9]{1,14}$/; + +const STANDARD_CAVEATS = [ + 'Rule-based Standard-lite research gate. Not investment advice, not a security audit, not LLM-generated research.', + 'No wallet custody, no trade execution, no order routing.', + 'Still not Full institutional DD (asset-dd); honeypot/owner/unlocks need deeper escrow or human review.' +]; + +/** + * Live quick verdict. Throws when DexScreener is required but unreachable for a + * contract-only input with no other anchors. + */ +export async function assessTokenDdVerdictLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const raw = String(input.asset ?? input.token ?? input.query ?? '').trim(); + if (!raw) { + throw new Error('asset (ticker, contract address, or URL) is required'); + } + + const parsed = parseAssetInput(raw); + const pillars = buildPillarBaseline(); + const hardStops = []; + const reasons = []; + + if (parsed.referral_risk) { + applyPillar(pillars, 'referral_and_promo_risk', 'fail', 'Referral or promo wrapper detected before canonical asset id.'); + hardStops.push('referral_or_promo_wrapper'); + reasons.push('Input looks like a referral/promo link without a reliable tradable identifier.'); + } + + if (!parsed.canonical_id) { + applyPillar(pillars, 'identifier_clarity', 'fail', 'No canonical ticker or on-chain identifier extracted.'); + hardStops.push('missing_canonical_identifier'); + reasons.push('Could not extract a ticker or contract/mint address to research.'); + } else { + applyPillar(pillars, 'identifier_clarity', 'pass', `Canonical id: ${parsed.canonical_id} (${parsed.id_type}).`); + } + + let dexContext = null; + const confidenceGaps = []; + const hardVetoGaps = []; + + if (parsed.evm_contract) { + dexContext = await fetchDexScreenerPairs(fetchImpl, parsed.evm_contract).catch(() => null); + if (!dexContext) { + applyPillar(pillars, 'liquidity_and_market_presence', 'warn', 'DexScreener lookup failed or returned no pairs.'); + reasons.push('On-chain liquidity could not be verified from public DEX data.'); + confidenceGaps.push('dex_lookup_failed'); + } else if (!dexContext.pairs.length) { + applyPillar(pillars, 'liquidity_and_market_presence', 'fail', 'No DEX pairs found for contract.'); + hardStops.push('no_public_dex_liquidity'); + reasons.push('No public DEX liquidity pairs found for this contract.'); + } else { + const top = dexContext.pairs[0]; + applyPillar( + pillars, + 'liquidity_and_market_presence', + top.liquidity_usd >= 50_000 ? 'pass' : 'warn', + `Top pair liquidity ~$${Math.round(top.liquidity_usd).toLocaleString('en-US')} on ${top.dexId}/${top.chainId}.` + ); + if (top.liquidity_usd < 10_000) { + hardStops.push('very_low_liquidity'); + reasons.push('Top DEX pair liquidity is very low (<$10k).'); + } + + // Standard-lite: activity / age / volatility proxies from pair stats + applyPillar( + pillars, + 'market_activity', + top.volume_h24 >= 25_000 ? 'pass' : (top.volume_h24 >= 2_000 ? 'warn' : 'fail'), + `24h volume ~$${Math.round(top.volume_h24).toLocaleString('en-US')}.` + ); + if (top.volume_h24 < 500) { + hardStops.push('negligible_24h_volume'); + reasons.push('24h DEX volume is negligible.'); + } + + const ageHours = top.pair_created_at + ? Math.max(0, (Date.now() - top.pair_created_at) / 3_600_000) + : null; + applyPillar( + pillars, + 'venue_maturity', + ageHours === null ? 'neutral' : (ageHours >= 24 * 14 ? 'pass' : (ageHours >= 24 ? 'warn' : 'fail')), + ageHours === null + ? 'Pair age unavailable from DexScreener.' + : `Top pair age ~${Math.round(ageHours)}h.` + ); + if (ageHours !== null && ageHours < 6) { + hardStops.push('brand_new_pair'); + reasons.push('Top pair is extremely new (<6h); rug / sniper risk elevated.'); + } + + const absChange = Math.abs(top.price_change_h24 ?? 0); + applyPillar( + pillars, + 'volatility_proxy', + absChange >= 80 ? 'warn' : 'pass', + `24h price change ~${round1(top.price_change_h24)}%.` + ); + if (absChange >= 150) { + reasons.push('Extreme 24h price move — treat as speculative.'); + } + + // Holder concentration not available on DexScreener free path + applyPillar(pillars, 'holder_concentration_proxy', 'neutral', + 'Holder concentration / top-10% not available on this public path.'); + confidenceGaps.push('holder_distribution_unavailable'); + hardVetoGaps.push('no_honeypot_tax_owner_scan'); + } + } else if (parsed.id_type === 'ticker') { + applyPillar(pillars, 'liquidity_and_market_presence', 'neutral', 'Ticker-only scan; no contract-level liquidity check.'); + applyPillar(pillars, 'market_activity', 'neutral', 'Ticker-only — skip DEX activity.'); + applyPillar(pillars, 'venue_maturity', 'neutral', 'Ticker-only — skip pair age.'); + applyPillar(pillars, 'volatility_proxy', 'neutral', 'Ticker-only — skip pair volatility.'); + applyPillar(pillars, 'holder_concentration_proxy', 'neutral', 'Ticker-only — skip holders.'); + reasons.push('Major ticker symbol without contract — use contract address for deeper on-chain checks.'); + reasons.push('Ticker-only inputs are capped at research_position; conviction requires a contract-level scan.'); + confidenceGaps.push('ticker_only_no_contract'); + hardVetoGaps.push('no_contract_security_scan'); + } else { + applyPillar(pillars, 'liquidity_and_market_presence', 'neutral', 'Non-EVM or unresolved identifier; skips automated DEX scan.'); + applyPillar(pillars, 'market_activity', 'neutral', 'Skipped.'); + applyPillar(pillars, 'venue_maturity', 'neutral', 'Skipped.'); + applyPillar(pillars, 'volatility_proxy', 'neutral', 'Skipped.'); + applyPillar(pillars, 'holder_concentration_proxy', 'neutral', 'Skipped.'); + confidenceGaps.push('unresolved_identifier'); + } + + applyPillar(pillars, 'security_heuristics', parsed.evm_contract ? 'warn' : 'neutral', + parsed.evm_contract + ? 'Standard-lite still does not run honeypot/tax/owner permission scanners; contract presence + venue heuristics only.' + : 'No contract-level security scan without an EVM address.'); + if (parsed.evm_contract) hardVetoGaps.push('no_honeypot_tax_owner_scan'); + + applyPillar(pillars, 'narrative_hype_risk', + parsed.referral_risk ? 'fail' : (/\b(moon|100x|gem|alpha group)\b/i.test(raw) ? 'warn' : 'pass'), + parsed.referral_risk ? 'Promo/referral language in input.' : 'No obvious hype phrases in raw input.'); + + applyPillar(pillars, 'tokenomics_unlock_gap', 'neutral', + 'Unlock schedule / emissions not checked in this endpoint — treat as unknown.'); + confidenceGaps.push('tokenomics_unlocks_unchecked'); + + const score = scoreFromPillars(pillars, hardStops); + let verdict_bucket = bucketFromScore(score, hardStops); + // Ticker-only must not claim conviction — no contract liquidity/security scan. + if (parsed.id_type === 'ticker' && (verdict_bucket === 'conviction' || verdict_bucket === 'tiny_speculative')) { + verdict_bucket = 'research_position'; + } + + const dex_scan = dexContext + ? { + pairs_found: dexContext.pairs.length, + top_pair: dexContext.pairs[0] ?? null, + venue_depth_cap_usd: dexContext.pairs[0]?.liquidity_usd ?? null + } + : null; + + return { + schema_version: '0.2', + service_id: SERVICE_ID, + mode: 'live', + tier: 'standard_lite', + generated_at: new Date().toISOString(), + input: { + asset: raw, + canonical_id: parsed.canonical_id, + id_type: parsed.id_type, + asset_class: parsed.evm_contract ? 'evm_token' : (parsed.id_type === 'ticker' ? 'ticker_symbol' : 'unknown') + }, + verdict_bucket, + score_0_100: score, + buyer_summary_zh: buildTokenBuyerSummaryZh(verdict_bucket, score, hardStops, dex_scan), + pillars: pillarsToArray(pillars), + hard_stops: hardStops, + hard_veto_gaps: [...new Set(hardVetoGaps)], + confidence_gaps: [...new Set(confidenceGaps)], + reasons, + dex_scan, + caveats: [...STANDARD_CAVEATS], + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + method: 'rule_based_standard_lite_dd', + dex_provider: parsed.evm_contract ? 'dexscreener_public_api' : null + } + }; +} + +export function buildTokenDdVerdictFallback(input = {}) { + const raw = String(input?.asset ?? input?.token ?? '0x000000000000000000000000000000000000dead').trim(); + return { + schema_version: '0.2', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + tier: 'standard_lite', + generated_at: new Date().toISOString(), + input: { asset: raw, canonical_id: null, id_type: 'unknown', asset_class: 'unknown' }, + verdict_bucket: 'watch_only', + score_0_100: 45, + pillars: pillarsToArray(buildPillarBaseline()), + hard_stops: ['live_data_unavailable'], + hard_veto_gaps: ['live_data_unavailable'], + confidence_gaps: ['live_data_unavailable'], + reasons: ['Live token lookup unavailable; serving static demo verdict only.'], + dex_scan: null, + caveats: [ + ...STANDARD_CAVEATS, + 'Demo mode: do not use for trading decisions.' + ], + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { method: 'static_fallback' } + }; +} + +function parseAssetInput(raw) { + const referral_risk = REFERRAL_PATTERNS.some((pattern) => pattern.test(raw)); + const evmMatch = raw.match(EVM_ADDRESS); + if (evmMatch) { + return { + referral_risk, + canonical_id: evmMatch[1].toLowerCase(), + id_type: 'evm_contract', + evm_contract: evmMatch[1].toLowerCase() + }; + } + + try { + const url = new URL(raw); + const path = `${url.pathname}${url.search}`; + if (REFERRAL_PATTERNS.some((pattern) => pattern.test(path))) { + return { referral_risk: true, canonical_id: null, id_type: 'unknown', evm_contract: null }; + } + const fromPath = path.match(EVM_ADDRESS); + if (fromPath) { + return { + referral_risk, + canonical_id: fromPath[1].toLowerCase(), + id_type: 'evm_contract', + evm_contract: fromPath[1].toLowerCase() + }; + } + } catch { + // not a URL + } + + const ticker = raw.replace(/[^A-Za-z0-9]/g, ''); + if (TICKER_ONLY.test(ticker)) { + return { + referral_risk, + canonical_id: ticker.toUpperCase(), + id_type: 'ticker', + evm_contract: null + }; + } + + return { referral_risk, canonical_id: null, id_type: 'unknown', evm_contract: null }; +} + +function buildPillarBaseline() { + return { + identifier_clarity: pillar('identifier_clarity', 'Can we identify the tradable object?'), + referral_and_promo_risk: pillar('referral_and_promo_risk', 'Referral/promo wrapper risk'), + liquidity_and_market_presence: pillar('liquidity_and_market_presence', 'Public liquidity / market presence'), + market_activity: pillar('market_activity', '24h venue activity / volume'), + venue_maturity: pillar('venue_maturity', 'Pair / venue age proxy'), + volatility_proxy: pillar('volatility_proxy', 'Short-horizon price volatility'), + holder_concentration_proxy: pillar('holder_concentration_proxy', 'Holder concentration (if available)'), + security_heuristics: pillar('security_heuristics', 'Automated security scan depth'), + narrative_hype_risk: pillar('narrative_hype_risk', 'Hype / social pressure signals in input'), + tokenomics_unlock_gap: pillar('tokenomics_unlock_gap', 'Unlock / emissions evidence gap') + }; +} + +function pillar(id, label) { + return { id, label, status: 'neutral', note: 'Pending evaluation.' }; +} + +function applyPillar(pillars, id, status, note) { + if (!pillars[id]) return; + pillars[id].status = status; + pillars[id].note = note; +} + +function pillarsToArray(pillars) { + return Object.values(pillars).map((entry) => ({ + id: entry.id, + label: entry.label, + status: mapStatusEmoji(entry.status), + note: entry.note + })); +} + +function mapStatusEmoji(status) { + if (status === 'pass') return '✅'; + if (status === 'warn' || status === 'neutral') return '⚠️'; + if (status === 'fail') return '➖'; + return '⚠️'; +} + +function scoreFromPillars(pillars, hardStops) { + if (hardStops.includes('referral_or_promo_wrapper') || hardStops.includes('missing_canonical_identifier')) { + return 15; + } + let score = 55; + for (const entry of Object.values(pillars)) { + if (entry.status === 'pass') score += 8; + if (entry.status === 'warn' || entry.status === 'neutral') score += 2; + if (entry.status === 'fail') score -= 18; + } + if (hardStops.includes('no_public_dex_liquidity')) score = Math.min(score, 25); + if (hardStops.includes('very_low_liquidity')) score = Math.min(score, 35); + return clamp(Math.round(score), 0, 100); +} + +function bucketFromScore(score, hardStops) { + if (hardStops.includes('referral_or_promo_wrapper') || hardStops.includes('missing_canonical_identifier')) { + return 'avoid'; + } + if (hardStops.includes('no_public_dex_liquidity') || score < 30) return 'avoid'; + if (score < 45) return 'watch_only'; + if (score < 60) return 'research_position'; + if (score < 75) return 'tiny_speculative'; + return 'conviction'; +} + +async function fetchDexScreenerPairs(fetchImpl, contract) { + const payload = await fetchJson(fetchImpl, `${DEXSCREENER_BASE}/${contract}`); + const pairs = (Array.isArray(payload?.pairs) ? payload.pairs : []) + .map((pair) => ({ + chainId: pair.chainId ?? null, + dexId: pair.dexId ?? null, + pairAddress: pair.pairAddress ?? null, + liquidity_usd: toNumber(pair?.liquidity?.usd), + volume_h24: toNumber(pair?.volume?.h24), + priceUsd: toNumber(pair?.priceUsd), + price_change_h24: toNumber(pair?.priceChange?.h24), + txns_h24: toNumber(pair?.txns?.h24?.buys) + toNumber(pair?.txns?.h24?.sells), + pair_created_at: pair?.pairCreatedAt ? toNumber(pair.pairCreatedAt) : null + })) + .filter((pair) => pair.liquidity_usd > 0) + .sort((a, b) => b.liquidity_usd - a.liquidity_usd); + return { pairs }; +} + +function round1(value) { + return Math.round(toNumber(value) * 10) / 10; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) { + throw new Error(`Upstream ${response.status} for ${url}`); + } + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function toNumber(value) { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function buildTokenBuyerSummaryZh(bucket, score, hardStops, dexScan) { + const bucketZh = { + avoid: '避开', + watch_only: '只观望', + research_position: '可研究', + tiny_speculative: '极小仓试错', + conviction: '高信念(仍非建议)' + }[bucket] || bucket; + const stopBit = hardStops?.length ? `;硬停 ${hardStops.slice(0, 2).join(', ')}` : ''; + const liq = dexScan?.venue_depth_cap_usd; + const liqBit = liq != null ? `;顶池流动性约 $${Math.round(liq).toLocaleString('en-US')}` : ''; + return `分桶 ${bucketZh},分数 ${score}/100${stopBit}${liqBit}。Standard-lite 规则闸门,非审计/非投资建议。`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/world-cup-upset-alert.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/world-cup-upset-alert.mjs new file mode 100644 index 00000000..b6c30bb8 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/world-cup-upset-alert.mjs @@ -0,0 +1,7 @@ +// Legacy World Cup Upset Alert — thin re-export of sports-generic implementation. +export { + assessWorldCupUpsetAlertLive, + buildWorldCupUpsetAlertFallback, + assessSportsUpsetAlertLive, + buildSportsUpsetAlertFallback +} from './sports-upset-alert.mjs'; diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money-live.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money-live.mjs new file mode 100644 index 00000000..511174da --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money-live.mjs @@ -0,0 +1,819 @@ +// Live Polymarket-backed implementations of the Smart Money Radar services: +// - World Cup Smart Money Radar (world_cup_smart_money_radar) — World Cup markets only. +// - Polymarket Smart Money Radar (polymarket_smart_money_radar) — site-wide, any topic. +// Both share the same scan pipeline (large taker trades -> wallet aggregation -> +// 7d PnL + position enrichment); only market discovery differs. +// +// Public endpoints used (no API key required), response shapes verified 2026-07-05: +// - Gamma: https://gamma-api.polymarket.com/events?tag_slug=world-cup&closed=false... +// -> [{ title, slug, volume24hr, markets: [{ conditionId, question, slug, +// outcomes: '["Yes","No"]', outcomePrices: '["0.465","0.535"]', active, closed, ... }] }] +// - Gamma: https://gamma-api.polymarket.com/public-search?q=&events_status=active&limit_per_type=10 +// -> { events: [{ title, slug, closed, markets: [...same market shape...] }], pagination } +// (text search; /events?title=... is NOT supported — param is ignored, verified 2026-07-05) +// - Gamma: https://gamma-api.polymarket.com/markets?closed=false&order=volume24hr... +// -> [{ conditionId, question, slug, outcomes, outcomePrices, volumeNum, ... }] (fallback) +// - Data: https://data-api.polymarket.com/trades?market=&takerOnly=true&filterType=CASH&filterAmount=... +// -> [{ proxyWallet, side: 'BUY'|'SELL', size, price, timestamp, outcome, conditionId, title, ... }] +// - Data: https://data-api.polymarket.com/positions?user=&market= +// -> [{ proxyWallet, size, avgPrice, totalBought, cashPnl, outcome, ... }] +// - LB: https://lb-api.polymarket.com/profit?window=7d&address= +// -> [{ proxyWallet, amount, name, pseudonym }] (empty array when wallet unranked) + +const SERVICE_ID = 'world_cup_smart_money_radar'; +const POLYMARKET_SERVICE_ID = 'polymarket_smart_money_radar'; +const GAMMA_BASE = 'https://gamma-api.polymarket.com'; +const DATA_BASE = 'https://data-api.polymarket.com'; +const LB_BASE = 'https://lb-api.polymarket.com'; + +const MIN_TRADE_NOTIONAL_USDT = 500; +const MAX_MARKETS_SCANNED = 8; +const TRADES_PER_MARKET = 100; +const FETCH_TIMEOUT_MS = 8000; +/** Near-settled prices are usually noise for "smart money" reads (locking PnL / dust). */ +const NEAR_SETTLED_PRICE_LOW = 0.05; +const NEAR_SETTLED_PRICE_HIGH = 0.95; + +const STANDARD_CAVEATS = [ + 'Data and analytics only. Not investment advice, not betting advice, and not a guarantee of future returns.', + 'No wallet custody, no user funds, no trade execution, no order routing.', + 'Smart-money signals are heuristic reads of recent large public trades on Polymarket and can be wrong or stale.', + 'Signals with last_trade_price ≤0.05 or ≥0.95 are tagged near_settled_noise and demoted; prefer mid-price markets.' +]; + +/** + * Build the full live response payload. + * Throws on upstream failure so callers can decide how to degrade. + * Internally uses sports-generic discovery with league=world_cup (legacy path). + */ +export async function assessWorldCupSmartMoneyLive(input = {}, options = {}) { + return assessSportsSmartMoneyLive({ + ...input, + sport: input.sport ?? 'football', + league: input.league ?? 'world_cup', + tag_slug: input.tag_slug ?? 'world-cup' + }, { ...options, serviceId: SERVICE_ID, legacyWorldCup: true }); +} + +/** + * Sports-generic Smart Money Radar — football leagues, tennis, NBA, NFL, UFC, MLB, etc. + * World Cup is one league tag, not the product identity. + */ +export async function assessSportsSmartMoneyLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const limit = clampInteger(input.limit, 1, 10, 5); + const scope = normalizeSportsScope(input); + const serviceId = options.serviceId ?? 'sports_smart_money_radar'; + + const { markets, usedFallback, discovery } = await resolveSportsMarkets(fetchImpl, scope); + + const expansionCaveat = discovery?.scope_expanded + ? `No active ${discovery.requested_scope} markets matched; expanded live discovery to ${discovery.effective_scope} sports markets.` + : null; + const fallbackCaveat = expansionCaveat || (usedFallback + ? (options.legacyWorldCup + ? 'No active World Cup markets matched; fell back to Polymarket top-volume markets site-wide.' + : `No active ${scope.label} markets matched; fell back to Polymarket top-volume / search.`) + : null); + + return buildLiveResponse({ + serviceId, + inputEcho: { + sport: scope.sport, + league: scope.league, + tag_slug: scope.tag_slug, + query: scope.query || 'all', + market: input.market ?? null, + limit + }, + fallbackCaveat, + scan: await scanMarketsForSmartMoney(fetchImpl, markets, limit), + extraSource: { + discovery, + scope: discovery?.effective_scope ?? scope.label, + ...(discovery?.scope_expanded + ? { + scope_expanded: true, + requested_scope: discovery.requested_scope, + effective_scope: discovery.effective_scope + } + : {}) + } + }); +} + +/** + * Site-wide Polymarket Smart Money Radar. Same scan pipeline as the World Cup + * radar, but market discovery searches the whole site: `market` / `topic` are + * treated as free-text search terms (Gamma /public-search, with a top-volume + * substring-match fallback); with no term it scans the top 24h-volume markets. + * Also accepts optional `event_type` / `tag_slug` for category-scoped scans + * (politics, crypto, sports, etc.). + * Throws on upstream failure so callers can decide how to degrade. + */ +export async function assessPolymarketSmartMoneyLive(input = {}, options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const searchTerm = normalizeText(input.market ?? input.topic ?? input.query ?? 'all'); + const limit = clampInteger(input.limit, 1, 10, 5); + const tagSlug = normalizeText(input.tag_slug ?? input.event_type ?? ''); + + let resolved; + if (tagSlug && tagSlug !== 'all') { + resolved = await resolveSportsMarkets(fetchImpl, { + sport: 'all', + league: null, + tag_slug: tagSlug, + query: searchTerm === 'all' ? '' : searchTerm, + label: `tag:${tagSlug}` + }); + } else { + resolved = await resolvePolymarketMarkets(fetchImpl, searchTerm); + } + + const { markets, usedFallback } = resolved; + + return buildLiveResponse({ + serviceId: POLYMARKET_SERVICE_ID, + inputEcho: { + market: input.market ?? null, + topic: input.topic ?? null, + query: searchTerm || 'all', + tag_slug: tagSlug || null, + event_type: input.event_type ?? null, + limit + }, + fallbackCaveat: usedFallback + ? `No active Polymarket markets matched "${searchTerm || tagSlug}"; fell back to top-volume markets site-wide.` + : null, + scan: await scanMarketsForSmartMoney(fetchImpl, markets, limit) + }); +} + +/** + * Shared scan pipeline: large taker trades per market -> per-wallet flow + * aggregation -> 7d PnL + open-position enrichment for the top wallets. + * Exported for reuse by the World Cup Upset Alert service. + */ +export async function scanMarketsForSmartMoney(fetchImpl, markets, limit) { + const scanned = markets.slice(0, MAX_MARKETS_SCANNED); + + const tradesPerMarket = await Promise.all( + scanned.map((market) => fetchLargeTrades(fetchImpl, market.condition_id).catch(() => [])) + ); + + const aggregates = aggregateWalletFlows(scanned, tradesPerMarket); + // Enrich a wider cut so cross-market cohort can reuse 7d PnL lookups. + const enrichCut = aggregates.slice(0, Math.max(limit, 16)); + + const pnlCache = new Map(); + const enriched = await Promise.all(enrichCut.map(async (entry) => { + let pnl = pnlCache.get(entry.wallet); + if (pnl === undefined) { + pnl = await fetchSevenDayPnl(fetchImpl, entry.wallet).catch(() => null); + pnlCache.set(entry.wallet, pnl); + } + const position = await fetchPosition(fetchImpl, entry.wallet, entry.condition_id).catch(() => null); + return buildSignal(entry, pnl, position); + })); + + // Prefer mid-price signals; keep near-settled only as filler if we lack cleaner ones. + const ranked = rankSignalsPreferClean(enriched).slice(0, limit); + const wallet_cohort = buildWalletCohort(aggregates, pnlCache); + + return { scanned, enriched: ranked, wallet_cohort, aggregates_count: aggregates.length }; +} + +function buildWalletCohort(aggregates, pnlCache = new Map()) { + const byWallet = new Map(); + for (const entry of aggregates) { + let row = byWallet.get(entry.wallet); + if (!row) { + row = { + wallet: entry.wallet, + address_label: shortenAddress(entry.wallet), + market_ids: new Set(), + market_titles: [], + outcomes: [], + gross_notional: 0, + trade_count: 0 + }; + byWallet.set(entry.wallet, row); + } + if (!row.market_ids.has(entry.condition_id)) { + row.market_ids.add(entry.condition_id); + if (row.market_titles.length < 6) row.market_titles.push(entry.market_title); + } + row.outcomes.push({ + market_title: entry.market_title, + outcome: entry.outcome, + notional_usdt: round2(entry.gross_notional) + }); + row.gross_notional += entry.gross_notional; + row.trade_count += entry.trade_count; + } + + return [...byWallet.values()] + .map((row) => { + const markets_touched = row.market_ids.size; + const pnl = pnlCache.has(row.wallet) ? pnlCache.get(row.wallet) : null; + return { + address_label: row.address_label, + markets_touched, + cross_market: markets_touched >= 2, + market_titles: row.market_titles, + outcomes_sample: row.outcomes + .slice() + .sort((a, b) => b.notional_usdt - a.notional_usdt) + .slice(0, 4), + gross_notional_usdt: round2(row.gross_notional), + trade_count: row.trade_count, + seven_day_pnl_usdt: pnl + }; + }) + .filter((row) => row.cross_market || row.gross_notional_usdt >= 2500) + .sort((a, b) => { + if (a.cross_market !== b.cross_market) return a.cross_market ? -1 : 1; + if (b.markets_touched !== a.markets_touched) return b.markets_touched - a.markets_touched; + return b.gross_notional_usdt - a.gross_notional_usdt; + }) + .slice(0, 8); +} + +function buildLiveResponse({ serviceId, inputEcho, fallbackCaveat, scan, extraSource = null }) { + const { scanned, enriched, wallet_cohort = [] } = scan; + const missingPnl = enriched.some((signal) => signal.seven_day_pnl_usdt === null); + const summary = buildSummary(enriched, wallet_cohort); + + const caveats = [...STANDARD_CAVEATS]; + if (fallbackCaveat) { + caveats.push(fallbackCaveat); + } + if (missingPnl) { + caveats.push('seven_day_pnl_usdt is null for wallets not present on the Polymarket 7-day profit leaderboard; values are never estimated.'); + } + const crossMarket = wallet_cohort.filter((w) => w.cross_market).length; + if (crossMarket > 0) { + caveats.push(`${crossMarket} wallet(s) appear across ≥2 scanned markets (wallet_cohort); still heuristic, not coordinated-trading proof.`); + } + + return { + schema_version: '0.3', + service_id: serviceId, + mode: 'live', + generated_at: new Date().toISOString(), + input: inputEcho, + // Machine-readable scope verdict so a calling agent can branch without parsing prose. + capability_status: extraSource?.scope_expanded ? 'off_scope_fallback' : 'on_scope', + ...(extraSource?.scope_expanded + ? { + requested_scope: extraSource.requested_scope, + effective_scope: extraSource.effective_scope + } + : {}), + buyer_summary_zh: buildSmartMoneyBuyerSummaryZh(summary, extraSource), + buyer_summary_en: buildSmartMoneyBuyerSummaryEn(summary, extraSource), + summary, + signals: enriched, + wallet_cohort, + caveats, + next_gate: 'OKX_ASP_listing_changes_require_Leo_approval', + source: { + provider: 'polymarket_public_api', + markets_scanned: scanned.map((market) => ({ + market_id: market.market_id, + condition_id: market.condition_id, + title: market.title + })), + min_trade_notional_usdt: MIN_TRADE_NOTIONAL_USDT, + max_markets_scanned: MAX_MARKETS_SCANNED, + wallet_cohort_rule: 'wallets touching ≥2 markets OR ≥$2500 gross notional in scan window', + ...(extraSource && typeof extraSource === 'object' ? extraSource : {}) + } + }; +} + +/** Map sport/league aliases → Gamma tag_slug candidates + search queries. */ +const SPORT_TAG_MAP = { + football: ['soccer', 'football', 'epl', 'premier-league', 'ucl', 'champions-league', 'la-liga', 'serie-a', 'bundesliga', 'mls'], + soccer: ['soccer', 'football', 'epl', 'ucl', 'la-liga', 'mls'], + tennis: ['tennis', 'atp', 'wta'], + nba: ['nba', 'basketball'], + basketball: ['nba', 'basketball'], + nfl: ['nfl', 'football'], + ufc: ['ufc', 'mma'], + mlb: ['mlb', 'baseball'], + baseball: ['mlb', 'baseball'], + world_cup: ['world-cup'], + worldcup: ['world-cup'] +}; + +const LEAGUE_TAG_MAP = { + world_cup: ['world-cup'], + worldcup: ['world-cup'], + epl: ['epl', 'premier-league', 'soccer'], + ucl: ['ucl', 'champions-league', 'soccer'], + laliga: ['la-liga', 'soccer'], + 'la-liga': ['la-liga', 'soccer'], + serie_a: ['serie-a', 'soccer'], + bundesliga: ['bundesliga', 'soccer'], + mls: ['mls', 'soccer'], + atp: ['tennis', 'atp'], + wta: ['tennis', 'wta'], + nba: ['nba'], + nfl: ['nfl'], + ufc: ['ufc'], + mlb: ['mlb'] +}; + +function normalizeSportsScope(input = {}) { + const sport = normalizeText(input.sport ?? 'all') || 'all'; + const league = normalizeText(input.league ?? '') || null; + const explicitTag = normalizeText(input.tag_slug ?? '') || null; + const query = normalizeText( + input.query ?? input.market ?? input.market_id ?? input.team ?? '' + ); + const queryClean = query === 'all' ? '' : query; + + const tags = []; + if (explicitTag) tags.push(explicitTag); + if (league && LEAGUE_TAG_MAP[league]) tags.push(...LEAGUE_TAG_MAP[league]); + if (sport && sport !== 'all' && SPORT_TAG_MAP[sport]) tags.push(...SPORT_TAG_MAP[sport]); + + const uniqueTags = [...new Set(tags.filter(Boolean))]; + const labelParts = [sport !== 'all' ? sport : null, league, explicitTag, queryClean].filter(Boolean); + + return { + sport, + league, + tag_slug: uniqueTags[0] ?? null, + tag_candidates: uniqueTags, + query: queryClean, + label: labelParts.join('/') || 'sports_all' + }; +} + +/** + * Sports / category market discovery: try Gamma tag_slug candidates, then + * public-search with sport/league/query, then top-volume fallback. + */ +export async function resolveSportsMarkets(fetchImpl, scopeInput) { + const scope = typeof scopeInput === 'string' + ? normalizeSportsScope({ query: scopeInput }) + : (scopeInput?.label && scopeInput.tag_candidates + ? scopeInput + : normalizeSportsScope(scopeInput ?? {})); + + const discovery = { tried_tags: [], search_terms: [], method: null }; + let markets = []; + + for (const tag of scope.tag_candidates) { + discovery.tried_tags.push(tag); + try { + const events = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?closed=false&active=true&limit=25&order=volume24hr&ascending=false&tag_slug=${encodeURIComponent(tag)}` + ); + markets = flattenEventMarkets(Array.isArray(events) ? events : []); + if (markets.length) { + discovery.method = `tag_slug:${tag}`; + maybeMarkScopeExpansion(discovery, scope, tag); + break; + } + } catch { + // try next tag + } + } + + if (scope.query && markets.length) { + const filtered = markets.filter((market) => + normalizeText(`${market.market_id} ${market.title} ${market.event_title} ${market.slug}`).includes(scope.query)); + if (filtered.length) { + if (discovery.method) { + maybeMarkScopeExpansion(discovery, scope, discovery.method.replace(/^tag_slug:/, '')); + } + return { markets: filtered, usedFallback: false, discovery }; + } + } + + if (markets.length) { + return { markets, usedFallback: false, discovery }; + } + + const searchTerms = [ + scope.query, + scope.league, + scope.sport !== 'all' ? scope.sport : null, + scope.tag_slug + ].filter(Boolean); + + for (const term of searchTerms) { + discovery.search_terms.push(term); + try { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(term)}&events_status=active&limit_per_type=10` + ); + markets = flattenEventMarkets(Array.isArray(result?.events) ? result.events : []); + if (markets.length) { + discovery.method = `public-search:${term}`; + maybeMarkScopeExpansion(discovery, scope, term); + return { markets, usedFallback: false, discovery }; + } + } catch { + // next term + } + } + + const fallback = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?closed=false&active=true&limit=25&order=volume24hr&ascending=false` + ); + const fallbackMarkets = (Array.isArray(fallback) ? fallback : []) + .filter((market) => market.conditionId && market.enableOrderBook !== false) + .map((market) => normalizeMarket(market, market.question ?? market.slug ?? '')); + discovery.method = 'top_volume_fallback'; + // 2026-07-30: this branch used to return site-wide top-volume markets without + // marking the scope change, so buildSmartMoneyBuyerSummaryZh/En took the on-scope + // wording. Measured after the 2026-07-19 World Cup final, /world-cup-smart-money-radar + // answered with "Top signal: … Will there be no change in Fed interest rates after the + // September 2026 meeting?" while the headline still read 已扫描真实 Polymarket 市场 — + // a macro market delivered under a World Cup SKU. The caveat existed but only inside + // `caveats`; buyers and calling agents read the summary and the status field. + markScopeFellBackSiteWide(discovery, scope); + return { markets: fallbackMarkets, usedFallback: true, discovery }; +} + +/** Scoped request answered with site-wide markets — must be visible, not buried. */ +function markScopeFellBackSiteWide(discovery, scope) { + const requested = scope?.league ?? scope?.sport ?? scope?.tag_slug ?? scope?.label ?? null; + if (!requested || requested === 'all') return; + discovery.scope_expanded = true; + discovery.requested_scope = requested; + discovery.effective_scope = 'site_wide_top_volume'; +} + +function maybeMarkScopeExpansion(discovery, scope, selectedToken) { + if (!isWorldCupScope(scope)) return; + const token = normalizeText(selectedToken).replace(/^tag_slug:|^public-search:/, ''); + if (token === 'world-cup' || token === 'world_cup' || token === 'worldcup') return; + discovery.scope_expanded = true; + discovery.requested_scope = 'world_cup'; + discovery.effective_scope = inferExpandedSportsScope(token); +} + +function isWorldCupScope(scope) { + return scope?.league === 'world_cup' + || scope?.sport === 'world_cup' + || scope?.sport === 'worldcup' + || scope?.tag_slug === 'world-cup' + || (scope?.tag_candidates || []).includes('world-cup'); +} + +function inferExpandedSportsScope(token) { + if (/soccer|football|epl|premier|ucl|champions|la-liga|serie-a|bundesliga|mls/.test(token)) { + return 'football'; + } + return 'sports'; +} + +/** + * Find active World Cup markets via Gamma events; fall back to site-wide + * top-volume markets when nothing matches. + * Exported (as resolveWorldCupMarkets) for the World Cup Upset Alert service. + */ +async function resolveMarkets(fetchImpl, marketHint) { + const resolved = await resolveSportsMarkets(fetchImpl, { + sport: 'football', + league: 'world_cup', + tag_slug: 'world-cup', + query: marketHint === 'all' ? '' : marketHint + }); + return { + markets: resolved.markets, + usedFallback: resolved.usedFallback, + discovery: resolved.discovery + }; +} + +/** + * Site-wide market discovery for the Polymarket radar. + * With a search term: Gamma /public-search first (real text search), then a + * substring match over top-volume events. Without one (or when nothing + * matches): site-wide top 24h-volume markets. + */ +async function resolvePolymarketMarkets(fetchImpl, searchTerm) { + const hasTerm = Boolean(searchTerm) && searchTerm !== 'all'; + + if (hasTerm) { + try { + const result = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/public-search?q=${encodeURIComponent(searchTerm)}&events_status=active&limit_per_type=10` + ); + const markets = flattenEventMarkets(Array.isArray(result?.events) ? result.events : []); + if (markets.length) { + return { markets, usedFallback: false }; + } + } catch { + // fall through to top-volume events + substring match + } + } + + let markets = []; + try { + const events = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/events?closed=false&active=true&limit=25&order=volume24hr&ascending=false` + ); + markets = flattenEventMarkets(Array.isArray(events) ? events : []); + } catch { + markets = []; + } + + if (hasTerm && markets.length) { + const filtered = markets.filter((market) => + normalizeText(`${market.market_id} ${market.title} ${market.event_title} ${market.slug}`).includes(searchTerm)); + if (filtered.length) { + return { markets: filtered, usedFallback: false }; + } + return { markets, usedFallback: true }; + } + + if (markets.length) { + return { markets, usedFallback: false }; + } + + const fallback = await fetchJson( + fetchImpl, + `${GAMMA_BASE}/markets?closed=false&active=true&limit=25&order=volume24hr&ascending=false` + ); + const fallbackMarkets = (Array.isArray(fallback) ? fallback : []) + .filter((market) => market.conditionId && market.enableOrderBook !== false) + .map((market) => normalizeMarket(market, market.question ?? market.slug ?? '')); + return { markets: fallbackMarkets, usedFallback: hasTerm }; +} + +function flattenEventMarkets(events) { + const markets = []; + for (const event of events) { + for (const market of event.markets ?? []) { + if (!market.conditionId || market.closed || market.active === false) continue; + markets.push(normalizeMarket(market, event.title ?? '')); + } + } + // Highest recent volume first so we scan where the money actually moves. + markets.sort((a, b) => b.volume_24hr - a.volume_24hr); + return markets; +} + +function normalizeMarket(market, eventTitle) { + return { + market_id: market.slug ?? market.conditionId, + condition_id: market.conditionId, + title: market.question ?? market.slug ?? market.conditionId, + event_title: eventTitle, + slug: market.slug ?? '', + volume_24hr: toNumber(market.volume24hr ?? market.volumeNum ?? market.volume), + updated_at: market.updatedAt ?? null + }; +} + +async function fetchLargeTrades(fetchImpl, conditionId) { + const url = `${DATA_BASE}/trades?market=${encodeURIComponent(conditionId)}` + + `&limit=${TRADES_PER_MARKET}&takerOnly=true&filterType=CASH&filterAmount=${MIN_TRADE_NOTIONAL_USDT}`; + const trades = await fetchJson(fetchImpl, url); + return Array.isArray(trades) ? trades : []; +} + +/** + * Aggregate large taker trades per (wallet, market, outcome). + */ +function aggregateWalletFlows(markets, tradesPerMarket) { + const byKey = new Map(); + + markets.forEach((market, index) => { + for (const trade of tradesPerMarket[index] ?? []) { + const wallet = normalizeText(trade.proxyWallet); + if (!wallet.startsWith('0x')) continue; + const notional = toNumber(trade.size) * toNumber(trade.price); + if (!(notional > 0)) continue; + + const key = `${wallet}|${market.condition_id}|${trade.outcome}`; + let entry = byKey.get(key); + if (!entry) { + entry = { + wallet, + market_id: market.market_id, + condition_id: market.condition_id, + market_title: market.title, + market_updated_at: market.updated_at, + outcome: trade.outcome ?? 'Unknown', + buy_notional: 0, + sell_notional: 0, + trade_count: 0, + last_price: toNumber(trade.price), + last_timestamp: toNumber(trade.timestamp) + }; + byKey.set(key, entry); + } + if (trade.side === 'SELL') { + entry.sell_notional += notional; + } else { + entry.buy_notional += notional; + } + entry.trade_count += 1; + if (toNumber(trade.timestamp) > entry.last_timestamp) { + entry.last_timestamp = toNumber(trade.timestamp); + entry.last_price = toNumber(trade.price); + } + } + }); + + return [...byKey.values()] + .map((entry) => ({ ...entry, gross_notional: entry.buy_notional + entry.sell_notional })) + .sort((a, b) => b.gross_notional - a.gross_notional); +} + +async function fetchSevenDayPnl(fetchImpl, wallet) { + const rows = await fetchJson(fetchImpl, `${LB_BASE}/profit?window=7d&address=${encodeURIComponent(wallet)}`); + if (Array.isArray(rows) && rows.length && Number.isFinite(Number(rows[0].amount))) { + return round2(Number(rows[0].amount)); + } + return null; +} + +async function fetchPosition(fetchImpl, wallet, conditionId) { + const rows = await fetchJson( + fetchImpl, + `${DATA_BASE}/positions?user=${encodeURIComponent(wallet)}&market=${encodeURIComponent(conditionId)}` + ); + return Array.isArray(rows) && rows.length ? rows[0] : null; +} + +function buildSignal(entry, sevenDayPnl, position) { + const action = classifyAction(entry, position); + const notional = round2(Math.max(entry.buy_notional, entry.sell_notional)); + const noiseFlags = []; + const lastPrice = entry.last_price; + if (Number.isFinite(lastPrice) && (lastPrice <= NEAR_SETTLED_PRICE_LOW || lastPrice >= NEAR_SETTLED_PRICE_HIGH)) { + noiseFlags.push('near_settled_noise'); + } + + let confidence = scoreConfidence(entry, sevenDayPnl); + if (noiseFlags.includes('near_settled_noise')) { + confidence = round2(Math.min(confidence, 0.45)); + } + + const rationale = buildRationale(entry, action, sevenDayPnl, position) + + (noiseFlags.includes('near_settled_noise') + ? ' Flagged near_settled_noise: last price is extreme; often settlement/lock-in flow, not a fresh thesis.' + : ''); + + return { + market_id: entry.market_id, + market_title: entry.market_title, + market_updated_at: entry.market_updated_at, + address_label: shortenAddress(entry.wallet), + side: entry.outcome, + action, + notional_usdt: notional, + last_trade_price: entry.last_price, + seven_day_pnl_usdt: sevenDayPnl, + confidence, + noise_flags: noiseFlags, + rationale + }; +} + +function rankSignalsPreferClean(signals) { + return signals.slice().sort((a, b) => { + const aNoise = (a.noise_flags || []).includes('near_settled_noise') ? 1 : 0; + const bNoise = (b.noise_flags || []).includes('near_settled_noise') ? 1 : 0; + if (aNoise !== bNoise) return aNoise - bNoise; + return (b.confidence || 0) - (a.confidence || 0); + }); +} + +function classifyAction(entry, position) { + if (entry.sell_notional > entry.buy_notional) { + return 'reduced_position'; + } + if (position && toNumber(position.totalBought) > 0) { + // Recent large buys explain (almost) the whole position -> fresh entry. + const recentShare = entry.buy_notional / Math.max(toNumber(position.avgPrice), 0.01); + if (recentShare >= toNumber(position.totalBought) * 0.9) { + return 'new_position'; + } + return 'increased_position'; + } + return 'new_position'; +} + +/** + * Heuristic confidence in [0.5, 0.9]: + * larger notional, repeat trades and positive 7d PnL all add conviction. + */ +function scoreConfidence(entry, sevenDayPnl) { + let score = 0.5; + score += 0.15 * Math.min(entry.gross_notional / 5000, 1); + score += entry.trade_count >= 3 ? 0.1 : entry.trade_count === 2 ? 0.05 : 0; + if (typeof sevenDayPnl === 'number' && sevenDayPnl > 0) { + score += 0.15 * Math.min(sevenDayPnl / 10000, 1); + } + return round2(Math.min(0.9, Math.max(0.5, score))); +} + +function buildRationale(entry, action, sevenDayPnl, position) { + const flow = entry.sell_notional > entry.buy_notional + ? `sold ~$${formatUsd(entry.sell_notional)} of "${entry.outcome}"` + : `bought ~$${formatUsd(entry.buy_notional)} of "${entry.outcome}"`; + const parts = [ + `Wallet ${flow} across ${entry.trade_count} large taker trade${entry.trade_count === 1 ? '' : 's'} (last price ${entry.last_price}).` + ]; + if (typeof sevenDayPnl === 'number') { + parts.push(`7d leaderboard PnL ${signedUsd(sevenDayPnl)}.`); + } + if (position && Number.isFinite(Number(position.cashPnl))) { + parts.push(`Open position PnL in this market: ${signedUsd(Number(position.cashPnl))}.`); + } + if (action === 'reduced_position') { + parts.push('Net flow was toward the exit door.'); + } + return parts.join(' '); +} + +function buildSummary(signals, walletCohort = []) { + if (!signals.length) { + return 'No large smart-money movement found in the scanned Polymarket markets.'; + } + const top = signals.slice().sort((a, b) => b.confidence - a.confidence)[0]; + const cross = walletCohort.filter((w) => w.cross_market).length; + const cohortNote = cross > 0 ? ` Cross-market cohort: ${cross} wallet(s).` : ''; + return `${signals.length} smart-money movements found. Top signal: ${top.action} on ${top.side} in ${top.market_title}.${cohortNote}`; +} + +function buildSmartMoneyBuyerSummaryZh(summary, source) { + if (source?.scope_expanded) { + return `请求范围 ${source.requested_scope} 当前无活跃市场,已自动扩展到 ${source.effective_scope} 体育市场并返回真实 Polymarket 大额交易信号。${summary}`; + } + return `已扫描真实 Polymarket 市场的大额 taker 交易。${summary}`; +} + +function buildSmartMoneyBuyerSummaryEn(summary, source) { + if (source?.scope_expanded) { + return `Requested ${source.requested_scope} had no active markets, so live discovery expanded to ${source.effective_scope} sports markets and returned real Polymarket large-trade signals. ${summary}`; + } + return `Scanned real Polymarket markets for large taker trades. ${summary}`; +} + +async function fetchJson(fetchImpl, url) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetchImpl(url, { + signal: controller.signal, + headers: { accept: 'application/json' } + }); + if (!response.ok) { + throw new Error(`Upstream ${response.status} for ${url}`); + } + return await response.json(); + } finally { + clearTimeout(timer); + } +} + +function shortenAddress(address) { + const value = String(address ?? ''); + if (!value.startsWith('0x') || value.length < 12) return value; + return `${value.slice(0, 6)}…${value.slice(-4)}`; +} + +function formatUsd(value) { + return Math.abs(round2(value)).toLocaleString('en-US'); +} + +function signedUsd(value) { + return `${value < 0 ? '-' : '+'}$${formatUsd(value)}`; +} + +function round2(value) { + return Math.round(Number(value) * 100) / 100; +} + +function toNumber(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function normalizeText(value) { + return String(value ?? '').trim().toLowerCase(); +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} + +export { resolveMarkets as resolveWorldCupMarkets }; diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money.mjs new file mode 100644 index 00000000..650c324c --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/src/worldcup-smart-money.mjs @@ -0,0 +1,103 @@ +const SERVICE_ID = 'world_cup_smart_money_radar'; + +const MOCK_MARKETS = [ + { + market_id: 'world-cup-2026-winner', + title: '2026 World Cup Winner', + updated_at: '2026-07-04T00:00:00Z', + signals: [ + { + address_label: 'wc-alpha-017', + side: 'Argentina YES', + action: 'increased_position', + notional_usdt: 1840, + seven_day_pnl_usdt: 612, + confidence: 0.78, + rationale: 'Profitable wallet added exposure while public odds softened.' + }, + { + address_label: 'wc-alpha-042', + side: 'Brazil YES', + action: 'reduced_position', + notional_usdt: 960, + seven_day_pnl_usdt: 231, + confidence: 0.64, + rationale: 'Top wallet trimmed into price strength after two prior profitable entries.' + } + ] + }, + { + market_id: 'world-cup-2026-group-stage', + title: '2026 World Cup Group Stage', + updated_at: '2026-07-04T00:00:00Z', + signals: [ + { + address_label: 'wc-alpha-009', + side: 'USA reaches round of 16', + action: 'contrarian_accumulation', + notional_usdt: 720, + seven_day_pnl_usdt: 188, + confidence: 0.59, + rationale: 'Wallet with positive World Cup history accumulated against consensus drift.' + } + ] + } +]; + +export function assessWorldCupSmartMoney(input = {}) { + const marketHint = normalizeText(input.market ?? input.market_id ?? input.query ?? 'all'); + const limit = clampInteger(input.limit, 1, 10, 5); + + const matchedMarkets = MOCK_MARKETS + .filter((market) => marketHint === 'all' || normalizeText(`${market.market_id} ${market.title}`).includes(marketHint)) + .slice(0, limit); + + const markets = matchedMarkets.length ? matchedMarkets : MOCK_MARKETS.slice(0, limit); + const signals = markets.flatMap((market) => market.signals.map((signal) => ({ + market_id: market.market_id, + market_title: market.title, + market_updated_at: market.updated_at, + ...signal + }))); + + return { + schema_version: '0.1', + service_id: SERVICE_ID, + mode: 'public_safe_demo', + generated_at: new Date().toISOString(), + input: { + market: input.market ?? input.market_id ?? input.query ?? 'all', + limit + }, + summary: buildSummary(signals), + signals, + caveats: [ + 'Demo data only. Production service must use fresh Polymarket-derived data before listing.', + 'Data and analytics only. Not investment advice, not betting advice, and not a guarantee of future returns.', + 'No wallet custody, no user funds, no trade execution, no order routing.' + ], + next_gate: 'production_data_feed_and_OKX_ASP_listing_require_Leo_approval' + }; +} + +function buildSummary(signals) { + if (!signals.length) { + return 'No smart-money movement found for the requested market.'; + } + + const top = signals + .slice() + .sort((a, b) => b.confidence - a.confidence)[0]; + + return `${signals.length} smart-money movements found. Top signal: ${top.action} on ${top.side} in ${top.market_title}.`; +} + +function normalizeText(value) { + return String(value ?? '').trim().toLowerCase(); +} + +function clampInteger(value, min, max, fallback) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/http-smoke.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/http-smoke.mjs new file mode 100644 index 00000000..bce17682 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/http-smoke.mjs @@ -0,0 +1,78 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createServer } from '../src/http-server.mjs'; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const server = createServer(); + +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const { port } = server.address(); +const base = `http://127.0.0.1:${port}`; + +try { + const health = await fetch(`${base}/health`).then((res) => res.json()); + assert(health.ok === true, 'health check failed'); + + const samples = await fetch(`${base}/api/sample-audits`).then((res) => res.json()); + assert(Array.isArray(samples) && samples.length === 5, 'sample audits endpoint failed'); + + const services = await fetch(`${base}/api/okx-ai-services`).then((res) => res.json()); + assert(services.services?.length === 4, 'OKX.AI service list mismatch'); + assert(services.services.some((service) => service.service_id === 'polymarket_smart_money_radar'), 'service list missing Polymarket service'); + + const discovery = await fetch(`${base}/.well-known/agent-service.json`).then((res) => res.json()); + assert(discovery.service_id === 'agent-acceptance-gate', 'agent discovery endpoint failed'); + assert(discovery.call_when.includes('before_release_payment'), 'agent discovery missing call_when'); + + const manifest = await fetch(`${base}/mcp-tool-manifest.json`).then((res) => res.json()); + assert(manifest.tools?.some((tool) => tool.name === 'audit_agent_delivery'), 'mcp manifest endpoint failed'); + assert(manifest.tools?.some((tool) => tool.name === 'world_cup_smart_money_radar'), 'mcp manifest missing World Cup tool'); + + const openapi = await fetch(`${base}/openapi.yaml`).then((res) => res.text()); + assert(openapi.includes('/audit-agent-deliverable'), 'openapi endpoint missing audit path'); + assert(openapi.includes('/world-cup-smart-money-radar'), 'openapi endpoint missing World Cup path'); + + const input = JSON.parse(fs.readFileSync(path.join(rootDir, 'sample-inputs/01-pmquant-rename.json'), 'utf8')); + const audit = await fetch(`${base}/audit-agent-deliverable`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(input) + }).then((res) => res.json()); + assert(audit.verdict === 'needs_review', 'audit endpoint verdict mismatch'); + assert(audit.machine_flags.includes('public_release_gate'), 'audit endpoint missing public_release_gate flag'); + + const radar = await fetch(`${base}/world-cup-smart-money-radar`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ market: 'winner', limit: 2 }) + }).then((res) => res.json()); + assert(radar.service_id === 'world_cup_smart_money_radar', 'World Cup radar service id mismatch'); + assert(radar.signals.length >= 1, 'World Cup radar returned no signals'); + assert(radar.caveats.some((caveat) => caveat.includes('Not investment advice')), 'World Cup radar missing advice caveat'); + + for (const path of [ + '/polymarket-smart-money-radar', + '/event-probability-crypto-divergence', + '/crypto-market-pulse-report' + ]) { + const report = await fetch(`${base}${path}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query: 'all', limit: 1 }) + }).then((res) => res.json()); + assert(report.mode === 'public_safe_demo', `${path} mode mismatch`); + assert(report.signals.length === 1, `${path} signal count mismatch`); + } + + const html = await fetch(`${base}/`).then((res) => res.text()); + assert(html.includes('Agent 验收门禁'), 'demo html missing title'); + + console.log(`PASS http smoke on ${base}`); +} finally { + await new Promise((resolve) => server.close(resolve)); +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/pm-event-dual-surface-test.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/pm-event-dual-surface-test.mjs new file mode 100644 index 00000000..12e3bc44 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/pm-event-dual-surface-test.mjs @@ -0,0 +1,130 @@ +import assert from 'node:assert/strict'; +import { + toCopilotResearchUnit, + COPILOT_SCHEMA_ID, + AGENT_EXTENSION_KEY +} from '../src/pm-event-to-copilot-research-unit.mjs'; + +function baseAgent(overrides = {}) { + return { + schema_version: '0.2', + service_id: 'pm_event_readout', + mode: 'live', + generated_at: '2026-07-12T04:00:00.000Z', + input: { + market_url: 'https://polymarket.com/event/fed-decision-in-july', + slug: 'fed-decision-in-july-no-change', + condition_id: null + }, + event: 'Fed decision in July', + event_slug: 'fed-decision-in-july', + market: 'fed-decision-in-july-no-change', + market_title: 'No change', + current_price: { yes: 0.82, no: 0.18 }, + event_time: '2026-07-31T00:00:00Z', + fixture_status: 'ok', + category: 'generic', + category_depth: 'core_only', + sources_read: ['gamma:market', 'gamma:event'], + base_case: 'Market prices "No change" at 0.82 (82% implied).', + market_implied_view: 'Hold is already the base case.', + what_is_already_priced: ['Hold @ 0.82'], + what_may_not_be_priced: ['Surprise cut path'], + event_matrix: [ + { slug: 'fed-decision-in-july-no-change', yes_price: 0.82, liquidity: 12000 }, + { slug: 'fed-decision-in-july-25-bps-decrease', yes_price: 0.15, liquidity: 8000 } + ], + matrix_status: 'complete', + related_market_count: 2, + missing_market_groups: [], + tradability: 'high', + tradability_reasons: [], + next_decision_card_needed: 'yes', + hard_gate: 'no_orders_no_account_mutation', + ...overrides + }; +} + +{ + const { ok, value, errors } = toCopilotResearchUnit(baseAgent(), { unitId: 'unit-fed-1' }); + assert.equal(ok, true, errors?.join('; ')); + assert.equal(value.schemaId, COPILOT_SCHEMA_ID); + assert.equal(value.unitId, 'unit-fed-1'); + assert.equal(value.market.platform, 'polymarket'); + assert.equal(value.market.marketId, 'fed-decision-in-july-no-change'); + assert.equal(value.freshness.status, 'complete'); + assert.equal(value.freshness.asOf, '2026-07-12T04:00:00.000Z'); + assert.equal(value.compliance.analysisAllowed, true); + assert.equal(value.decision.eligibility, 'OBSERVE'); + assert.notEqual(value.decision.eligibility, 'BET'); + assert.equal(value.decision.edge, null); + assert.ok(value.summary.en.includes('82%')); + assert.equal(value.extensions[AGENT_EXTENSION_KEY].service_id, 'pm_event_readout'); + assert.ok(!('weather' in value)); + assert.ok(!('orders' in value)); +} + +{ + const { ok, value } = toCopilotResearchUnit( + baseAgent({ + tradability: 'weak', + matrix_status: 'incomplete', + tradability_reasons: ['missing_outcome_prices'], + event_matrix: [{ slug: 'only', yes_price: 0.5 }], + related_market_count: 1 + }), + { unitId: 'unit-partial' } + ); + assert.equal(ok, true); + assert.equal(value.freshness.status, 'partial'); + assert.ok(value.freshness.partialReasons.includes('missing_outcome_prices')); + assert.equal(value.decision.eligibility, 'AVOID'); +} + +{ + const { ok, value } = toCopilotResearchUnit( + baseAgent({ + current_price: {}, + event_matrix: [], + matrix_status: 'incomplete', + tradability: 'weak' + }), + { unitId: 'unit-missing' } + ); + assert.equal(ok, true); + assert.equal(value.freshness.status, 'missing'); + assert.equal(value.decision, null); + assert.equal(value.evidence.hasPrices, false); +} + +{ + const { ok, value } = toCopilotResearchUnit( + baseAgent({ tradability: 'low', matrix_status: 'complete' }), + { unitId: 'unit-low' } + ); + assert.equal(ok, true); + assert.equal(value.decision.eligibility, 'AVOID'); +} + +{ + const { ok, value } = toCopilotResearchUnit( + baseAgent({ hard_gate: 'blocked_for_test' }), + { unitId: 'unit-blocked' } + ); + assert.equal(ok, true); + assert.equal(value.compliance.analysisAllowed, false); + assert.equal(value.decision, null); +} + +{ + const r = toCopilotResearchUnit(baseAgent(), {}); + assert.equal(r.ok, false); + assert.ok(r.errors.some((e) => e.includes('unitId'))); +} + +{ + const r = toCopilotResearchUnit(null, { unitId: 'x' }); + assert.equal(r.ok, false); +} + +console.log('pm-event-dual-surface-test: PASS'); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/run-samples.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/run-samples.mjs new file mode 100644 index 00000000..0eac2a47 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/run-samples.mjs @@ -0,0 +1,51 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { auditDelivery } from '../src/auditor.mjs'; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const sampleInputDir = path.join(rootDir, 'sample-inputs'); +const sampleOutputDir = path.join(rootDir, 'sample-outputs'); + +const cases = [ + ['01-pmquant-rename.json', '01-pmquant-rename-audit.json', ['deferred_build', 'public_release_gate']], + ['02-t310-governance-update.json', '02-t310-governance-update-audit.json', ['deferred_build', 'protocol_tension']], + ['03-alkanes-red-stop.json', '03-alkanes-red-stop-audit.json', ['guard_triggered', 'task_failed_safely']], + ['04-dashboard-curation-readout.json', '04-dashboard-curation-readout-audit.json', ['dirty_state', 'deferred_build']], + ['05-claude-science-readout.json', '05-claude-science-readout-audit.json', ['read_only_delivery', 'hard_gates_declared']] +]; + +let failures = 0; + +for (const [inputFile, expectedFile, requiredFlags] of cases) { + const input = JSON.parse(fs.readFileSync(path.join(sampleInputDir, inputFile), 'utf8')); + const expected = JSON.parse(fs.readFileSync(path.join(sampleOutputDir, expectedFile), 'utf8')); + const actual = auditDelivery(input); + + const errors = []; + if (actual.schema_version !== '0.1') errors.push('schema_version mismatch'); + if (actual.verdict !== expected.verdict) errors.push(`verdict ${actual.verdict} != expected ${expected.verdict}`); + if (typeof actual.score !== 'number' || actual.score < 0 || actual.score > 100) errors.push(`invalid score ${actual.score}`); + for (const key of ['missing', 'risks', 'positive_evidence', 'questions_for_seller', 'machine_flags']) { + if (!Array.isArray(actual[key])) errors.push(`${key} is not an array`); + } + for (const flag of requiredFlags) { + if (!actual.machine_flags.includes(flag)) errors.push(`missing required flag ${flag}`); + } + + if (errors.length) { + failures += 1; + console.error(`FAIL ${inputFile}`); + for (const error of errors) console.error(` - ${error}`); + } else { + console.log(`PASS ${inputFile}: ${actual.verdict} score=${actual.score}`); + } +} + +if (failures) { + console.error(`${failures} sample audit case(s) failed`); + process.exit(1); +} + +console.log('All sample audit cases passed'); + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/schema-smoke.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/schema-smoke.mjs new file mode 100644 index 00000000..1d0b7787 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/schema-smoke.mjs @@ -0,0 +1,15 @@ +import fs from 'node:fs'; + +const files = [ + 'discovery/agent-service.json', + 'discovery/mcp-tool-manifest.json', + 'schemas/agent-transaction-assessment.schema.json', + 'sample-inputs/06-agent-budget-spend-precall.json', + 'sample-outputs/06-agent-budget-spend-precall-assessment.json' +]; + +for (const file of files) { + JSON.parse(fs.readFileSync(file, 'utf8')); + console.log(`PASS json ${file}`); +} + diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/wave-b-services-test.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/wave-b-services-test.mjs new file mode 100644 index 00000000..f7739135 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/wave-b-services-test.mjs @@ -0,0 +1,2314 @@ +import assert from 'node:assert/strict'; +import worker from '../worker/index.mjs'; +import { assessTokenDdVerdictLive } from '../src/token-dd-verdict.mjs'; +import { assessPmTradePreflightLive } from '../src/pm-trade-preflight.mjs'; +import { assessPmEventReadoutLive } from '../src/pm-event-readout.mjs'; +import { assessContentVerifyClaims } from '../src/content-verify-claims.mjs'; + +const BASE = 'https://gate.example.com'; + +// ---- unit: token-dd-verdict ------------------------------------------------- + +{ + const referral = await assessTokenDdVerdictLive({ asset: 'https://rise.rich/ref/abcd' }); + assert.equal(referral.verdict_bucket, 'avoid'); + assert.ok(referral.hard_stops.includes('referral_or_promo_wrapper')); +} + +{ + const ticker = await assessTokenDdVerdictLive({ asset: 'ETH' }); + assert.ok(['watch_only', 'research_position', 'tiny_speculative', 'conviction'].includes(ticker.verdict_bucket)); + assert.notEqual(ticker.verdict_bucket, 'conviction'); + assert.equal(ticker.input.id_type, 'ticker'); +} + +{ + const mockFetch = async (url) => { + if (String(url).includes('dexscreener')) { + return new Response(JSON.stringify({ + pairs: [{ + chainId: 'ethereum', + dexId: 'uniswap', + pairAddress: '0xpair', + liquidity: { usd: 120000 }, + volume: { h24: 50000 }, + priceUsd: '1.23' + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error('unexpected url'); + }; + const contract = await assessTokenDdVerdictLive( + { asset: '0x000000000000000000000000000000000000dead' }, + { fetchImpl: mockFetch } + ); + assert.ok(contract.dex_scan?.pairs_found >= 1); + assert.ok(contract.score_0_100 >= 45); +} + +// ---- unit: pm-trade-preflight ----------------------------------------------- + +{ + const mockGamma = async (url) => { + const u = String(url); + if (!u.includes('gamma-api.polymarket.com/markets')) { + throw new Error(`unexpected ${u}`); + } + return new Response(JSON.stringify([{ + conditionId: '0xabc', + slug: 'demo-slug', + question: 'Will demo happen?', + active: true, + closed: false, + volume24hr: 25000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.42","0.58"]', + bestBid: 0.41, + bestAsk: 0.43 + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + }; + + const preflight = await assessPmTradePreflightLive( + { slug: 'demo-slug', side: 'yes', size_usd: 50 }, + { fetchImpl: mockGamma } + ); + assert.equal(preflight.action, 'eligible'); + assert.equal(preflight.side_price, 0.42); + assert.equal(preflight.service_id, 'pm_trade_preflight'); +} + +{ + const mockGammaClosed = async () => new Response(JSON.stringify([{ + conditionId: '0xclosed', + slug: 'closed-slug', + question: 'Closed market', + active: false, + closed: true, + volume24hr: 0, + outcomes: '["Yes","No"]', + outcomePrices: '["0.5","0.5"]' + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + + const preflight = await assessPmTradePreflightLive( + { slug: 'closed-slug', side: 'yes' }, + { fetchImpl: mockGammaClosed } + ); + assert.equal(preflight.action, 'skip'); +} + +// ---- unit: pm-event-readout (L0 matrix + honest tradability) --------------- + +{ + const marketsBySlug = { + 'demo-readout': [{ + conditionId: '0xreadout', + slug: 'demo-readout', + question: 'Will demo happen by July?', + active: true, + closed: false, + volume24hr: 80000, + oneDayPriceChange: 0.01, + outcomes: '["Yes","No"]', + outcomePrices: '["0.12","0.88"]', + bestBid: 0.11, + bestAsk: 0.13, + endDate: '2026-12-31', + events: [{ id: '1', slug: 'demo-event', title: 'Demo Event' }] + }], + 'fed-hold': [{ + conditionId: '0xfedhold', + slug: 'fed-hold', + question: 'Will there be no change in Fed interest rates after the July 2026 meeting?', + groupItemTitle: 'No change', + active: true, + closed: false, + volume24hr: 700000, + oneDayPriceChange: 0.055, + outcomes: '["Yes","No"]', + outcomePrices: '["0.82","0.18"]', + bestBid: 0.81, + bestAsk: 0.82, + endDate: '2026-07-29T00:00:00Z', + events: [{ id: '287395', slug: 'fed-decision-in-july-181', title: 'Fed Decision in July?' }] + }] + }; + + const fedEvent = [{ + id: '287395', + slug: 'fed-decision-in-july-181', + title: 'Fed Decision in July?', + markets: [ + { + conditionId: '0xfedhold', + slug: 'fed-hold', + question: 'Will there be no change in Fed interest rates after the July 2026 meeting?', + groupItemTitle: 'No change', + active: true, + closed: false, + volume24hr: 700000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.82","0.18"]', + bestBid: 0.81, + bestAsk: 0.82 + }, + { + conditionId: '0xfedhike', + slug: 'fed-hike-25', + question: 'Will the Fed increase interest rates by 25 bps after the July 2026 meeting?', + groupItemTitle: '25 bps increase', + active: true, + closed: false, + volume24hr: 400000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.19","0.81"]', + bestBid: 0.19, + bestAsk: 0.192 + }, + { + conditionId: '0xfedcut', + slug: 'fed-cut-25', + question: 'Will the Fed decrease interest rates by 25 bps after the July 2026 meeting?', + groupItemTitle: '25 bps decrease', + active: true, + closed: false, + volume24hr: 200000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.006","0.994"]', + bestBid: 0.005, + bestAsk: 0.006 + } + ] + }]; + + const mockGamma = async (url) => { + const u = String(url); + if (u.includes('/events?slug=fed-decision-in-july-181')) { + return new Response(JSON.stringify(fedEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=demo-event')) { + return new Response(JSON.stringify([{ + slug: 'demo-event', + title: 'Demo Event', + markets: marketsBySlug['demo-readout'] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=fed-hold')) { + return new Response(JSON.stringify(marketsBySlug['fed-hold']), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=demo-readout')) { + return new Response(JSON.stringify(marketsBySlug['demo-readout']), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const readout = await assessPmEventReadoutLive( + { slug: 'demo-readout' }, + { fetchImpl: mockGamma } + ); + assert.equal(readout.service_id, 'pm_event_readout'); + assert.equal(readout.schema_version, '0.2'); + assert.ok(['weak', 'low', 'medium', 'high'].includes(readout.tradability)); + assert.equal(readout.next_decision_card_needed, 'yes'); + assert.ok(Array.isArray(readout.event_matrix)); + assert.ok(readout.event_matrix.length >= 1); + + const fed = await assessPmEventReadoutLive( + { slug: 'fed-hold' }, + { + fetchImpl: mockGamma, + externalAnchors: [{ + id: 'cme_fedwatch_style', + status: 'ok', + hold_prob: 0.70, + agreement: 'disagree', + detail: 'test inject' + }] + } + ); + assert.equal(fed.category, 'macro_fed'); + assert.equal(fed.category_depth, 'enriched'); + assert.equal(fed.category_plugin.ladder_status, 'rate_ladder'); + assert.equal(fed.category_plugin.leaderboard[0].bucket, 'hold'); + assert.equal(fed.matrix_status, 'complete'); + assert.equal(fed.related_market_count, 3); + assert.ok(fed.event_matrix.some((row) => row.group_item_title === '25 bps increase')); + assert.notEqual(fed.tradability, 'high'); // anchor conflict must cap + assert.ok(fed.tradability_reasons.includes('external_anchor_conflict')); + assert.ok(!String(fed.what_may_not_be_priced || '').includes('Mid-range price')); + assert.ok(String(fed.what_may_not_be_priced || '').includes('Cross-venue gap') + || String(fed.what_is_already_priced || '').includes('majority-priced')); + + // Musk L1 plugin: ladder distribution + count mapping + const muskMarkets = [{ + conditionId: '0xmusk160', + slug: 'elon-musk-of-tweets-july-3-july-10-160-179', + question: 'Will Elon Musk post 160-179 tweets from July 3 to July 10, 2026?', + groupItemTitle: '160-179', + active: true, + closed: false, + volume24hr: 120000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.48","0.52"]', + bestBid: 0.47, + bestAsk: 0.49, + endDate: '2026-07-10T00:00:00Z', + events: [{ id: 'm1', slug: 'elon-musk-of-tweets-july-3-july-10', title: 'Elon Musk # tweets July 3 - July 10, 2026?' }] + }]; + const muskEvent = [{ + slug: 'elon-musk-of-tweets-july-3-july-10', + title: 'Elon Musk # tweets July 3 - July 10, 2026?', + markets: [ + { ...muskMarkets[0] }, + { + conditionId: '0xmusk180', + slug: 'elon-musk-of-tweets-july-3-july-10-180-199', + question: '180-199 tweets?', + groupItemTitle: '180-199', + active: true, + closed: false, + volume24hr: 100000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.27","0.73"]', + bestBid: 0.26, + bestAsk: 0.28 + }, + { + conditionId: '0xmusk140', + slug: 'elon-musk-of-tweets-july-3-july-10-140-159', + question: '140-159 tweets?', + groupItemTitle: '140-159', + active: true, + closed: false, + volume24hr: 90000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.11","0.89"]', + bestBid: 0.10, + bestAsk: 0.12 + } + ] + }]; + const mockMusk = async (url) => { + const u = String(url); + if (u.includes('/events?slug=elon-musk-of-tweets-july-3-july-10')) { + return new Response(JSON.stringify(muskEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=elon-musk-of-tweets-july-3-july-10-160-179')) { + return new Response(JSON.stringify(muskMarkets), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const musk = await assessPmEventReadoutLive( + { + slug: 'elon-musk-of-tweets-july-3-july-10-160-179', + musk: { current_count: 165, hours_left: 12, snapshot_time: '2026-07-09T12:00:00Z' } + }, + { fetchImpl: mockMusk } + ); + assert.equal(musk.category, 'musk'); + assert.equal(musk.category_depth, 'enriched'); + assert.ok(musk.category_plugin); + assert.equal(musk.category_plugin.modal_bucket.bucket, '160-179'); + assert.equal(musk.category_plugin.count_vs_ladder.status, 'mapped'); + assert.ok(musk.category_plugin.full_bucket_surface.length >= 3); + assert.ok(musk.category_plugin.batch2_public); + assert.equal(musk.category_plugin.batch2_public.category, 'musk'); + assert.equal(musk.category_plugin.batch2_public.fixture_status, 'ok'); + assert.equal(musk.category_plugin.batch2_public.matrix_status, 'complete'); + assert.ok(musk.category_plugin.batch2_public.market_implied_shape); + assert.ok(musk.category_plugin.batch2_public.thesis); + assert.equal(musk.category_plugin.batch2_public.best_expression.market, 'elon-musk-of-tweets-july-3-july-10-160-179'); + assert.ok(['watch', 'skip', 'no_trade'].includes(musk.category_plugin.batch2_public.action)); + assert.equal(typeof musk.category_plugin.batch2_public.confidence, 'number'); + assert.ok(Array.isArray(musk.category_plugin.batch2_public.risk_flags)); + assert.ok(musk.category_plugin.batch2_public.postmortem_key); + assert.ok(!('bankroll_pct' in musk.category_plugin.batch2_public)); + assert.ok(!('leo_private' in musk)); + + // Without count: honest freshness + tradability cap + Batch2 action=skip + const muskNoCount = await assessPmEventReadoutLive( + { slug: 'elon-musk-of-tweets-july-3-july-10-160-179' }, + { fetchImpl: mockMusk } + ); + assert.equal(muskNoCount.category_plugin.count_source_freshness.status, 'missing'); + assert.equal(muskNoCount.category_plugin.count_vs_ladder.status, 'no_count'); + assert.equal(muskNoCount.category_plugin.tradability_cap, 'medium'); + assert.notEqual(muskNoCount.tradability, 'high'); + assert.ok(muskNoCount.tradability_reasons.includes('musk_count_snapshot_missing')); + assert.equal(muskNoCount.category_plugin.batch2_public.fixture_status, 'failed_or_unverified'); + assert.equal(muskNoCount.category_plugin.batch2_public.action, 'skip'); + assert.ok(muskNoCount.category_plugin.batch2_public.risk_flags.includes('count_snapshot_missing')); + + // Football L1: must merge more-markets siblings; bare ML-only = incomplete + const fraMainMarkets = [{ + conditionId: '0xfra', + slug: 'fifwc-fra-mar-2026-07-09-fra', + question: 'Will France win on 2026-07-09?', + groupItemTitle: 'France', + sportsMarketType: 'moneyline', + active: true, + closed: false, + volume24hr: 500000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.615","0.385"]', + bestBid: 0.61, + bestAsk: 0.62, + endDate: '2026-07-09T20:00:00Z', + eventStartTime: '2026-07-09T20:00:00Z', + events: [{ id: 'fm', slug: 'fifwc-fra-mar-2026-07-09', title: 'France vs. Morocco' }] + }]; + const fraMainEvent = [{ + id: 'parent-fra-mar', + slug: 'fifwc-fra-mar-2026-07-09', + title: 'France vs. Morocco', + startTime: '2026-07-09T20:00:00Z', + markets: [ + { ...fraMainMarkets[0] }, + { + conditionId: '0xdraw', + slug: 'fifwc-fra-mar-2026-07-09-draw', + question: 'Draw (France vs. Morocco)', + groupItemTitle: 'Draw (France vs. Morocco)', + sportsMarketType: 'moneyline', + active: true, + closed: false, + volume24hr: 200000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.245","0.755"]', + bestBid: 0.24, + bestAsk: 0.25 + }, + { + conditionId: '0xmar', + slug: 'fifwc-fra-mar-2026-07-09-mar', + question: 'Will Morocco win?', + groupItemTitle: 'Morocco', + sportsMarketType: 'moneyline', + active: true, + closed: false, + volume24hr: 150000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.135","0.865"]', + bestBid: 0.13, + bestAsk: 0.14 + } + ] + }]; + const fraMoreEvent = [{ + id: 'child-more', + slug: 'fifwc-fra-mar-2026-07-09-more-markets', + title: 'France vs. Morocco - More Markets', + parentEventId: 'parent-fra-mar', + markets: [ + { + conditionId: '0xtot25', + slug: 'fifwc-fra-mar-2026-07-09-total-2pt5', + question: 'O/U 2.5', + groupItemTitle: 'O/U 2.5', + sportsMarketType: 'totals', + active: true, + closed: false, + volume24hr: 100000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.475","0.525"]', + bestBid: 0.47, + bestAsk: 0.48 + }, + { + conditionId: '0xtot15', + slug: 'fifwc-fra-mar-2026-07-09-total-1pt5', + question: 'O/U 1.5', + groupItemTitle: 'O/U 1.5', + sportsMarketType: 'totals', + active: true, + closed: false, + volume24hr: 80000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.745","0.255"]', + bestBid: 0.74, + bestAsk: 0.75 + }, + { + conditionId: '0xsp15', + slug: 'fifwc-fra-mar-2026-07-09-spread-home-1pt5', + question: 'France (-1.5)', + groupItemTitle: 'France (-1.5)', + sportsMarketType: 'spreads', + active: true, + closed: false, + volume24hr: 90000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.345","0.655"]', + bestBid: 0.34, + bestAsk: 0.35 + }, + { + conditionId: '0xbtts', + slug: 'fifwc-fra-mar-2026-07-09-btts', + question: 'Both Teams to Score', + groupItemTitle: 'Both Teams to Score', + sportsMarketType: 'both_teams_to_score', + active: true, + closed: false, + volume24hr: 70000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.495","0.505"]', + bestBid: 0.49, + bestAsk: 0.50 + }, + { + conditionId: '0xtt', + slug: 'fifwc-fra-mar-2026-07-09-team-total-home-1pt5', + question: 'France O/U 1.5', + groupItemTitle: 'France O/U 1.5', + sportsMarketType: 'soccer_team_totals', + active: true, + closed: false, + volume24hr: 60000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.545","0.455"]', + bestBid: 0.54, + bestAsk: 0.55 + }, + { + conditionId: '0xadv', + slug: 'fifwc-fra-mar-2026-07-09-team-to-advance', + question: 'Team to Advance', + groupItemTitle: 'Team to Advance', + sportsMarketType: 'soccer_team_to_advance', + active: true, + closed: false, + volume24hr: 200000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.775","0.225"]', + bestBid: 0.77, + bestAsk: 0.78 + } + ] + }]; + + const mockFootball = async (url) => { + const u = String(url); + if (u.includes('/markets?slug=fifwc-fra-mar-2026-07-09-fra')) { + return new Response(JSON.stringify(fraMainMarkets), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('parent_event_id=parent-fra-mar')) { + return new Response(JSON.stringify(fraMoreEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=fifwc-fra-mar-2026-07-09-more-markets')) { + return new Response(JSON.stringify(fraMoreEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=fifwc-fra-mar-2026-07-09-team-to-advance')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=fifwc-fra-mar-2026-07-09-exact-score')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=fifwc-fra-mar-2026-07-09')) { + return new Response(JSON.stringify(fraMainEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const football = await assessPmEventReadoutLive( + { + slug: 'fifwc-fra-mar-2026-07-09-fra', + football: { + verified: true, + market_fixture_match: 'yes', + scheduled_time_utc: '2026-07-09T20:00:00Z', + fixture_sources: ['test_fixture'] + } + }, + { fetchImpl: mockFootball } + ); + assert.equal(football.category, 'football'); + assert.equal(football.category_depth, 'enriched'); + assert.equal(football.matrix_status, 'complete'); + assert.equal(football.category_plugin.matrix_completeness.status, 'complete'); + assert.deepEqual(football.category_plugin.hard_veto_gaps, []); + assert.ok(football.related_market_count >= 9); + assert.ok(football.category_plugin.sibling_event_slugs.includes('fifwc-fra-mar-2026-07-09-more-markets')); + assert.equal(football.category_plugin.fixture.fixture_status, 'ok'); + assert.ok(football.category_plugin.expression_comparison.candidates.length >= 2); + assert.ok(football.category_plugin.market_implied_shape.central_thesis); + assert.ok(football.category_plugin.expression_comparison.ladder_context); + assert.ok(football.category_plugin.expression_comparison.ladder_context.totals_ladder_count >= 2); + // Draw mass thesis must not recommend advance (coherence gate) + assert.ok( + football.category_plugin.market_implied_shape.state_flags.includes('draw_has_material_mass') + ); + assert.notEqual( + football.category_plugin.expression_comparison.recommended?.expression, + 'team_to_advance' + ); + assert.ok( + ['draw_90m', 'totals_pivot'].includes( + football.category_plugin.expression_comparison.recommended?.expression + ) + ); + assert.equal(football.category_plugin.discovery, 'parent_event_id'); + + // Without verification, fixture must not be ok and action hint no_trade + const footballUnverified = await assessPmEventReadoutLive( + { slug: 'fifwc-fra-mar-2026-07-09-fra' }, + { fetchImpl: mockFootball } + ); + assert.notEqual(footballUnverified.category_plugin.fixture.fixture_status, 'ok'); + assert.equal(footballUnverified.category_plugin.default_action_hint, 'no_trade'); + assert.notEqual(footballUnverified.tradability, 'high'); +} + + // Tennis L1: named ML + format + domination check + fixture gate +{ + const tennisMarkets = [{ + conditionId: '0xtml', + slug: 'wta-muchova-gauff-2026-07-09', + question: 'Wimbledon WTA: Karolina Muchova vs Coco Gauff', + sportsMarketType: 'moneyline', + active: true, + closed: false, + volume24hr: 400000, + outcomes: '["Karolina Muchova","Coco Gauff"]', + outcomePrices: '["0.435","0.565"]', + bestBid: 0.43, + bestAsk: 0.44, + endDate: '2026-07-09T18:00:00Z', + eventStartTime: '2026-07-09T14:00:00Z', + events: [{ id: 'tm', slug: 'wta-muchova-gauff-2026-07-09', title: 'Wimbledon WTA: Karolina Muchova vs Coco Gauff' }] + }]; + const tennisEvent = [{ + id: 'parent-tennis', + slug: 'wta-muchova-gauff-2026-07-09', + title: 'Wimbledon WTA: Karolina Muchova vs Coco Gauff', + startTime: '2026-07-09T14:00:00Z', + markets: [ + { ...tennisMarkets[0] }, + { + conditionId: '0xtsh', + slug: 'wta-muchova-gauff-2026-07-09-set-handicap-home-1pt5', + question: 'Set Handicap: Muchova (-1.5) vs Gauff (+1.5)', + groupItemTitle: 'Set Handicap +/-1.5', + sportsMarketType: 'tennis_set_handicap', + active: true, + closed: false, + volume24hr: 80000, + outcomes: '["Muchova","Gauff"]', + outcomePrices: '["0.30","0.70"]', + bestBid: 0.28, + bestAsk: 0.32 + }, + { + conditionId: '0xtsets', + slug: 'wta-muchova-gauff-2026-07-09-set-totals-2pt5', + question: 'Total Sets O/U 2.5', + groupItemTitle: 'Total Sets: O/U 2.5', + sportsMarketType: 'tennis_set_totals', + active: true, + closed: false, + volume24hr: 90000, + outcomes: '["Over 2.5","Under 2.5"]', + outcomePrices: '["0.62","0.38"]', + bestBid: 0.60, + bestAsk: 0.64 + }, + { + conditionId: '0xtmg', + slug: 'wta-muchova-gauff-2026-07-09-match-total-22pt5', + question: 'Match O/U 22.5', + groupItemTitle: 'Match O/U 22.5', + sportsMarketType: 'tennis_match_totals', + active: true, + closed: false, + volume24hr: 70000, + outcomes: '["Over","Under"]', + outcomePrices: '["0.55","0.45"]', + bestBid: 0.53, + bestAsk: 0.57 + }, + { + conditionId: '0xts1', + slug: 'wta-muchova-gauff-2026-07-09-first-set-winner', + question: 'Set 1 Winner', + groupItemTitle: 'Set 1 Winner', + sportsMarketType: 'tennis_first_set_winner', + active: true, + closed: false, + volume24hr: 50000, + outcomes: '["Muchova","Gauff"]', + outcomePrices: '["0.40","0.60"]', + bestBid: 0.39, + bestAsk: 0.41 + }, + { + conditionId: '0xtcm', + slug: 'wta-muchova-gauff-2026-07-09-completed-match', + question: 'Completed Match', + groupItemTitle: 'Completed Match', + sportsMarketType: 'tennis_completed_match', + active: true, + closed: false, + volume24hr: 10000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.95","0.05"]', + bestBid: 0.94, + bestAsk: 0.96 + } + ] + }]; + + const mockTennis = async (url) => { + const u = String(url); + if (u.includes('/markets?slug=wta-muchova-gauff-2026-07-09')) { + return new Response(JSON.stringify(tennisMarkets), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('parent_event_id=parent-tennis')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?slug=wta-muchova-gauff-2026-07-09')) { + return new Response(JSON.stringify(tennisEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const tennis = await assessPmEventReadoutLive( + { + slug: 'wta-muchova-gauff-2026-07-09', + tennis: { + verified: true, + market_fixture_match: 'yes', + scheduled_time_utc: '2026-07-09T14:00:00Z', + fixture_sources: ['test_fixture'], + tournament: 'Wimbledon', + format: 'best_of_3' + } + }, + { fetchImpl: mockTennis } + ); + assert.equal(tennis.category, 'tennis'); + assert.equal(tennis.category_depth, 'enriched'); + assert.equal(tennis.matrix_status, 'complete'); + assert.equal(tennis.category_plugin.matrix_completeness.status, 'complete'); + assert.deepEqual(tennis.category_plugin.hard_veto_gaps, []); + assert.equal(tennis.category_plugin.format.best_of, 3); + assert.ok(tennis.category_plugin.market_implied_shape.moneyline.player_a); + assert.ok(tennis.category_plugin.market_implied_shape.moneyline.player_b); + assert.ok(tennis.category_plugin.expression_comparison.candidates.length >= 2); + assert.ok(tennis.category_plugin.straight_set_domination_check); + assert.equal(tennis.category_plugin.fixture.fixture_status, 'ok'); + assert.ok(tennis.current_price.named?.length >= 2); + assert.equal( + tennis.current_price.named[0].price, + tennis.category_plugin.market_implied_shape.moneyline.player_a.yes + ); + // Mock underdog cover at 0.70 is actionable; must not pick a full price + assert.ok(tennis.category_plugin.expression_comparison.recommended); + assert.ok( + !['full', 'rich', 'no_edge'].includes( + tennis.category_plugin.expression_comparison.recommended.price_status + ) + ); + + const tennisUnverified = await assessPmEventReadoutLive( + { slug: 'wta-muchova-gauff-2026-07-09' }, + { fetchImpl: mockTennis } + ); + assert.notEqual(tennisUnverified.category_plugin.fixture.fixture_status, 'ok'); + assert.equal(tennisUnverified.category_plugin.default_action_hint, 'no_trade'); + assert.notEqual(tennisUnverified.tradability, 'high'); +} + +// ---- unit: content-verify-claims ------------------------------------------ + +{ + const pass = assessContentVerifyClaims({ + claims: ['OKX marketplace has 358 ASPs and 2982 cumulative calls.'], + sources: [{ text: 'Scan found 358 unique ASPs and 2982 soldCount on 2026-07-07.' }] + }); + assert.ok(['pass', 'needs_review'].includes(pass.verdict)); + assert.equal(pass.service_id, 'content_verify_claims'); +} + +{ + const fail = assessContentVerifyClaims({ + claims: ['Revenue hit 10 million USD yesterday.'], + sources: [{ text: 'The product is still in beta with zero customers.' }] + }); + assert.ok(['fail', 'needs_review'].includes(fail.verdict)); + assert.ok(fail.unsupported.length >= 1 || fail.conflicts.length >= 1); +} + +{ + // Mixed: one supported + one needs_review must not throw (buildConsensus bugfix). + const mixed = assessContentVerifyClaims({ + claims: [ + 'OKX marketplace has 358 ASPs.', + 'Something vague about synergy tomorrow.' + ], + sources: [{ text: 'Scan found 358 unique ASPs on 2026-07-07.' }] + }); + assert.equal(typeof mixed.consensus, 'string'); + assert.ok(mixed.consensus.includes('need review') || mixed.verdict === 'needs_review' || mixed.supported.length >= 1); +} + +{ + const { assessContentSlopCheck } = await import('../src/content-slop-check.mjs'); + const sloppy = assessContentSlopCheck({ + text: "In today's digital landscape, it is crucial to delve into synergy. As an AI, I am excited to underscore this game-changer." + }); + assert.equal(sloppy.service_id, 'content_slop_check'); + assert.ok(sloppy.slop_score_0_100 >= 30); + assert.ok(sloppy.slop_flags.length >= 2); +} + +// ---- unit: pm-brier --------------------------------------------------------- + +{ + const { assessPmBrierLive } = await import('../src/pm-brier.mjs'); + const mockBrier = async (url) => { + const u = String(url); + if (u.includes('data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([ + { title: 'A', redeemable: true, avgPrice: 0.7, currentValue: 1 }, + { title: 'B', redeemable: true, avgPrice: 0.2, currentValue: 0 }, + { title: 'C', redeemable: false, avgPrice: 0.5, currentValue: 0.5 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const brier = await assessPmBrierLive( + { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a' }, + { fetchImpl: mockBrier } + ); + assert.equal(brier.service_id, 'pm_brier'); + assert.equal(brier.settled_markets, 2); + assert.equal(brier.wins, 1); + // mean((0.7-1)^2 + (0.2-0)^2) = (0.09 + 0.04)/2 = 0.065 + assert.equal(brier.brier, 0.065); + // 2026-07-30: a 2-market sample is below the rateable threshold, so the service + // must withhold a verdict instead of calling it "good" (previous behaviour). + assert.equal(brier.rating, 'not_rateable'); + assert.equal(brier.sample_bias, 'sample_below_rateable_threshold'); + // base rate 0.5 → predicting it every time scores 0.5*0.5 = 0.25 + assert.equal(brier.baseline_brier, 0.25); + assert.equal(brier.skill_vs_baseline, 0.185); + assert.ok(brier.confidence_gaps.includes('redeemed_winners_absent_from_positions_page')); +} + +// pm-brier: zero-win sample is the survivorship-bias signature — never rate it. +{ + const { assessPmBrierLive } = await import('../src/pm-brier.mjs'); + const allLosses = async (url) => { + if (String(url).includes('data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([ + { title: 'L1', redeemable: true, avgPrice: 0.33, currentValue: 0 }, + { title: 'L2', redeemable: true, avgPrice: 0.29, currentValue: 0 }, + { title: 'L3', redeemable: true, avgPrice: 0.4, currentValue: 0 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${url}`); + }; + const out = await assessPmBrierLive( + { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a' }, + { fetchImpl: allLosses } + ); + assert.equal(out.wins, 0); + assert.equal(out.win_rate, 0); + assert.equal(out.sample_bias, 'zero_wins_survivorship_suspected'); + assert.equal(out.rating, 'not_rateable'); + // cheap losing longshots still produce a low raw Brier — that must not read as skill + assert.ok(out.brier < 0.15); + assert.ok(out.skill_vs_baseline < 0); + assert.ok(out.buyer_summary_zh.includes('不给评级')); +} + +// ---- unit: sports upset max_prob + smart-money cohort ----------------------- + +{ + const { assessSportsUpsetAlertLive } = await import('../src/sports-upset-alert.mjs'); + const marketsPayload = [{ + conditionId: '0xm1', + slug: 'underdog-yes', + question: 'Underdog wins?', + active: true, + closed: false, + volume24hr: 90000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.22","0.78"]' + }, { + conditionId: '0xm2', + slug: 'other-yes', + question: 'Other event?', + active: true, + closed: false, + volume24hr: 80000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.40","0.60"]' + }]; + + const mockUpset = async (url) => { + const u = String(url); + if (u.includes('/events?') || u.includes('/markets?') || u.includes('public-search')) { + if (u.includes('/markets?')) { + return new Response(JSON.stringify(marketsPayload), { status: 200, headers: { 'content-type': 'application/json' } }); + } + return new Response(JSON.stringify([{ + title: 'Sports card', + closed: false, + volume24hr: 100000, + markets: marketsPayload + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/trades?')) { + const market = u.includes('0xm1') ? '0xm1' : '0xm2'; + const price = market === '0xm1' ? 0.22 : 0.4; + return new Response(JSON.stringify([{ + proxyWallet: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + side: 'BUY', + size: 2000, + price, + timestamp: 1_700_000_000, + outcome: 'Yes', + conditionId: market, + title: 'm' + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('lb-api.polymarket.com/profit')) { + return new Response(JSON.stringify([{ proxyWallet: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', amount: 1200 }]), { + status: 200, headers: { 'content-type': 'application/json' } + }); + } + if (u.includes('/positions?')) { + return new Response(JSON.stringify([{ totalBought: 2000, avgPrice: 0.22, cashPnl: 10 }]), { + status: 200, headers: { 'content-type': 'application/json' } + }); + } + throw new Error(`unexpected ${u}`); + }; + + const tight = await assessSportsUpsetAlertLive( + { sport: 'football', league: 'epl', max_prob: 0.15, limit: 5 }, + { fetchImpl: mockUpset } + ); + assert.equal(tight.input.max_prob, 0.15); + assert.equal(tight.upset_alerts.length, 0); + + const loose = await assessSportsUpsetAlertLive( + { sport: 'football', league: 'epl', max_prob: 0.35, limit: 5 }, + { fetchImpl: mockUpset } + ); + assert.ok(loose.upset_alerts.length >= 1); + assert.ok(Array.isArray(loose.wallet_cohort)); +} + +{ + const { assessSportsSmartMoneyLive } = await import('../src/worldcup-smart-money-live.mjs'); + const mockSm = async (url) => { + const u = String(url); + if (u.includes('/events?') || u.includes('public-search') || u.includes('/markets?')) { + const markets = [{ + conditionId: '0xa', + slug: 'm-a', + question: 'Match A', + active: true, + closed: false, + volume24hr: 50000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.5","0.5"]' + }, { + conditionId: '0xb', + slug: 'm-b', + question: 'Match B', + active: true, + closed: false, + volume24hr: 40000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.5","0.5"]' + }]; + if (u.includes('/markets?')) { + return new Response(JSON.stringify(markets), { status: 200, headers: { 'content-type': 'application/json' } }); + } + return new Response(JSON.stringify([{ title: 'card', closed: false, markets }]), { + status: 200, headers: { 'content-type': 'application/json' } + }); + } + if (u.includes('/trades?')) { + return new Response(JSON.stringify([{ + proxyWallet: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + side: 'BUY', size: 3000, price: 0.45, timestamp: 1_700_000_100, + outcome: 'Yes', conditionId: u.includes('0xa') ? '0xa' : '0xb' + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('lb-api')) { + return new Response(JSON.stringify([{ amount: 500 }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/positions?')) { + return new Response(JSON.stringify([{ totalBought: 3000, avgPrice: 0.45, cashPnl: 1 }]), { + status: 200, headers: { 'content-type': 'application/json' } + }); + } + throw new Error(`unexpected ${u}`); + }; + const sm = await assessSportsSmartMoneyLive( + { sport: 'tennis', limit: 3 }, + { fetchImpl: mockSm } + ); + assert.ok(Array.isArray(sm.wallet_cohort)); + assert.ok(sm.wallet_cohort.some((w) => w.cross_market === true)); + assert.ok(sm.schema_version === '0.3'); +} + +{ + const { + assessWorldCupSmartMoneyLive + } = await import('../src/worldcup-smart-money-live.mjs'); + const { + assessWorldCupUpsetAlertLive + } = await import('../src/sports-upset-alert.mjs'); + + const footballMarkets = [{ + conditionId: '0xfoot1', + slug: 'soccer-upset-yes', + question: 'Will the underdog win the football match?', + active: true, + closed: false, + volume24hr: 90000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.22","0.78"]' + }, { + conditionId: '0xfoot2', + slug: 'soccer-other-yes', + question: 'Will another football match happen?', + active: true, + closed: false, + volume24hr: 75000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.48","0.52"]' + }]; + + const mockWorldCupExpand = async (url) => { + const u = String(url); + const parsed = new URL(u); + if (u.includes('/events?')) { + const tag = parsed.searchParams.get('tag_slug'); + if (tag === 'world-cup') { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (tag === 'soccer' || tag === 'football') { + return new Response(JSON.stringify([{ + title: 'Football markets', + closed: false, + volume24hr: 165000, + markets: footballMarkets + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('public-search') || u.includes('/markets?')) { + return new Response(JSON.stringify(u.includes('public-search') ? { events: [] } : []), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + if (u.includes('/trades?')) { + const market = u.includes('0xfoot1') ? '0xfoot1' : '0xfoot2'; + const price = market === '0xfoot1' ? 0.22 : 0.48; + return new Response(JSON.stringify([{ + proxyWallet: '0xcccccccccccccccccccccccccccccccccccccccc', + side: 'BUY', + size: 3000, + price, + timestamp: 1_700_000_200, + outcome: 'Yes', + conditionId: market + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('lb-api')) { + return new Response(JSON.stringify([{ amount: 1500 }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/positions?')) { + return new Response(JSON.stringify([{ totalBought: 3000, avgPrice: 0.22, cashPnl: 5 }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + throw new Error(`unexpected ${u}`); + }; + + const radar = await assessWorldCupSmartMoneyLive( + { query: 'all', limit: 3 }, + { fetchImpl: mockWorldCupExpand } + ); + assert.equal(radar.source.scope_expanded, true); + assert.equal(radar.source.requested_scope, 'world_cup'); + assert.equal(radar.source.effective_scope, 'football'); + assert.ok(radar.buyer_summary_en.includes('expanded')); + assert.ok(radar.signals.length >= 1); + + const upset = await assessWorldCupUpsetAlertLive( + { query: 'all', limit: 3, max_prob: 0.35 }, + { fetchImpl: mockWorldCupExpand } + ); + assert.equal(upset.source.discovery.scope_expanded, true); + assert.equal(upset.source.discovery.requested_scope, 'world_cup'); + assert.equal(upset.source.discovery.effective_scope, 'football'); + assert.ok(upset.buyer_summary_en.includes('expanded')); + assert.ok(upset.upset_alerts.length >= 1); +} + +// ---- unit: nba category plugin --------------------------------------------- + +{ + const { enrichNbaCategory, classifyNbaGroup } = await import('../src/pm-category-nba.mjs'); + assert.equal(classifyNbaGroup({ + sports_market_type: 'moneyline', + group_item_title: 'Lakers', + title: 'Lakers vs Celtics', + yes: 0.58 + }), 'moneyline'); + assert.equal(classifyNbaGroup({ + sports_market_type: 'spreads', + group_item_title: 'Lakers -4.5', + title: 'Spread', + yes: 0.51 + }), 'spreads_ladder'); + + const plugin = enrichNbaCategory({ + market: { title: 'NBA: Lakers vs Celtics', slug: 'nba-lal-bos', yes: 0.58 }, + eventBundle: { title: 'Lakers vs Celtics', slug: 'nba-lal-bos-2026' }, + eventMatrix: [ + { title: 'Lakers', group_item_title: 'Lakers', slug: 'lal', yes: 0.58, sports_market_type: 'moneyline', is_primary: true }, + { title: 'Celtics', group_item_title: 'Celtics', slug: 'bos', yes: 0.42, sports_market_type: 'moneyline' }, + { title: 'Spread', group_item_title: 'Lakers -4.5', slug: 'spread', yes: 0.5, sports_market_type: 'spreads' }, + { title: 'Total', group_item_title: 'O/U 224.5', slug: 'total', yes: 0.52, sports_market_type: 'totals' } + ], + fixture: { + verified: true, + market_fixture_match: 'yes', + scheduled_time_utc: '2026-07-24T01:00:00Z', + home_team: 'Lakers', + away_team: 'Celtics' + } + }); + assert.equal(plugin.category, 'nba'); + assert.equal(plugin.matrix_status, 'complete'); + assert.equal(plugin.fixture.fixture_status, 'ok'); + assert.ok(plugin.coherence?.coherence_status); +} + +// ---- unit: pm-decision-card ------------------------------------------------ + +{ + const { assessPmDecisionCardLive } = await import('../src/pm-decision-card.mjs'); + const mockDc = async (url) => { + const u = String(url); + if (u.includes('/markets?')) { + return new Response(JSON.stringify([{ + conditionId: '0xdec', + slug: 'demo-decision', + question: 'Will demo happen?', + active: true, + closed: false, + volume24hr: 25000, + outcomes: '["Yes","No"]', + outcomePrices: '["0.42","0.58"]', + bestBid: 0.41, + bestAsk: 0.43, + events: [{ slug: 'demo-event', title: 'Demo event' }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events')) { + return new Response(JSON.stringify([{ + slug: 'demo-event', + title: 'Demo event', + closed: false, + markets: [{ + conditionId: '0xdec', + slug: 'demo-decision', + question: 'Will demo happen?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.42","0.58"]', + volume24hr: 25000, + active: true, + closed: false + }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const card = await assessPmDecisionCardLive( + { slug: 'demo-decision', side: 'yes' }, + { fetchImpl: mockDc } + ); + assert.equal(card.service_id, 'pm_decision_card'); + assert.equal(card.schema_version, '0.4'); + assert.ok(['skip', 'watch', 'eligible_for_manual_review'].includes(card.action)); + assert.ok(['no_edge', 'data_blocked', 'manual_micro_validation', 'strong_micro_candidate', 'event_outcome'].includes(card.opportunity_state)); + assert.ok(['event_outcome', 'price_edge', 'no_trade'].includes(card.decision_mode)); + assert.equal(card.threshold_source, 'asp_public_heuristic'); + assert.equal(card.threshold_version, 'v0.4'); + assert.ok(['cheap', 'acceptable', 'full', 'rich', 'no_edge'].includes(card.price_status)); + assert.equal(card.hard_gate, 'no_orders_no_signing_no_wallet_custody_no_leo_private_bankroll'); + assert.ok(Array.isArray(card.missing_evidence)); + assert.ok(card.missing_evidence.includes('caller_size_or_bankroll_not_supplied')); + assert.ok(['pending_caller_size', 'pending_anchor'].includes(card.order_quantity_shares) + || typeof card.order_quantity_shares === 'number'); + assert.equal(card.sizing_authority, 'caller_supplied_bankroll_or_size_usd'); + assert.ok(['ok', 'conflict', 'incomplete'].includes(card.consistency_check)); + assert.equal(card.decision_card?.opportunity_state, card.opportunity_state); + assert.ok(card.buyer_summary_en.includes('opportunity_state=')); + assert.ok(card.value_loop?.stale_after_minutes); + assert.ok(card.value_loop?.stale_at); + assert.ok(card.buyer_summary_en); + assert.ok(card.paid_checks?.checks?.length >= 6); + assert.equal(typeof card.paid_checks.pass_count, 'number'); + assert.ok(card.decision_card?.next_actions?.length >= 1); + + const sized = await assessPmDecisionCardLive( + { + slug: 'demo-decision', + side: 'yes', + size_usd: 20, + bankroll_usd: 1000, + existing_exposure_usd: 0, + fair_prob: 0.55 + }, + { fetchImpl: mockDc } + ); + assert.equal(typeof sized.order_quantity_shares, 'number'); + assert.ok(sized.order_quantity_shares >= 1); + assert.equal(typeof sized.estimated_cost_u, 'number'); + assert.ok(['B_minus_probe', 'caller_supplied'].includes(sized.sizing_role)); + assert.ok(!sized.missing_evidence.includes('caller_size_or_bankroll_not_supplied')); +} + +// ---- unit: pm-market-scan (pure ranker) ------------------------------------- + +{ + const { rankMarketsForScan, assessPmMarketScanLive } = await import('../src/pm-market-scan.mjs'); + const ranked = rankMarketsForScan([ + { slug: 'low', question: 'Low', active: true, closed: false, volume24hr: 500, spread: 0.01 }, + { slug: 'wide', question: 'Wide', active: true, closed: false, volume24hr: 9000, spread: 0.05 }, + { slug: 'top', question: 'Top', active: true, closed: false, volume24hr: 50000, spread: 0.01 }, + { slug: 'closed', question: 'Closed', active: false, closed: true, volume24hr: 99999, spread: 0.01 } + ], { minVolume24hr: 1000, limit: 10 }); + assert.equal(ranked.length, 2); + assert.equal(ranked[0].slug, 'top'); + assert.equal(ranked[1].slug, 'wide'); + + const mockScan = async (url) => { + const u = String(url); + if (!u.includes('gamma-api.polymarket.com/markets')) throw new Error(`unexpected ${u}`); + return new Response(JSON.stringify([ + { slug: 'scan-a', question: 'A?', active: true, closed: false, volume24hr: 12000, spread: 0.02, bestBid: 0.4, bestAsk: 0.42 }, + { slug: 'scan-b', question: 'B?', active: true, closed: false, volume24hr: 800, spread: 0.01 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + }; + const scan = await assessPmMarketScanLive({ limit: 5, min_volume: 1000 }, { fetchImpl: mockScan }); + assert.equal(scan.service_id, 'pm_market_scan'); + assert.equal(scan.market_count, 1); + assert.equal(scan.markets[0].slug, 'scan-a'); +} + +// ---- unit: pm-market-health (classify via mock gamma) ----------------------- + +{ + const { assessPmMarketHealthLive } = await import('../src/pm-market-health.mjs'); + const mockTight = async (url) => { + const u = String(url); + if (!u.includes('gamma-api.polymarket.com/markets')) throw new Error(`unexpected ${u}`); + return new Response(JSON.stringify([{ + slug: 'health-tight', + question: 'Tight book?', + active: true, + closed: false, + volume24hr: 25000, + spread: 0.02, + bestBid: 0.48, + bestAsk: 0.5, + outcomePrices: '["0.49","0.51"]', + liquidityNum: 100000 + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + }; + const tight = await assessPmMarketHealthLive({ slug: 'health-tight' }, { fetchImpl: mockTight }); + assert.equal(tight.service_id, 'pm_market_health'); + assert.equal(tight.health_verdict, 'ok_tight'); + assert.equal(tight.primary.overround, 1); + + const mockWide = async () => new Response(JSON.stringify([{ + slug: 'health-wide', + question: 'Wide?', + active: true, + closed: false, + volume24hr: 10000, + spread: 0.12, + outcomePrices: '["0.4","0.6"]', + bestBid: 0.35, + bestAsk: 0.47 + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + const wide = await assessPmMarketHealthLive({ slug: 'health-wide' }, { fetchImpl: mockWide }); + assert.equal(wide.health_verdict, 'wide_spread'); +} + +// ---- unit: pm-wallet-report (compose with mocks) ---------------------------- + +{ + const { assessPmWalletReportLive } = await import('../src/pm-wallet-report.mjs'); + const address = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a'; + const mockWallet = async (url) => { + const u = String(url); + if (u.includes('lb-api.polymarket.com/profit')) { + return new Response(JSON.stringify([{ + amount: 15, + name: 'demo', + proxyWallet: address + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([ + { title: 'A', size: 10, avgPrice: 0.4, cashPnl: 10, currentValue: 14, redeemable: true }, + { title: 'B', size: 5, avgPrice: 0.2, cashPnl: 3, currentValue: 0, redeemable: true }, + { title: 'C', size: 2, avgPrice: 0.5, cashPnl: 1, currentValue: 3, redeemable: false } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('data-api.polymarket.com/activity')) { + return new Response(JSON.stringify([{ type: 'TRADE' }, { type: 'TRADE' }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + throw new Error(`unexpected ${u}`); + }; + const report = await assessPmWalletReportLive( + { address, pnl_mode: 'quick' }, + { fetchImpl: mockWallet } + ); + assert.equal(report.service_id, 'pm_wallet_report'); + assert.equal(report.layers.pnl_audit.divergence_verdict, 'aligned'); + assert.ok(['trust_for_copy', 'trust_with_calibration_check', 'verify_manually', 'distrust_claims'].includes(report.composite_action)); + assert.equal(typeof report.layers.profile.pnl_7d, 'number'); + assert.ok(report.layers.brier.rating); + assert.ok(report.buyer_summary_zh.includes('钱包一页纸')); +} + +// ---- unit: pm-updown-readout (mock gamma) ----------------------------------- + +{ + const { assessPmUpdownReadoutLive } = await import('../src/pm-updown-readout.mjs'); + const mockUpdown = async (url) => { + const u = String(url); + if (u.includes('/events?slug=')) { + return new Response(JSON.stringify([{ + title: 'Bitcoin Up or Down - July 26', + slug: 'btc-updown-demo', + endDate: '2026-07-26T12:00:00Z', + resolutionSource: 'https://www.binance.com/en/trade/BTC_USDT', + markets: [{ + question: 'Bitcoin Up or Down', + slug: 'btc-updown-m1', + conditionId: '0xup', + groupItemTitle: 'Up', + outcomePrices: '["0.55","0.45"]', + bestBid: 0.54, + bestAsk: 0.56, + spread: 0.02, + resolutionSource: 'binance' + }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const updown = await assessPmUpdownReadoutLive( + { event_slug: 'btc-updown-demo' }, + { fetchImpl: mockUpdown } + ); + assert.equal(updown.service_id, 'pm_updown_readout'); + assert.equal(updown.event.slug, 'btc-updown-demo'); + assert.equal(updown.markets.length, 1); + assert.ok(updown.pitfalls.length >= 2); + assert.ok(updown.buyer_summary_zh.includes('涨跌盘')); +} + +// ---- unit: pm-pnl-audit ----------------------------------------------------- + +{ + const { assessPmPnlAuditLive } = await import('../src/pm-pnl-audit.mjs'); + const address = '0x63ce342161250d705dc0b16df89036c8e5f9ba9a'; + const mockPnl = async (url) => { + const u = String(url); + if (u.includes('lb-api.polymarket.com/profit')) { + return new Response(JSON.stringify([{ + amount: 15, + name: 'demo', + proxyWallet: address + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([ + { title: 'A', size: 10, avgPrice: 0.4, cashPnl: 10, currentValue: 14 }, + { title: 'B', size: 5, avgPrice: 0.6, cashPnl: 3, currentValue: 6 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('data-api.polymarket.com/activity') && u.includes('MAKER_REBATE')) { + return new Response(JSON.stringify([{ type: 'MAKER_REBATE' }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + if (u.includes('data-api.polymarket.com/activity') && u.includes('TRADE')) { + return new Response(JSON.stringify([{ type: 'TRADE' }, { type: 'TRADE' }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + throw new Error(`unexpected ${u}`); + }; + const audit = await assessPmPnlAuditLive( + { address, mode: 'quick', positions_limit: 100 }, + { fetchImpl: mockPnl } + ); + assert.equal(audit.service_id, 'pm_pnl_audit'); + assert.equal(audit.mode, 'live_quick'); + assert.equal(audit.leaderboard_profit.amount_usd, 15); + assert.equal(audit.positions_cash_pnl.total_cash_pnl_usd, 13); + assert.equal(audit.divergence_verdict, 'aligned'); + // Quick must NOT claim trust_for_copy — only full cashflow replay may. + assert.equal(audit.action, 'quick_triage_ok'); + assert.equal(audit.value_loop.paid_value_tier, 'A_tier_audit'); + assert.equal(audit.activity_hint.trade_rows_first_page, 2); + + const mockFull = async (url) => { + const u = String(url); + if (u.includes('lb-api.polymarket.com/profit')) { + return new Response(JSON.stringify([{ + amount: 12, + name: 'demo', + proxyWallet: address + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([ + { title: 'A', size: 10, avgPrice: 0.4, cashPnl: 10, currentValue: 14, curPrice: 0.5 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('type=TRADE')) { + return new Response(JSON.stringify([ + { type: 'TRADE', side: 'BUY', usdcSize: 20, timestamp: 1700000000 }, + { type: 'TRADE', side: 'SELL', usdcSize: 25, timestamp: 1700000100 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('type=REDEEM')) { + return new Response(JSON.stringify([ + { type: 'REDEEM', usdcSize: 2, timestamp: 1700000200 } + ]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('type=MERGE') || u.includes('type=SPLIT') || u.includes('type=MAKER_REBATE') + || u.includes('type=REWARD') || u.includes('type=REFERRAL_REWARD') || u.includes('type=CONVERSION')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected full ${u}`); + }; + const full = await assessPmPnlAuditLive( + { address, mode: 'full' }, + { fetchImpl: mockFull, fullBudgetMs: 5000 } + ); + assert.equal(full.mode, 'live_full'); + assert.equal(full.cashflow_replay.status, 'complete'); + // SELL 25 + REDEEM 2 - BUY 20 + unrealized 10*0.5 = 12 + assert.equal(full.cashflow_replay.pnl_trading_usd, 12); + assert.equal(full.divergence_verdict, 'aligned'); + assert.equal(full.action, 'trust_for_copy'); + assert.equal(full.divergence.compared_layer, 'cashflow_replay'); +} + +// ---- unit: scenario discovery category hard gate --------------------------- + +{ + const { + scoreCategoryMatch, + assessTennisMatchCardLive, + assessFootballMatchCardLive + } = await import('../src/pm-scenario-skus.mjs'); + + assert.equal( + scoreCategoryMatch('counter-strike liquid vs atputies - map 1 winner', ['tennis']), + 0, + 'esports + Atputies must not score as tennis' + ); + assert.ok( + scoreCategoryMatch('atp wimbledon djokovic vs alcaraz', ['tennis']) >= 5, + 'real tennis blob should score' + ); + assert.equal( + scoreCategoryMatch('bruno fernandes pfa team of the year', ['football']), + 0, + 'award markets without football tokens should not score as football' + ); + assert.ok( + scoreCategoryMatch('premier league arsenal vs chelsea moneyline', ['football']) >= 5 + ); + + const csOnlySearch = async (url) => { + const u = String(url); + if (u.includes('public-search')) { + return new Response(JSON.stringify({ + events: [{ + title: 'Counter-Strike: Liquid vs Atputies - Map 1 Winner', + slug: 'cs-liquid-atputies-map1', + closed: false, + volume24hr: 90000, + markets: [{ + conditionId: '0xcs1', + slug: 'cs-liquid-atputies-map1-winner', + question: 'Counter-Strike: Liquid vs Atputies - Map 1 Winner', + outcomes: '["Yes","No"]', + outcomePrices: '["0.55","0.45"]', + volume24hr: 90000, + active: true, + closed: false + }] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + // No tennis tag/category defaults in this mock → unavailable + if (u.includes('/events') || u.includes('/markets')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const tennisMisroute = await assessTennisMatchCardLive( + { query: 'atp' }, + { fetchImpl: csOnlySearch } + ); + assert.equal(tennisMisroute.capability_status, 'no_active_markets'); + assert.equal(tennisMisroute.action, 'unavailable'); + assert.notEqual(tennisMisroute.input?.slug, 'cs-liquid-atputies-map1-winner'); + + const footballAwardOnly = async (url) => { + const u = String(url); + if (u.includes('public-search') || u.includes('/events') || u.includes('/markets')) { + if (u.includes('public-search')) { + return new Response(JSON.stringify({ + events: [{ + title: 'Bruno Fernandes PFA Team of the Year', + slug: 'bruno-fernandes-pfa-toty', + closed: false, + volume24hr: 80000, + markets: [{ + conditionId: '0xaw1', + slug: 'bruno-fernandes-pfa-toty-yes', + question: 'Will Bruno Fernandes make the PFA Team of the Year?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.4","0.6"]', + volume24hr: 80000, + active: true, + closed: false + }] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const footballMisroute = await assessFootballMatchCardLive( + { query: 'premier league' }, + { fetchImpl: footballAwardOnly } + ); + assert.equal(footballMisroute.capability_status, 'no_active_markets'); +} + +// ---- unit: scenario SKUs --------------------------------------------------- + +{ + const { + assessPoliticsEventReadoutLive, + assessWeatherEventReadoutLive + } = await import('../src/pm-scenario-skus.mjs'); + + const mockSearch = async (url) => { + const u = String(url); + if (u.includes('public-search')) { + return new Response(JSON.stringify({ + events: [{ + title: '2028 Presidential Election', + slug: 'pres-2028', + closed: false, + volume24hr: 90000, + markets: [{ + conditionId: '0xpol1', + slug: 'pres-2028-a', + question: 'Will Candidate A win the presidential election?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.41","0.59"]', + volume24hr: 50000, + active: true, + closed: false + }] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=')) { + return new Response(JSON.stringify([{ + conditionId: '0xpol1', + slug: 'pres-2028-a', + question: 'Will Candidate A win the presidential election?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.41","0.59"]', + volume24hr: 50000, + active: true, + closed: false, + events: [{ slug: 'pres-2028', title: '2028 Presidential Election' }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') || u.includes('/events/')) { + return new Response(JSON.stringify([{ + slug: 'pres-2028', + title: '2028 Presidential Election', + closed: false, + markets: [{ + conditionId: '0xpol1', + slug: 'pres-2028-a', + question: 'Will Candidate A win the presidential election?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.41","0.59"]', + volume24hr: 50000, + active: true, + closed: false, + groupItemTitle: 'A' + }, { + conditionId: '0xpol2', + slug: 'pres-2028-b', + question: 'Will Candidate B win the presidential election?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.33","0.67"]', + volume24hr: 40000, + active: true, + closed: false, + groupItemTitle: 'B' + }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const politics = await assessPoliticsEventReadoutLive( + { query: 'president' }, + { fetchImpl: mockSearch } + ); + assert.equal(politics.service_id, 'politics_event_readout'); + assert.equal(politics.scenario?.id, 'politics_event_readout'); + assert.ok(typeof politics.buyer_summary_zh === 'string'); + + const weatherFallback = (await import('../src/pm-scenario-skus.mjs')).buildWeatherEventReadoutFallback({}); + assert.equal(weatherFallback.service_id, 'weather_event_readout'); +} + +{ + const { assessWeatherEventReadoutLive } = await import('../src/pm-scenario-skus.mjs'); + const weatherMarket = { + conditionId: '0xweather1', + slug: 'nyc-high-temp-july-26', + question: 'Will NYC high temperature be 90°F or above on July 26?', + outcomes: '["Yes","No"]', + outcomePrices: '["0.44","0.56"]', + volume24hr: 125000, + active: true, + closed: false, + events: [{ id: 'weather-parent', slug: 'nyc-high-temp-july-26-event', title: 'NYC High Temperature July 26' }] + }; + const weatherEvent = [{ + id: 'weather-parent', + slug: 'nyc-high-temp-july-26-event', + title: 'NYC High Temperature July 26', + closed: false, + markets: [weatherMarket] + }]; + + const mockCategoryDefault = async (url) => { + const u = String(url); + const parsed = new URL(u); + if (u.includes('/public-search')) { + return new Response(JSON.stringify({ events: [] }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') && parsed.searchParams.get('tag_slug') === 'weather') { + return new Response(JSON.stringify(weatherEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=nyc-high-temp-july-26')) { + return new Response(JSON.stringify([weatherMarket]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') && parsed.searchParams.get('slug') === 'nyc-high-temp-july-26-event') { + return new Response(JSON.stringify(weatherEvent), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const weather = await assessWeatherEventReadoutLive( + { query: 'impossible caller query' }, + { fetchImpl: mockCategoryDefault } + ); + assert.equal(weather.mode, 'live'); + assert.equal(weather.category, 'weather'); + assert.equal(weather.scenario.resolved_via, 'category_default'); + assert.equal(weather.input.slug, 'nyc-high-temp-july-26'); +} + +{ + const { assessWeatherEventReadoutLive } = await import('../src/pm-scenario-skus.mjs'); + const mockNoMarkets = async (url) => { + const u = String(url); + if (u.includes('/public-search')) { + return new Response(JSON.stringify({ events: [] }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') || u.includes('/markets?')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const unavailable = await assessWeatherEventReadoutLive( + { query: 'no such weather market' }, + { fetchImpl: mockNoMarkets } + ); + assert.equal(unavailable.service_id, 'weather_event_readout'); + assert.equal(unavailable.mode, 'live'); + assert.equal(unavailable.capability_status, 'no_active_markets'); + assert.equal(unavailable.action, 'unavailable'); + assert.ok(unavailable.buyer_summary_zh.includes('没有找到')); + assert.ok(unavailable.buyer_summary_en.includes('No active')); + assert.ok(unavailable.paid_checks.fail_count >= 1); +} + +{ + const { assessNbaMatchCardLive } = await import('../src/pm-scenario-skus.mjs'); + const lakersMarket = { + conditionId: '0xlakers', + slug: 'nba-lakers-celtics-lakers', + question: 'NBA: Will the Los Angeles Lakers beat the Boston Celtics?', + groupItemTitle: 'Los Angeles Lakers', + sportsMarketType: 'moneyline', + outcomes: '["Yes","No"]', + outcomePrices: '["0.57","0.43"]', + volume24hr: 25000, + active: true, + closed: false, + events: [{ id: 'nba-lal-bos', slug: 'nba-lakers-celtics', title: 'NBA: Los Angeles Lakers vs Boston Celtics' }] + }; + const clippersMarket = { + conditionId: '0xclips', + slug: 'nba-lebron-clippers-points', + question: 'NBA: Will LeBron James score 25+ points vs the Los Angeles Clippers?', + groupItemTitle: 'LeBron James', + sportsMarketType: 'player_points', + outcomes: '["Yes","No"]', + outcomePrices: '["0.51","0.49"]', + volume24hr: 900000, + active: true, + closed: false, + events: [{ id: 'nba-clips', slug: 'nba-lebron-clippers', title: 'NBA: LeBron James vs Los Angeles Clippers' }] + }; + + const mockLakersSearch = async (url) => { + const u = String(url); + const parsed = new URL(u); + if (u.includes('/public-search')) { + return new Response(JSON.stringify({ + events: [{ + title: 'NBA: LeBron James vs Los Angeles Clippers', + slug: 'nba-lebron-clippers', + closed: false, + markets: [clippersMarket] + }, { + title: 'NBA: Los Angeles Lakers vs Boston Celtics', + slug: 'nba-lakers-celtics', + closed: false, + markets: [lakersMarket] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/markets?slug=nba-lakers-celtics-lakers')) { + return new Response(JSON.stringify([lakersMarket]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') && parsed.searchParams.get('slug') === 'nba-lakers-celtics') { + return new Response(JSON.stringify([{ + id: 'nba-lal-bos', + slug: 'nba-lakers-celtics', + title: 'NBA: Los Angeles Lakers vs Boston Celtics', + markets: [lakersMarket, { + ...lakersMarket, + conditionId: '0xceltics', + slug: 'nba-lakers-celtics-celtics', + question: 'NBA: Will the Boston Celtics beat the Los Angeles Lakers?', + groupItemTitle: 'Boston Celtics', + outcomePrices: '["0.43","0.57"]', + volume24hr: 22000 + }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') || u.includes('/markets?')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + + const lakers = await assessNbaMatchCardLive( + { query: 'Lakers', nba: { verified: true, market_fixture_match: 'yes' } }, + { fetchImpl: mockLakersSearch } + ); + assert.equal(lakers.input.slug, 'nba-lakers-celtics-lakers'); + assert.equal(lakers.scenario.resolved_via, 'public_search'); + + const mockClippersOnly = async (url) => { + const u = String(url); + if (u.includes('/public-search')) { + return new Response(JSON.stringify({ + events: [{ + title: 'NBA: LeBron James vs Los Angeles Clippers', + slug: 'nba-lebron-clippers', + closed: false, + markets: [clippersMarket] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('/events?') || u.includes('/markets?')) { + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const unavailable = await assessNbaMatchCardLive( + { query: 'Lakers' }, + { fetchImpl: mockClippersOnly } + ); + assert.equal(unavailable.service_id, 'nba_match_card'); + assert.equal(unavailable.action, 'unavailable'); + assert.equal(unavailable.capability_status, 'no_active_markets'); +} + +// ---- unit: finance-cockpit ------------------------------------------------- + +{ + const { assessFinanceCockpitLive } = await import('../src/finance-cockpit.mjs'); + const mockFc = async (url) => { + const u = String(url); + if (u.includes('okx.com') && u.includes('ticker')) { + return new Response(JSON.stringify({ + code: '0', + data: [{ last: '65000', open24h: '64000' }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('okx.com') && u.includes('funding-rate')) { + return new Response(JSON.stringify({ + code: '0', + data: [{ fundingRate: '0.0001', premium: '0.0002' }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('okx.com') && u.includes('open-interest')) { + return new Response(JSON.stringify({ + code: '0', + data: [{ oiUsd: '1000000' }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + if (u.includes('gamma-api.polymarket.com')) { + return new Response(JSON.stringify({ + events: [{ + closed: false, + markets: [{ + conditionId: '0x1', + question: 'Will Bitcoin reach $100k?', + slug: 'btc-100k', + outcomes: '["Yes","No"]', + outcomePrices: '["0.4","0.6"]', + oneDayPriceChange: -0.05, + volume24hr: 20000, + active: true, + closed: false + }] + }] + }), { status: 200, headers: { 'content-type': 'application/json' } }); + } + throw new Error(`unexpected ${u}`); + }; + const card = await assessFinanceCockpitLive( + { focus: 'bitcoin', limit: 2 }, + { fetchImpl: mockFc } + ); + assert.equal(card.service_id, 'finance_cockpit'); + assert.ok(card.regime?.regime); + assert.ok(typeof card.buyer_summary_zh === 'string'); + assert.ok(['risk_on_clean', 'risk_off_clean', 'divergence_review', 'hold_observe'].includes(card.action)); +} + +// ---- unit: politics category plugin ---------------------------------------- + +{ + const { enrichPoliticsCategory } = await import('../src/pm-category-politics.mjs'); + const politics = enrichPoliticsCategory({ + market: { title: 'Presidential Election Winner', slug: 'pres-2028', yes: 0.41 }, + eventBundle: { title: '2028 Presidential Election', slug: 'pres-2028' }, + eventMatrix: [ + { title: 'Candidate A', group_item_title: 'A', slug: 'a', yes: 0.41, is_primary: true }, + { title: 'Candidate B', group_item_title: 'B', slug: 'b', yes: 0.33 }, + { title: 'Candidate C', group_item_title: 'C', slug: 'c', yes: 0.12 } + ] + }); + assert.equal(politics.category, 'politics'); + assert.equal(politics.ladder_status, 'multi_candidate'); + assert.equal(politics.leaderboard[0].label, 'A'); +} + +// ---- unit: macro Fed category plugin --------------------------------------- + +{ + const { enrichMacroFedCategory } = await import('../src/pm-category-macro-fed.mjs'); + const fed = enrichMacroFedCategory({ + market: { title: 'Fed Decision in September?', slug: 'fed-september' }, + eventBundle: { title: 'Fed Decision in September?', slug: 'fed-september' }, + eventMatrix: [ + { title: 'No change in Fed interest rates', group_item_title: 'No change', slug: 'fed-hold', yes: 0.58, is_primary: true }, + { title: 'Fed decrease interest rates by 25 bps', group_item_title: '25 bps decrease', slug: 'fed-cut-25', yes: 0.31 }, + { title: 'Fed decrease interest rates by 50 bps', group_item_title: '50 bps decrease', slug: 'fed-cut-50', yes: 0.08 }, + { title: 'Fed increase interest rates by 25 bps', group_item_title: '25 bps increase', slug: 'fed-hike-25', yes: 0.03 } + ] + }); + assert.equal(fed.category, 'macro_fed'); + assert.equal(fed.category_depth, 'enriched'); + assert.equal(fed.ladder_status, 'rate_ladder'); + assert.equal(fed.leaderboard[0].bucket, 'hold'); + assert.equal(fed.implied_expected_move.status, 'estimated'); + assert.ok(fed.central_thesis.includes('implied expected move')); +} + +// ---- unit: football outright category plugin ------------------------------- + +{ + const { enrichFootballCategory } = await import('../src/pm-category-football.mjs'); + const outright = enrichFootballCategory({ + market: { title: '2026 Premier League Winner', slug: '2026-premier-league-winner' }, + eventBundle: { title: '2026 Premier League Winner', slug: '2026-premier-league-winner' }, + eventMatrix: [ + { title: 'Will Arsenal win the Premier League?', group_item_title: 'Arsenal', slug: 'pl-arsenal', yes: 0.34, volume_24h_usd: 80000, is_primary: true }, + { title: 'Will Manchester City win the Premier League?', group_item_title: 'Manchester City', slug: 'pl-man-city', yes: 0.28, volume_24h_usd: 70000 }, + { title: 'Will Liverpool win the Premier League?', group_item_title: 'Liverpool', slug: 'pl-liverpool', yes: 0.16, volume_24h_usd: 60000 } + ] + }); + assert.equal(outright.category, 'football'); + assert.equal(outright.category_depth, 'enriched'); + assert.equal(outright.market_type, 'outright_season'); + assert.equal(outright.market_surface.outright_leaderboard[0].label, 'Arsenal'); + assert.ok(outright.central_thesis.includes('Outright leaderboard leads')); + assert.ok(!outright.central_thesis.includes('Insufficient structure')); +} + +// ---- unit: publish-readiness ------------------------------------------------ + +{ + const { assessPublishReadiness } = await import('../src/publish-readiness.mjs'); + const blocked = assessPublishReadiness({ + text: "In today's digital landscape, it is crucial to delve into synergy. As an AI, I am excited to underscore this game-changer." + }); + assert.equal(blocked.service_id, 'publish_readiness'); + assert.equal(blocked.action, 'block'); + assert.ok(blocked.buyer_summary_zh.includes('先别发') || blocked.blockers.length >= 1); + + const ready = assessPublishReadiness({ + text: 'Marketplace scan on 2026-07-07 found 358 unique ASPs and 2982 cumulative soldCount.', + claims: ['Marketplace has 358 unique ASPs and 2982 cumulative soldCount.'], + sources: [{ text: 'Marketplace scan on 2026-07-07 found 358 unique ASPs and 2982 cumulative soldCount.' }] + }); + assert.equal(ready.action, 'ready'); + assert.ok(typeof ready.buyer_summary_zh === 'string'); +} + +// ---- worker integration (degraded fallback, no external network) ------------ + +const savedFetch = globalThis.fetch; +globalThis.fetch = async (url) => { + if (String(url).startsWith('https://web3.okx.com/')) { + throw new Error('x402 should not run in this test'); + } + if (String(url).startsWith('https://gamma-api.polymarket.com/')) { + const u = String(url); + return new Response(JSON.stringify(u.includes('/public-search') ? { events: [] } : []), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + if (String(url).startsWith('https://lb-api.polymarket.com/')) { + return new Response(JSON.stringify([{ amount: 20, name: 'worker-demo', proxyWallet: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a' }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + if (String(url).startsWith('https://data-api.polymarket.com/positions')) { + return new Response(JSON.stringify([{ title: 'Worker A', size: 10, avgPrice: 0.4, cashPnl: 18, currentValue: 22 }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + if (String(url).startsWith('https://data-api.polymarket.com/activity')) { + return new Response(JSON.stringify([{ type: 'TRADE' }]), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + throw new Error('external network disabled'); +}; + +try { + const tokenRes = await worker.fetch(new Request(`${BASE}/token-dd-verdict`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ asset: 'ETH' }) + })); + assert.equal(tokenRes.status, 200); + const tokenBody = await tokenRes.json(); + assert.equal(tokenBody.service_id, 'token_dd_verdict'); + assert.ok(['degraded', 'public_safe_demo'].includes(tokenBody.mode) || tokenBody.verdict_bucket); + + const preRes = await worker.fetch(new Request(`${BASE}/pm-trade-preflight`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ slug: 'demo-slug', side: 'yes' }) + })); + assert.equal(preRes.status, 200); + const preBody = await preRes.json(); + assert.equal(preBody.service_id, 'pm_trade_preflight'); + assert.ok(['eligible', 'watch', 'skip'].includes(preBody.action)); + + const auditRes = await worker.fetch(new Request(`${BASE}/pm-pnl-audit`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a', mode: 'quick' }) + })); + assert.equal(auditRes.status, 200); + const auditBody = await auditRes.json(); + assert.equal(auditBody.service_id, 'pm_pnl_audit'); + assert.equal(auditBody.divergence_verdict, 'aligned'); + + const catalog = await worker.fetch(new Request(`${BASE}/api/okx-ai-services`)).then((r) => r.json()); + assert.ok(catalog.services.some((s) => s.service_id === 'token_dd_verdict')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_event_readout')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_pnl_audit')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_market_scan')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_market_health')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_wallet_report')); + assert.ok(catalog.services.some((s) => s.service_id === 'pm_updown_readout')); + + const scanRes = await worker.fetch(new Request(`${BASE}/pm-market-scan`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ limit: 3, min_volume: 1000 }) + })); + assert.equal(scanRes.status, 200); + const scanBody = await scanRes.json(); + assert.equal(scanBody.service_id, 'pm_market_scan'); + + const healthRes = await worker.fetch(new Request(`${BASE}/pm-market-health`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ slug: 'demo-health' }) + })); + assert.equal(healthRes.status, 200); + const healthBody = await healthRes.json(); + assert.equal(healthBody.service_id, 'pm_market_health'); + + const walletRes = await worker.fetch(new Request(`${BASE}/pm-wallet-report`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a', pnl_mode: 'quick' }) + })); + assert.equal(walletRes.status, 200); + const walletBody = await walletRes.json(); + assert.equal(walletBody.service_id, 'pm_wallet_report'); + + const updownRes = await worker.fetch(new Request(`${BASE}/pm-updown-readout`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ event_slug: 'btc-updown-demo' }) + })); + assert.equal(updownRes.status, 200); + const updownBody = await updownRes.json(); + assert.equal(updownBody.service_id, 'pm_updown_readout'); + + const readRes = await worker.fetch(new Request(`${BASE}/pm-event-readout`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ slug: 'will-argentina-win-the-2026-fifa-world-cup-245' }) + })); + assert.equal(readRes.status, 200); + const readBody = await readRes.json(); + assert.equal(readBody.service_id, 'pm_event_readout'); + + const weatherUnavailableRes = await worker.fetch(new Request(`${BASE}/weather-event-readout`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query: 'no active weather markets in test' }) + })); + assert.equal(weatherUnavailableRes.status, 200); + const weatherUnavailable = await weatherUnavailableRes.json(); + assert.equal(weatherUnavailable.service_id, 'weather_event_readout'); + assert.equal(weatherUnavailable.mode, 'live'); + assert.equal(weatherUnavailable.capability_status, 'no_active_markets'); + assert.equal(weatherUnavailable.action, 'unavailable'); + + const verifyRes = await worker.fetch(new Request(`${BASE}/content-verify-claims`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + claims: ['Platform has about 360 ASPs.'], + sources: [{ text: 'Marketplace scan: 358 unique ASPs on 2026-07-07.' }] + }) + })); + assert.equal(verifyRes.status, 200); + const verifyBody = await verifyRes.json(); + assert.equal(verifyBody.service_id, 'content_verify_claims'); + + const sample = await worker.fetch(new Request(`${BASE}/token-dd-verdict`, { method: 'GET' })).then((r) => r.json()); + assert.equal(sample.mode, 'public_sample'); + assert.ok(sample.sample_response?.verdict_bucket); +} finally { + globalThis.fetch = savedFetch; +} + +console.log('PASS wave-b-services-test'); + +// ---- unit: shared asset stance classifier ----------------------------------- +// 2026-07-30: extracted from two drifted copies. The old rule ended with +// `|| /\$\s?\d/ → +1`, so 66 of 362 live Gamma markets fell through and were all +// called bullish while only ~6 deserved it. Range buckets must be skipped, not signed. +{ + const { classifyAssetStance } = await import('../src/market-stance.mjs'); + const yes = (title) => classifyAssetStance({ primary_outcome: 'Yes', title }); + + // explicit outcomes still short-circuit + assert.equal(classifyAssetStance({ primary_outcome: 'Up', title: 'anything' }), 1); + assert.equal(classifyAssetStance({ primary_outcome: 'Down', title: 'anything' }), -1); + assert.equal(classifyAssetStance({ primary_outcome: 'Chiefs', title: 'anything' }), 0); + + // the three shapes that were all scored +1 before + assert.equal(yes('Will the price of Ethereum be less than $1,400 on July 30?'), -1); + assert.equal(yes('Will the price of Ethereum be between $1,400 and $1,500 on July 30?'), 0); + assert.equal(yes('Will the price of Ethereum be greater than $2,300 on July 30?'), 1); + + // range bucket variants must all skip + assert.equal(yes('Will Solana settle $30 to $40 on August 1?'), 0); + assert.equal(yes('Will BTC close $60,000–$62,000 today?'), 0); + + // bearish phrasings the old keyword list missed + assert.equal(yes('Will Solana be worth less than $100 in 2026?'), -1); + assert.equal(yes('Will BTC trade sub-$50,000 this month?'), -1); + assert.equal(yes('Will ETH be lower than $2,000 on Friday?'), -1); + + // keyword polarity preserved + assert.equal(yes('Will Bitcoin dip to $60,000 in July?'), -1); + assert.equal(yes('Will Bitcoin reach an all-time high in 2026?'), 1); + + // fail closed: a bare dollar figure is no longer read as bullish + assert.equal(yes('Will Bitcoin be $60,000 on July 30?'), 0); + assert.equal(yes('Some unparseable question about ETH'), 0); +} + +// ---- unit: scoped smart-money must never deliver off-scope silently ----------- +// 2026-07-30: after the 2026-07-19 World Cup final, /world-cup-smart-money-radar +// answered with a Fed-rates market while the headline still read 已扫描真实 Polymarket +// 市场 — the top_volume_fallback branch never marked the scope change, so the summary +// took the on-scope wording. The caveat existed but only inside `caveats`. +{ + const { assessSportsSmartMoneyLive } = await import('../src/worldcup-smart-money-live.mjs'); + // Every scoped discovery attempt misses; only the site-wide top-volume list answers. + const siteWideOnly = async (url) => { + const u = String(url); + if (u.includes('/markets?closed=false')) { + return new Response(JSON.stringify([{ + conditionId: '0xfed', + question: 'Will there be no change in Fed interest rates?', + slug: 'fed-no-change', + outcomes: '["Yes","No"]', + outcomePrices: '["0.8","0.2"]', + volume24hr: 250000, + enableOrderBook: true + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + // tag_slug / public-search / trades / leaderboard lookups all come back empty + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + }; + + const out = await assessSportsSmartMoneyLive( + { league: 'world_cup', tag_slug: 'world-cup' }, + { fetchImpl: siteWideOnly, legacyWorldCup: true, serviceId: 'world_cup_smart_money_radar' } + ); + + assert.equal(out.capability_status, 'off_scope_fallback'); + assert.equal(out.requested_scope, 'world_cup'); + assert.equal(out.effective_scope, 'site_wide_top_volume'); + // the headline itself must carry the scope change, not just the caveats array + assert.ok(out.buyer_summary_zh.includes('world_cup')); + assert.ok(/无活跃市场/.test(out.buyer_summary_zh)); + assert.ok(/expanded/i.test(out.buyer_summary_en)); + assert.ok(out.caveats.some((c) => /No active/i.test(c))); +} + +// on-scope requests must stay clean: no scope fields, no warning wording +{ + const { assessSportsSmartMoneyLive } = await import('../src/worldcup-smart-money-live.mjs'); + const onScope = async (url) => { + const u = String(url); + if (u.includes('tag_slug=world-cup')) { + return new Response(JSON.stringify([{ + closed: false, + markets: [{ + conditionId: '0xwc', + question: 'Will Brazil win the World Cup?', + slug: 'brazil-wc', + outcomes: '["Yes","No"]', + outcomePrices: '["0.3","0.7"]', + volume24hr: 90000, + active: true, + closed: false, + enableOrderBook: true + }] + }]), { status: 200, headers: { 'content-type': 'application/json' } }); + } + return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }); + }; + const out = await assessSportsSmartMoneyLive( + { league: 'world_cup', tag_slug: 'world-cup' }, + { fetchImpl: onScope, legacyWorldCup: true, serviceId: 'world_cup_smart_money_radar' } + ); + assert.equal(out.capability_status, 'on_scope'); + assert.equal(out.requested_scope, undefined); + assert.ok(!/无活跃市场/.test(out.buyer_summary_zh)); +} + +// ---- unit: agent-budget-preflight (spend gate) ------------------------------- +// 2026-07-30: this service is the gate an agent calls BEFORE paying, and it had no +// test coverage at all. Adding it surfaced a live defect: remaining = cap - spent - +// held never checked the sign of the caller-supplied ledger, so budget_cap_usdt=1 +// with spent_usdt=-100 reported remaining=101 and returned action=buy for a 50 USDT +// offer — approving 50x its own cap. +{ + const { assessAgentBudgetPreflight, buildAgentBudgetPreflightFallback } = + await import('../src/agent-budget-preflight.mjs'); + const offer = { provider: 'okx-asp', price_usdt: 1 }; + + // --- the defect: a negative ledger must never widen the cap + assert.throws( + () => assessAgentBudgetPreflight({ + budget_cap_usdt: 1, spent_usdt: -100, max_per_call_usdt: 60, + offer: { provider: 'okx-asp', price_usdt: 50 } + }), + /spent_usdt must be zero or positive/ + ); + assert.throws( + () => assessAgentBudgetPreflight({ budget_cap_usdt: 1, held_usdt: -99, offer }), + /held_usdt must be zero or positive/ + ); + assert.throws( + () => assessAgentBudgetPreflight({ budget_cap_usdt: 10, max_per_call_usdt: 0, offer }), + /max_per_call_usdt must be a positive number/ + ); + + // --- happy path + const buy = assessAgentBudgetPreflight({ budget_cap_usdt: 10, spent_usdt: 2, offer }); + assert.equal(buy.action, 'buy'); + assert.equal(buy.reason, 'within_policy_and_budget'); + assert.equal(buy.amount_usdt, 1); + assert.equal(buy.remaining_usdt, 8); + + // held funds reduce what is spendable + const withHold = assessAgentBudgetPreflight({ + budget_cap_usdt: 10, spent_usdt: 2, held_usdt: 7, offer + }); + assert.equal(withHold.remaining_usdt, 1); + assert.equal(withHold.action, 'buy'); + + // price exactly equal to remaining is still affordable (epsilon, not strict >) + const exact = assessAgentBudgetPreflight({ budget_cap_usdt: 10, spent_usdt: 9, offer }); + assert.equal(exact.action, 'buy'); + + // --- rejections, and their precedence + const overTotal = assessAgentBudgetPreflight({ + budget_cap_usdt: 10, spent_usdt: 9.5, max_per_call_usdt: 5, offer + }); + assert.equal(overTotal.action, 'reject_budget'); + assert.equal(overTotal.reason, 'total_cap_exceeded'); + + const overPerCall = assessAgentBudgetPreflight({ + budget_cap_usdt: 100, max_per_call_usdt: 0.5, offer + }); + assert.equal(overPerCall.action, 'reject_budget'); + assert.equal(overPerCall.reason, 'per_call_cap_exceeded'); + + // allowlist is checked before any budget maths — an affordable call from an + // unlisted provider must still be refused on policy, not waved through + const notAllowed = assessAgentBudgetPreflight({ + budget_cap_usdt: 100, allowlisted_providers: ['trusted-asp'], offer + }); + assert.equal(notAllowed.action, 'reject_policy'); + assert.equal(notAllowed.reason, 'provider_not_allowed'); + assert.equal(notAllowed.amount_usdt, 0); + + // allowlist matching is case-insensitive on the offer side + const allowed = assessAgentBudgetPreflight({ + budget_cap_usdt: 100, allowlisted_providers: ['okx-asp'], + offer: { provider: 'OKX-ASP', price_usdt: 1 } + }); + assert.equal(allowed.action, 'buy'); + + // --- evidence gate: do not pay for what you already know + const skip = assessAgentBudgetPreflight({ + budget_cap_usdt: 10, evidence_sufficient: true, offer + }); + assert.equal(skip.action, 'skip_sufficient'); + assert.equal(skip.reason, 'evidence_already_sufficient'); + assert.equal(skip.amount_usdt, 0); + + // --- input validation + assert.throws(() => assessAgentBudgetPreflight({ offer }), /budget_cap_usdt is required/); + assert.throws(() => assessAgentBudgetPreflight({ budget_cap_usdt: 0, offer }), /budget_cap_usdt is required/); + assert.throws(() => assessAgentBudgetPreflight({ budget_cap_usdt: 10 }), /offer is required/); + // a malformed price must not fall through to a buy + assert.throws( + () => assessAgentBudgetPreflight({ budget_cap_usdt: 10, offer: { provider: 'x', price_usdt: 'abc' } }), + /offer is required/ + ); + assert.throws( + () => assessAgentBudgetPreflight({ budget_cap_usdt: 10, offer: { provider: '', price_usdt: 1 } }), + /offer is required/ + ); + + // --- the gate never settles, whatever it decides + for (const r of [buy, skip, notAllowed, overTotal]) { + assert.equal(r.service_id, 'agent_budget_preflight'); + assert.ok(r.caveats.some((c) => /Does not hold funds, sign, settle/.test(c))); + } + assert.equal(buildAgentBudgetPreflightFallback().action, 'reject_policy'); +} + +// ---- unit: nba shape thresholds are disclosed -------------------------------- +// 2026-07-31: last of the three category plugins to get the treatment football and +// tennis already had. These cutoffs decide what the buyer is told about the market +// ("clear favorite", "ML vs spread mismatch"); inline, they were indistinguishable +// from a typo. Values unchanged — disclosure only. +{ + const { enrichNbaCategory, NBA_SHAPE_THRESHOLDS } = + await import('../src/pm-category-nba.mjs'); + + assert.ok(Object.isFrozen(NBA_SHAPE_THRESHOLDS)); + assert.equal(NBA_SHAPE_THRESHOLDS.clear_favorite, 0.62); + assert.equal(NBA_SHAPE_THRESHOLDS.lean_favorite, 0.55); + assert.equal(NBA_SHAPE_THRESHOLDS.heavy_favorite, 0.7); + assert.equal(NBA_SHAPE_THRESHOLDS.tight_spread_abs, 2.5); + assert.equal(NBA_SHAPE_THRESHOLDS.wide_spread_abs, 8); + + const empty = enrichNbaCategory({ + market: {}, eventBundle: null, eventMatrix: [], fixture: null + }); + assert.deepEqual(empty.coherence.shape_thresholds, { ...NBA_SHAPE_THRESHOLDS }); + // the response carries a copy — a caller cannot reach through it and retune the service + empty.coherence.shape_thresholds.clear_favorite = 0.99; + assert.equal(NBA_SHAPE_THRESHOLDS.clear_favorite, 0.62); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-facilitator-smoke.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-facilitator-smoke.mjs new file mode 100644 index 00000000..d396c819 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-facilitator-smoke.mjs @@ -0,0 +1,61 @@ +// Standalone smoke: proves our OK-ACCESS HMAC signing is accepted by the OKX +// hosted x402 facilitator, using the read-only `supported` endpoint. +// +// Not part of `npm test` (needs network + real credentials). Run manually: +// node ./test/x402-facilitator-smoke.mjs +// +// Reads OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE from process.env, +// falling back to .dev.vars in the repo root. Never prints credential values. + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { facilitatorRequest } from '../worker/x402.mjs'; + +function loadDevVars() { + const path = join(dirname(fileURLToPath(import.meta.url)), '..', '.dev.vars'); + const env = {}; + try { + for (const line of readFileSync(path, 'utf8').split('\n')) { + const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); + if (match) env[match[1]] = match[2].replace(/^["']|["']$/g, ''); + } + } catch { + // no .dev.vars — rely on process.env only + } + return env; +} + +const devVars = loadDevVars(); +const env = { + OKX_API_KEY: process.env.OKX_API_KEY || devVars.OKX_API_KEY, + OKX_SECRET_KEY: process.env.OKX_SECRET_KEY || devVars.OKX_SECRET_KEY, + OKX_PASSPHRASE: process.env.OKX_PASSPHRASE || devVars.OKX_PASSPHRASE +}; + +if (!env.OKX_API_KEY || !env.OKX_SECRET_KEY || !env.OKX_PASSPHRASE) { + console.error('SKIP: OKX credentials not found in env or .dev.vars'); + process.exit(2); +} + +try { + // facilitatorRequest throws on HTTP errors AND on OKX envelope code !== "0", + // so reaching the lines below means transport auth and the business envelope + // both succeeded (code === "0"). + const supported = await facilitatorRequest(env, 'GET', '/api/v6/pay/x402/supported'); + const kinds = supported?.kinds ?? []; + const hasExactXLayer = kinds.some((k) => k.scheme === 'exact' && k.network === 'eip155:196'); + if (!hasExactXLayer) { + console.error('FAIL: facilitator responded (code=0) but exact/eip155:196 is not in supported kinds.'); + console.error(JSON.stringify(supported, null, 2)); + process.exit(1); + } + console.log('PASS facilitator auth accepted (HTTP 200 + envelope code=0) — GET /api/v6/pay/x402/supported data:'); + console.log(JSON.stringify(supported, null, 2)); + console.log('exact/eip155:196 supported: yes'); + process.exit(0); +} catch (error) { + console.error(`FAIL facilitator request rejected: ${error.message}`); + if (error.detail) console.error(`detail: ${error.detail}`); + process.exit(1); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-worker-test.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-worker-test.mjs new file mode 100644 index 00000000..4349b2dd --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/x402-worker-test.mjs @@ -0,0 +1,317 @@ +// Unit tests for the OKX x402 paywall using a mocked facilitator (no network, +// no credentials). Part of `npm test`. +// +// Covers: +// 1. X402_ENABLED=false → byte-identical free behavior (status + headers) +// 2. enabled + no payment → 402 by default (free trial opt-in only) +// 3. enabled + X402_FREE_TRIAL=true → one free POST, then 402 +// 4. enabled + official `PAYMENT` header accepted +// 5. verify isValid=false → 402, settle never called +// 6. settle pending → settle/status poll → success → 200 delivered +// 7. settle timeout → still pending → 402 pending_tx; replay same payment +// after confirmation → 200 delivered with NO second settle call +// 8. resource.url mismatch → 402, no facilitator calls +// 9. OKX envelope code!=="0" → 502 facilitator_error with generic code only + +import assert from 'node:assert/strict'; +import worker from '../worker/index.mjs'; + +const BASE = 'https://gate.example.com'; +const RESOURCE = `${BASE}/world-cup-smart-money-radar`; + +const ENV = { + X402_ENABLED: 'true', + OKX_API_KEY: 'test-key', + OKX_SECRET_KEY: 'test-secret', + OKX_PASSPHRASE: 'test-pass', + X402_SETTLE_POLL_BUDGET_MS: '120', + X402_SETTLE_POLL_INTERVAL_MS: '10' +}; + +// ---- facilitator mock ------------------------------------------------------ + +let script; // { verify, settle, status: [...] } — status entries shift()ed per call +let calls; + +function okxEnvelope(data, code = '0') { + return new Response(JSON.stringify({ code, msg: code === '0' ? '' : 'mock error', data }), { + status: 200, + headers: { 'content-type': 'application/json' } + }); +} + +globalThis.fetch = async (input) => { + const url = typeof input === 'string' ? input : input.url; + if (url.startsWith('https://web3.okx.com/api/v6/pay/x402/')) { + const path = new URL(url).pathname + new URL(url).search; + if (path.startsWith('/api/v6/pay/x402/verify')) { + calls.verify += 1; + return script.verify(); + } + if (path.startsWith('/api/v6/pay/x402/settle/status')) { + calls.status += 1; + const next = script.status.length > 1 ? script.status.shift() : script.status[0]; + return next(); + } + if (path.startsWith('/api/v6/pay/x402/settle')) { + calls.settle += 1; + return script.settle(); + } + } + // Any other network call (live Polymarket fetch) fails → worker serves its + // static fallback dataset, which is fine for these tests. + throw new Error('external network disabled in tests'); +}; + +function resetMock(overrides = {}) { + calls = { verify: 0, settle: 0, status: 0 }; + script = { + verify: () => okxEnvelope({ isValid: true, payer: '0x1111111111111111111111111111111111111111' }), + settle: () => okxEnvelope({ + success: true, status: 'success', transaction: '0x' + 'ab'.repeat(32), network: 'eip155:196' + }), + status: [() => okxEnvelope({ success: true, status: 'success' })], + ...overrides + }; +} + +// ---- helpers ---------------------------------------------------------------- + +function paymentHeader({ nonce = '0x' + '11'.repeat(32), resourceUrl = undefined, amount = '100000' } = {}) { + const now = Math.floor(Date.now() / 1000); + const accepted = { + scheme: 'exact', + network: 'eip155:196', + amount, + asset: '0x779ded0c9e1022225f8e0630b35a9b54be713736', + payTo: '0x1e1a2f7ac1bc6df29a1878c3f26b17dccdc16e15', + maxTimeoutSeconds: 300, + extra: { name: 'USD₮0', version: '1' } + }; + const payload = { + x402Version: 2, + ...(resourceUrl ? { resource: { url: resourceUrl } } : {}), + accepted, + payload: { + authorization: { + from: '0x1111111111111111111111111111111111111111', + to: accepted.payTo, + value: accepted.amount, + validAfter: String(now - 5), + validBefore: String(now + 300), + nonce + }, + signature: '0x' + '22'.repeat(65) + } + }; + return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64'); +} + +function post(headers = {}, env = ENV, body = { limit: 1 }) { + return worker.fetch(new Request(RESOURCE, { + method: 'POST', + headers: { 'content-type': 'application/json', ...headers }, + body: JSON.stringify(body) + }), env); +} + +function decodeB64Json(value) { + return JSON.parse(Buffer.from(value, 'base64').toString('utf8')); +} + +let passed = 0; +function ok(name) { + passed += 1; + console.log(`PASS ${name}`); +} + +// ---- 1. disabled → byte-identical free behavior ----------------------------- + +{ + resetMock(); + // null (not undefined) — undefined would trigger post()'s ENV default param + for (const env of [{}, { X402_ENABLED: 'false' }, null]) { + const res = await post({}, env); + assert.equal(res.status, 200); + assert.equal(res.headers.get('access-control-allow-headers'), 'content-type'); + assert.equal(res.headers.get('access-control-expose-headers'), null); + assert.equal(res.headers.get('payment-required'), null); + assert.equal(res.headers.get('payment-response'), null); + assert.equal(res.headers.get('content-type'), 'application/json; charset=utf-8'); + assert.equal(res.headers.get('access-control-allow-origin'), '*'); + assert.equal(res.headers.get('access-control-allow-methods'), 'GET, POST, OPTIONS'); + const body = await res.json(); + assert.equal(body.schema_version, '0.1'); + assert.equal(calls.verify + calls.settle + calls.status, 0); + } + // OPTIONS stays identical too when disabled + const preflight = await worker.fetch(new Request(RESOURCE, { method: 'OPTIONS' }), {}); + assert.equal(preflight.status, 204); + assert.equal(preflight.headers.get('access-control-allow-headers'), 'content-type'); + assert.equal(preflight.headers.get('access-control-expose-headers'), null); + ok('X402_ENABLED=false keeps free behavior and original headers byte-identical'); +} + +// ---- 2. enabled, no payment → 402 by default (listing-safe) ------------------- + +{ + resetMock(); + const res = await post({ 'cf-connecting-ip': '203.0.113.49' }); + assert.equal(res.status, 402); + const challengeHeader = res.headers.get('payment-required'); + assert.ok(challengeHeader, 'PAYMENT-REQUIRED header present'); + const decoded = decodeB64Json(challengeHeader); + const body = await res.json(); + assert.deepEqual(decoded, body); + assert.equal(body.x402Version, 2); + assert.equal(body.error, 'Payment required'); + assert.equal(body.resource.url, RESOURCE); + assert.equal(body.accepts[0].amount, '100000'); + assert.equal(calls.verify + calls.settle + calls.status, 0); + ok('no payment → unpaid POST returns 402 when free trial disabled (default)'); +} + +// ---- 2b. X402_FREE_TRIAL=true → free trial once, then 402 -------------------- + +{ + resetMock(); + const trialEnv = { ...ENV, X402_FREE_TRIAL: 'true' }; + const trialIp = { 'cf-connecting-ip': '203.0.113.50' }; + const first = await post(trialIp, trialEnv); + assert.equal(first.status, 200); + const firstBody = await first.json(); + assert.equal(firstBody.billing?.mode, 'free_trial'); + assert.equal(firstBody.billing?.list_price_usdt, '0.1'); + assert.equal(calls.verify + calls.settle + calls.status, 0); + + const second = await post(trialIp, trialEnv); + assert.equal(second.status, 402); + const challengeHeader = second.headers.get('payment-required'); + assert.ok(challengeHeader, 'PAYMENT-REQUIRED header present'); + const decoded = decodeB64Json(challengeHeader); + const body = await second.json(); + assert.deepEqual(decoded, body); + assert.equal(body.x402Version, 2); + assert.equal(body.error, 'Payment required'); + assert.equal(body.resource.url, RESOURCE); + const req = body.accepts[0]; + assert.equal(req.amount, '100000'); + assert.equal(calls.verify + calls.settle + calls.status, 0); + ok('X402_FREE_TRIAL=true → first POST free trial, second POST 402'); +} + +// ---- 3. official PAYMENT header + verify+settle success → 200 ---------------- + +{ + resetMock(); + const res = await post({ PAYMENT: paymentHeader({ nonce: '0x' + 'a1'.repeat(32), amount: '100000' }), 'cf-connecting-ip': '203.0.113.51' }); + assert.equal(res.status, 200); + const settle = decodeB64Json(res.headers.get('payment-response')); + assert.equal(settle.status, 'success'); + const body = await res.json(); + assert.equal(body.schema_version, '0.1'); + assert.equal(calls.verify, 1); + assert.equal(calls.settle, 1); + ok('PAYMENT header accepted; verify+settle success → 200 with PAYMENT-RESPONSE'); +} + +// ---- 4. verify rejects → 402, settle never called ---------------------------- + +{ + resetMock({ verify: () => okxEnvelope({ isValid: false, invalidReason: 'invalid_signature' }) }); + const res = await post({ 'payment-signature': paymentHeader({ nonce: '0x' + 'a2'.repeat(32), amount: '100000' }), 'cf-connecting-ip': '203.0.113.52' }); + assert.equal(res.status, 402); + const body = await res.json(); + assert.equal(body.error, 'invalid_signature'); + assert.equal(calls.verify, 1); + assert.equal(calls.settle, 0); + ok('verify isValid=false → 402 challenge, no settle'); +} + +// ---- 5. settle pending → poll settle/status → success → 200 ------------------ + +{ + resetMock({ + settle: () => okxEnvelope({ + success: false, status: 'pending', transaction: '0x' + 'b1'.repeat(32), network: 'eip155:196' + }), + status: [ + () => okxEnvelope({ success: true, status: 'pending' }), + () => okxEnvelope({ success: true, status: 'success' }) + ] + }); + const res = await post({ PAYMENT: paymentHeader({ nonce: '0x' + 'a3'.repeat(32), amount: '100000' }), 'cf-connecting-ip': '203.0.113.53' }); + assert.equal(res.status, 200); + assert.ok(calls.status >= 2, 'polled settle/status until success'); + const settle = decodeB64Json(res.headers.get('payment-response')); + assert.equal(settle.status, 'success'); + ok('settle pending → status poll confirms → 200 (pending alone never delivers)'); +} + +// ---- 6. settle timeout → 402 pending_tx → replay collects without re-settle -- + +{ + const tx = '0x' + 'c1'.repeat(32); + resetMock({ + settle: () => okxEnvelope({ success: false, status: 'timeout', transaction: tx, network: 'eip155:196' }), + status: [() => okxEnvelope({ success: true, status: 'pending' })] + }); + const header = paymentHeader({ nonce: '0x' + 'a4'.repeat(32), amount: '100000' }); + const res = await post({ PAYMENT: header, 'cf-connecting-ip': '203.0.113.54' }); + assert.equal(res.status, 402); + const body = await res.json(); + assert.equal(body.error, 'settlement_pending'); + assert.equal(body.status, 'pending_tx'); + assert.equal(body.transaction, tx); + assert.equal(body.retry.retry_with_same_payment, true); + assert.equal(calls.settle, 1); + + // tx confirms; buyer replays the SAME payment header + script.status = [() => okxEnvelope({ success: true, status: 'success' })]; + const replay = await post({ PAYMENT: header }); + assert.equal(replay.status, 200); + const replayBody = await replay.json(); + assert.equal(replayBody.schema_version, '0.1'); + assert.equal(calls.settle, 1, 'no second settle on replay'); + assert.equal(calls.verify, 1, 'no second verify on replay'); + const settle = decodeB64Json(replay.headers.get('payment-response')); + assert.equal(settle.status, 'success'); + + // and a third replay hits the success receipt directly (no facilitator calls) + const statusCallsBefore = calls.status; + const third = await post({ PAYMENT: header }); + assert.equal(third.status, 200); + assert.equal(calls.status, statusCallsBefore, 'success receipt served without facilitator'); + ok('settle timeout → 402 pending_tx + txHash; replay same payment → delivered without re-settle'); +} + +// ---- 7. resource mismatch → 402, no facilitator calls ------------------------ + +{ + resetMock(); + const res = await post({ + PAYMENT: paymentHeader({ nonce: '0x' + 'a5'.repeat(32), amount: '100000', resourceUrl: `${BASE}/polymarket-smart-money-radar` }), + 'cf-connecting-ip': '203.0.113.55' + }); + assert.equal(res.status, 402); + const body = await res.json(); + assert.match(body.error, /resource mismatch/i); + assert.equal(calls.verify + calls.settle + calls.status, 0); + ok('payment signed for another resource → 402, facilitator untouched'); +} + +// ---- 8. OKX envelope business error → 502 generic ---------------------------- + +{ + resetMock({ verify: () => okxEnvelope(null, '50113') }); + const res = await post({ PAYMENT: paymentHeader({ nonce: '0x' + 'a6'.repeat(32), amount: '100000' }), 'cf-connecting-ip': '203.0.113.56' }); + assert.equal(res.status, 502); + const body = await res.json(); + assert.equal(body.error, 'facilitator_error'); + assert.equal(body.operation, 'verify'); + assert.equal(body.code, 'okx_business_50113'); + assert.equal(body.message, undefined, 'no upstream body excerpt leaks'); + ok('OKX code!=="0" on HTTP 200 → 502 facilitator_error with generic code only'); +} + +console.log(`All ${passed} x402 worker test groups passed`); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-contract-test.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-contract-test.mjs new file mode 100644 index 00000000..a877b380 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-contract-test.mjs @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict'; +import worker from '../worker/index.mjs'; + +const COMMIT = '0123456789abcdef0123456789abcdef01234567'; +const SLUG = 'runesleo-agent-acceptance-gate'; +const ORIGIN = 'https://api.leolabs.me'; +const env = { + XAGENT_GIT_COMMIT: COMMIT, + XAGENT_PROJECT_SLUG: SLUG +}; + +{ + const res = await worker.fetch(new Request(`${ORIGIN}/health`), env); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.status, 'ok'); + assert.equal(body.commit, COMMIT); +} + +{ + const res = await worker.fetch( + new Request(`${ORIGIN}/.well-known/xagent-verification.json`), + env + ); + assert.equal(res.status, 200); + assert.deepEqual(await res.json(), { + schemaVersion: 1, + slug: SLUG, + commit: COMMIT + }); +} + +{ + const res = await worker.fetch(new Request(`${ORIGIN}/health`), {}); + assert.equal(res.status, 503); + const body = await res.json(); + assert.equal(body.status, 'misconfigured'); + assert.equal(body.commit, null); +} + +{ + const reviewEnv = { ...env, XAGENT_REVIEW_ENABLED: 'true' }; + const res = await worker.fetch( + new Request(`${ORIGIN}/xagent/agent-delivery-acceptance-audit`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + task: 'Verify a read-only Worker delivery.', + delivery_summary: 'Added version-bound health and proof endpoints.', + artifacts: ['worker/index.mjs'], + changed_files: ['worker/index.mjs'], + validation: ['npm test'], + validation_output: 'All tests passed.', + hard_gates: ['no deploy without owner approval'], + next_gate: 'Owner approves deployment.' + }) + }), + reviewEnv + ); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.service_id, 'agent_delivery_acceptance_audit'); + assert.ok(['pass', 'needs_review', 'fail'].includes(body.verdict)); +} + +{ + const reviewEnv = { ...env, XAGENT_REVIEW_ENABLED: 'true' }; + const res = await worker.fetch( + new Request(`${ORIGIN}/xagent/agent-delivery-acceptance-audit`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ task: 'missing delivery summary' }) + }), + reviewEnv + ); + assert.equal(res.status, 400); + const body = await res.json(); + assert.equal(body.error, 'bad_request'); + assert.match(body.message, /delivery_summary/); +} + +{ + const res = await worker.fetch( + new Request(`${ORIGIN}/xagent/agent-delivery-acceptance-audit`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ task: 'disabled review route', delivery_summary: 'none' }) + }), + env + ); + assert.equal(res.status, 404); +} + +console.log('PASS X-Agent deployment identity and review capability contract'); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-submission-contract-test.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-submission-contract-test.mjs new file mode 100644 index 00000000..9276eb81 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/test/xagent-submission-contract-test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; + +const openapi = fs.readFileSync(new URL('../openapi.yaml', import.meta.url), 'utf8'); +const deployment = fs.readFileSync(new URL('../DEPLOYMENT.md', import.meta.url), 'utf8'); +const readme = fs.readFileSync(new URL('../README.md', import.meta.url), 'utf8'); +const reviewEnv = fs.readFileSync( + new URL('../config/xagent-review.env.example', import.meta.url), + 'utf8' +); +const packageJson = JSON.parse( + fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8') +); +const packageLock = JSON.parse( + fs.readFileSync(new URL('../package-lock.json', import.meta.url), 'utf8') +); + +assert.match(openapi, /https:\/\/api\.leolabs\.me/); +assert.match(openapi, /\/\.well-known\/xagent-verification\.json:/); +assert.match(openapi, /\/xagent\/agent-delivery-acceptance-audit:/); +assert.doesNotMatch(openapi, /Not deployed as a public API/i); + +for (const name of [ + 'XAGENT_GIT_COMMIT', + 'XAGENT_PROJECT_SLUG', + 'XAGENT_REVIEW_ENABLED' +]) { + assert.match(deployment, new RegExp(name)); +} +assert.match(deployment, /https:\/\/api\.leolabs\.me\/health/); +assert.match(deployment, /not yet deployed/i); +assert.match(deployment, /--var XAGENT_GIT_COMMIT:/); +assert.match(deployment, /--var XAGENT_PROJECT_SLUG:runesleo-agent-acceptance-gate/); +assert.match(deployment, /--var XAGENT_REVIEW_ENABLED:true/); +assert.doesNotMatch(deployment, /No public API endpoint\./); +assert.doesNotMatch(readme, /No public endpoint exists\./); +assert.match(reviewEnv, /XAGENT_GIT_COMMIT=[0-9a-f]{40}/); +assert.match(reviewEnv, /XAGENT_PROJECT_SLUG=runesleo-agent-acceptance-gate/); +assert.match(reviewEnv, /XAGENT_REVIEW_ENABLED=false/); +assert.equal(packageJson.devDependencies.wrangler, '4.133.0'); +assert.equal(packageLock.packages[''].devDependencies.wrangler, '4.133.0'); + +console.log('PASS X-Agent submission documentation contract'); diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/index.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/index.mjs new file mode 100644 index 00000000..76d2ae15 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/index.mjs @@ -0,0 +1,1115 @@ +import { assessWorldCupSmartMoney } from '../src/worldcup-smart-money.mjs'; +import { + assessPolymarketSmartMoneyLive, + assessWorldCupSmartMoneyLive, + assessSportsSmartMoneyLive +} from '../src/worldcup-smart-money-live.mjs'; +import { + assessOkxAiDataService, + getOkxAiDataServiceByPath, + listOkxAiDataServices +} from '../src/okx-ai-data-services.mjs'; +import { + assessEventPriceDivergenceLive, + buildEventPriceDivergenceFallback +} from '../src/event-price-divergence.mjs'; +import { + assessCryptoMarketRegimeLive, + buildCryptoMarketRegimeFallback +} from '../src/crypto-market-regime.mjs'; +import { + assessWorldCupUpsetAlertLive, + buildWorldCupUpsetAlertFallback, + assessSportsUpsetAlertLive, + buildSportsUpsetAlertFallback +} from '../src/world-cup-upset-alert.mjs'; +import { + assessPmProfileLive, + buildPmProfileFallback +} from '../src/pm-profile.mjs'; +import { + assessPmPnlAuditLive, + buildPmPnlAuditFallback +} from '../src/pm-pnl-audit.mjs'; +import { + assessAgentBudgetPreflight, + buildAgentBudgetPreflightFallback +} from '../src/agent-budget-preflight.mjs'; +import { + assessTokenDdVerdictLive, + buildTokenDdVerdictFallback +} from '../src/token-dd-verdict.mjs'; +import { + assessPmTradePreflightLive, + buildPmTradePreflightFallback +} from '../src/pm-trade-preflight.mjs'; +import { + assessPmEventReadoutLive, + buildPmEventReadoutFallback +} from '../src/pm-event-readout.mjs'; +import { + assessContentVerifyClaims +} from '../src/content-verify-claims.mjs'; +import { + assessContentSlopCheck, + buildContentSlopCheckFallback +} from '../src/content-slop-check.mjs'; +import { + assessPmBrierLive, + buildPmBrierFallback +} from '../src/pm-brier.mjs'; +import { + assessPublishReadiness, + buildPublishReadinessFallback +} from '../src/publish-readiness.mjs'; +import { + assessFinanceCockpitLive, + buildFinanceCockpitFallback +} from '../src/finance-cockpit.mjs'; +import { + assessSportsCockpitLive, + buildSportsCockpitFallback +} from '../src/sports-cockpit.mjs'; +import { + assessWeatherEventReadoutLive, + buildWeatherEventReadoutFallback, + assessPoliticsEventReadoutLive, + buildPoliticsEventReadoutFallback, + assessMacroFedReadoutLive, + buildMacroFedReadoutFallback, + assessFootballMatchCardLive, + buildFootballMatchCardFallback, + assessTennisMatchCardLive, + buildTennisMatchCardFallback, + assessNbaMatchCardLive, + buildNbaMatchCardFallback, + samplePayloadForScenario +} from '../src/pm-scenario-skus.mjs'; +import { + assessPmDecisionCardLive, + buildPmDecisionCardFallback +} from '../src/pm-decision-card.mjs'; +import { + assessPmMarketScanLive, + buildPmMarketScanFallback +} from '../src/pm-market-scan.mjs'; +import { + assessPmMarketHealthLive, + buildPmMarketHealthFallback +} from '../src/pm-market-health.mjs'; +import { + assessPmWalletReportLive, + buildPmWalletReportFallback +} from '../src/pm-wallet-report.mjs'; +import { + assessPmUpdownReadoutLive, + buildPmUpdownReadoutFallback +} from '../src/pm-updown-readout.mjs'; +import { auditDelivery } from '../src/auditor.mjs'; +import { handlePaidRequest, isX402Enabled, X402_CORS_HEADERS } from './x402.mjs'; +import { getFeeAtomicForPath, getServiceCatalogEntry, LISTED_SERVICE_PATHS, SERVICE_CATALOG } from './service-catalog.mjs'; +import { + hasUsedFreeTrial, + isFreeTrialEnabled, + markFreeTrialUsed, + withTrialBilling +} from './trial.mjs'; + +// Base headers are byte-identical to the pre-paywall deployment; x402-specific +// CORS additions are only applied when X402_ENABLED === 'true'. +const JSON_HEADERS = { + 'content-type': 'application/json; charset=utf-8', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET, POST, OPTIONS', + 'access-control-allow-headers': 'content-type' +}; + +// Paid A2MCP endpoints (per-service x402 fee; optional one free trial per IP when X402_FREE_TRIAL=true). +const PAID_RADAR_ROUTES = { + '/world-cup-smart-money-radar': { + description: 'World Cup Smart Money Radar (legacy alias) — sports-generic pipeline scoped to world_cup; prefer /sports-smart-money-radar for other leagues.', + load: (payload) => worldCupRadarWithCache(payload) + }, + '/sports-smart-money-radar': { + description: 'Sports Smart Money Radar — profitable Polymarket wallet signals for football/tennis/NBA/NFL/UFC/MLB etc. Pass sport + optional league/query/tag_slug.', + load: (payload) => sportsSmartMoneyWithCache(payload) + }, + '/polymarket-smart-money-radar': { + description: 'Polymarket Smart Money Radar — site-wide or tag/event_type scoped wallet signals from large public trades.', + load: (payload) => polymarketRadarWithCache(payload) + }, + '/event-price-divergence-radar': { + description: 'Event Price Divergence Radar — flags Polymarket event-probability moves that diverge from 24h crypto spot momentum on OKX.', + load: (payload) => eventPriceDivergenceWithCache(payload) + }, + '/crypto-market-regime-radar': { + description: 'Crypto Market Regime Radar — blends OKX spot momentum, perp funding/premium and Polymarket event-probability drift into an explainable risk_on / risk_off / neutral / mixed regime call with a 0-100 score.', + load: (payload) => cryptoMarketRegimeWithCache(payload) + }, + '/world-cup-upset-alert': { + description: 'World Cup Upset Alert (legacy alias) — prefer /sports-upset-alert for other competitions.', + load: (payload) => worldCupUpsetAlertWithCache(payload) + }, + '/sports-upset-alert': { + description: 'Sports Upset Alert — profitable wallets entering low-probability sports outcomes (football leagues, tennis, NBA, etc.).', + load: (payload) => sportsUpsetAlertWithCache(payload) + }, + '/pm-profile': { + description: 'PM Profile — read-only Polymarket wallet snapshot (7d LB PnL + positions sample). Productized from polymarket-toolkit.', + load: (payload) => pmProfileWithCache(payload) + }, + '/pm-pnl-audit': { + description: 'PM PnL Audit — quick LB vs position cashPnL, or mode=full Worker-safe cashflow replay with honest pagination_incomplete. Data only.', + load: (payload) => pmPnlAuditWithCache(payload) + }, + '/pm-brier': { + description: 'PM Brier — read-only calibration score from settled Polymarket positions (Brier). Productized from polymarket-toolkit.', + load: (payload) => pmBrierWithCache(payload) + }, + '/agent-budget-preflight': { + description: 'Agent Budget Preflight — deterministic buy/skip/reject gate before an agent pays for an API/x402 call. No wallet, no settle. From arc-budget-agent policy.', + load: (payload) => runAgentBudgetPreflight(payload) + }, + '/agent-delivery-acceptance-audit': { + description: 'Agent Delivery Audit Gate — audits an agent task delivery (evidence, validation, hard gates) and returns pass / needs_review / fail with a buyer summary.', + // Deterministic per-payload audit — no cache (every audit input is unique). + load: (payload) => runDeliveryAcceptanceAudit(payload) + }, + '/token-dd-verdict': { + description: 'Token DD Verdict — Standard-lite rule-based token research gate with optional DexScreener heuristics for EVM contracts.', + load: (payload) => tokenDdVerdictWithCache(payload) + }, + '/pm-trade-preflight': { + description: 'PM Trade Preflight — read-only eligible/watch/skip gate + decision-card-lite fields before a Polymarket order.', + load: (payload) => pmTradePreflightWithCache(payload) + }, + '/pm-event-readout': { + description: 'PM Event Analyst — same-event matrix + football/tennis/NBA/politics/weather/macro-Fed/Musk plugins; fixture/hard_veto where applicable.', + load: (payload) => pmEventReadoutWithCache(payload) + }, + '/content-verify-claims': { + description: 'Content Verify Claims — rule-based check that publish claims overlap caller-supplied source excerpts (numbers + keywords); returns pass, needs_review, or fail.', + load: (payload) => runContentVerifyClaims(payload) + }, + '/content-slop-check': { + description: 'Content Slop Check — rule-based AI-filler / spam-pattern detector for draft text; returns slop_score and flags. Not a rewrite service.', + load: (payload) => runContentSlopCheck(payload) + }, + '/publish-readiness': { + description: 'Publish Readiness — combines slop check + claim verify into ready / edit_first / block before publish. No rewrite, no post.', + load: (payload) => runPublishReadiness(payload) + }, + '/finance-cockpit': { + description: 'Finance Cockpit — composed crypto co-pilot: regime score + event-price divergence in one card. Data only.', + load: (payload) => financeCockpitWithCache(payload) + }, + '/sports-cockpit': { + description: 'Sports Cockpit — composed sports co-pilot: smart-money + upset alerts (+ wallet cohort) in one card. Data only.', + load: (payload) => sportsCockpitWithCache(payload) + }, + '/weather-event-readout': { + description: 'Weather Event Readout — temperature-ladder + hard_veto_gaps/adjacent-ladder; optional caller weather{}; no scrape. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('weather_event_readout', payload) + }, + '/politics-event-readout': { + description: 'Politics Event Readout — election/politics yes-mass ladder card. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('politics_event_readout', payload) + }, + '/macro-fed-readout': { + description: 'Macro Fed Readout — Fed/FOMC L1 rate ladder + expected-move heuristic; honest anchor gaps. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('macro_fed_readout', payload) + }, + '/football-match-card': { + description: 'Football Match Card — matrix + fixture gate + hard_veto_gaps + expression compare; match vs outright. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('football_match_card', payload) + }, + '/tennis-match-card': { + description: 'Tennis Match Card — format-aware matrix + fixture/hard_veto_gaps + domination check. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('tennis_match_card', payload) + }, + '/nba-match-card': { + description: 'NBA Match Card — moneyline/spread/totals matrix; match vs outright filter. Pass query or slug.', + load: (payload) => scenarioSkuWithCache('nba_match_card', payload) + }, + '/pm-decision-card': { + description: 'PM Decision Card — opportunity_state + skip/watch/eligible; optional size/bankroll → share-first quantity. Replay before each order. Not a buy tip.', + load: (payload) => pmDecisionCardWithCache(payload) + }, + '/pm-market-scan': { + description: 'PM Market Scan — read-only Gamma volume+spread scanner (polymarket-toolkit pm scan). Optional query; min_volume + limit.', + load: (payload) => pmMarketScanWithCache(payload) + }, + '/pm-market-health': { + description: 'PM Market Health — spread / depth / overround snapshot for one market or event. Read-only; not a buy tip.', + load: (payload) => pmMarketHealthWithCache(payload) + }, + '/pm-wallet-report': { + description: 'PM Wallet Report — one-pager composing profile + brier + pnl audit (quick/full). Read-only toolkit Drawer A.', + load: (payload) => pmWalletReportWithCache(payload) + }, + '/pm-updown-readout': { + description: 'PM Up/Down Readout — crypto up/down event surface + resolution-source pitfalls (polymarket-toolkit pm updown). Read-only.', + load: (payload) => pmUpdownReadoutWithCache(payload) + } +}; + +export default { + async fetch(request, env) { + try { + if (request.method === 'OPTIONS') { + return new Response(null, { + status: 204, + headers: isX402Enabled(env) ? { ...JSON_HEADERS, ...X402_CORS_HEADERS } : JSON_HEADERS + }); + } + + const url = new URL(request.url); + + if (request.method === 'GET' && url.pathname === '/health') { + const identity = xAgentDeploymentIdentity(env); + if (!identity.ok) { + return json({ + status: 'misconfigured', + commit: null, + error: identity.error + }, 503); + } + return json({ + status: 'ok', + commit: identity.commit, + ok: true, + service: 'agent-acceptance-gate', + mode: 'edge_worker', + launch_lane: 'okx_ai_asp' + }); + } + + if ( + request.method === 'GET' + && url.pathname === '/.well-known/xagent-verification.json' + ) { + const identity = xAgentDeploymentIdentity(env); + if (!identity.ok) { + return json({ + schemaVersion: 1, + slug: identity.slug, + commit: null, + error: identity.error + }, 503); + } + return json({ + schemaVersion: 1, + slug: identity.slug, + commit: identity.commit + }); + } + + if (request.method === 'GET' && url.pathname === '/api/okx-ai-services') { + const trialOn = isFreeTrialEnabled(env); + const listed = [...LISTED_SERVICE_PATHS].map((path) => { + const meta = SERVICE_CATALOG[path]; + const route = PAID_RADAR_ROUTES[path]; + const row = { + service_id: meta.service_id, + path, + title: meta.title, + category: meta.category, + fee_usdt: meta.fee_usdt, + description: route?.description ?? meta.title, + mode: meta.mode + }; + if (trialOn) row.free_trial = 'one_per_client_ip_per_service'; + return row; + }); + const unlisted = Object.entries(SERVICE_CATALOG) + .filter(([path]) => !LISTED_SERVICE_PATHS.has(path)) + .map(([path, meta]) => ({ + service_id: meta.service_id, + path, + title: meta.title, + category: meta.category, + fee_usdt: meta.fee_usdt, + mode: meta.mode + })); + return json({ + schema_version: '0.1', + mode: 'edge_worker_live', + billing: { + x402_enabled: isX402Enabled(env), + free_trial: trialOn + ? 'One POST per client IP per service path, then x402 at fee_usdt.' + : 'disabled (unpaid POST returns 402; set X402_FREE_TRIAL=true to opt in)', + sample_get: 'GET the same service path for a public sample payload.' + }, + fulfillment: { + model: 'edge_on_demand', + host: 'Cloudflare Worker at api.leolabs.me', + operator_always_online: false, + always_on_agent_required: false, + llm_api_key_required: false, + detail: + 'Each paid POST is fulfilled synchronously by the Worker: verify x402 → fetch public data / run rules → return JSON. No seller laptop, no standing agent, no LLM key for listed SKUs.' + }, + services: [...listed, ...unlisted] + }); + } + + if ( + request.method === 'POST' + && url.pathname === '/xagent/agent-delivery-acceptance-audit' + ) { + if (!isXAgentReviewEnabled(env)) { + return json({ + error: 'not_found', + message: 'X-Agent review capability is disabled.' + }, 404); + } + const identity = xAgentDeploymentIdentity(env); + if (!identity.ok) { + return json({ + error: 'misconfigured', + message: identity.error + }, 503); + } + return json(runDeliveryAcceptanceAudit(await readJson(request))); + } + + if (request.method === 'GET' && PAID_RADAR_ROUTES[url.pathname]) { + // Edge-cache public samples: each GET used to hit upstream data sources + // live, so an unauthenticated crawler could burn upstream API quota. + const sampleCache = globalThis.caches?.default; + const sampleCacheKey = new Request(`${url.origin}${url.pathname}`, { method: 'GET' }); + if (sampleCache) { + const cachedSample = await sampleCache.match(sampleCacheKey); + if (cachedSample) return cachedSample; + } + const route = PAID_RADAR_ROUTES[url.pathname]; + const meta = getServiceCatalogEntry(url.pathname); + const sampleRequest = samplePayloadForPath(url.pathname); + const sampleResponse = await route.load(sampleRequest); + const trialOn = isFreeTrialEnabled(env); + const body = { + schema_version: '0.1', + mode: 'public_sample', + path: url.pathname, + fee_usdt: meta?.fee_usdt ?? null, + sample_request: sampleRequest, + sample_response: sampleResponse + }; + if (trialOn) { + body.free_trial = 'POST once without payment per client IP, then x402.'; + } else { + body.billing = { + mode: 'x402', + unpaid_post: 'HTTP 402 payment-required challenge' + }; + } + const sampleResp = json(body, 200, { 'cache-control': 'public, max-age=600' }); + if (sampleCache) await sampleCache.put(sampleCacheKey, sampleResp.clone()); + return sampleResp; + } + + if (request.method === 'POST' && PAID_RADAR_ROUTES[url.pathname]) { + const route = PAID_RADAR_ROUTES[url.pathname]; + const pathname = url.pathname; + const catalog = getServiceCatalogEntry(pathname); + const priceAtomic = getFeeAtomicForPath(pathname); + + if (!isX402Enabled(env)) { + return json(await route.load(await readJson(request))); + } + + const hasPayment = request.headers.get('payment') + || request.headers.get('payment-signature') + || request.headers.get('x-payment'); + + // Opt-in free trial only. Default unpaid path must 402 for OKX listing checks. + if ( + !hasPayment + && isFreeTrialEnabled(env) + && !(await hasUsedFreeTrial(env, request, pathname)) + ) { + const payload = await route.load(await readJson(request)); + await markFreeTrialUsed(env, request, pathname); + return json( + withTrialBilling(payload, { pathname, fee_usdt: catalog?.fee_usdt ?? '0.1' }), + 200, + X402_CORS_HEADERS + ); + } + + return handlePaidRequest(request, env, { + resourceUrl: `${url.origin}${pathname}`, + description: route.description, + priceAtomic, + deliver: async () => route.load(await readJson(request)), + respond: (payload, status = 200, extraHeaders = undefined) => + json(payload, status, { ...X402_CORS_HEADERS, ...(extraHeaders || {}) }) + }); + } + + if (request.method === 'POST') { + const service = getOkxAiDataServiceByPath(url.pathname); + if (service) { + const payload = await readJson(request); + return json(assessOkxAiDataService(service, payload)); + } + } + + return json({ + error: 'not_found', + message: 'Use GET /health, GET /api/okx-ai-services, or POST one of the listed service paths.' + }, 404); + } catch (error) { + return json({ + error: 'bad_request', + message: error instanceof Error ? error.message : String(error) + }, 400); + } + } +}; + +// In-memory per-isolate cache for radar responses. Shields the paid A2MCP +// endpoints from Polymarket rate limits; entries expire after CACHE_TTL_MS. +const CACHE_TTL_MS = 120_000; +const radarCache = new Map(); + +async function worldCupRadarWithCache(payload) { + const market = String(payload?.market ?? payload?.market_id ?? payload?.query ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `wc|${market}|${limit}`, + loadLive: () => assessWorldCupSmartMoneyLive(payload), + loadFallback: () => assessWorldCupSmartMoney(payload) + }); +} + +async function sportsSmartMoneyWithCache(payload) { + const sport = String(payload?.sport ?? 'all').trim().toLowerCase(); + const league = String(payload?.league ?? '').trim().toLowerCase(); + const query = String(payload?.query ?? payload?.market ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `sports-sm|${sport}|${league}|${query}|${limit}`, + loadLive: () => assessSportsSmartMoneyLive(payload), + loadFallback: () => assessWorldCupSmartMoney(payload) + }); +} + +async function sportsUpsetAlertWithCache(payload) { + const sport = String(payload?.sport ?? 'all').trim().toLowerCase(); + const league = String(payload?.league ?? '').trim().toLowerCase(); + const query = String(payload?.query ?? payload?.market ?? 'all').trim().toLowerCase(); + const maxProb = String(payload?.max_prob ?? payload?.max_implied_probability ?? '0.35'); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `sports-upset|${sport}|${league}|${query}|${maxProb}|${limit}`, + loadLive: () => assessSportsUpsetAlertLive(payload), + loadFallback: () => buildSportsUpsetAlertFallback(payload) + }); +} + +async function pmProfileWithCache(payload) { + const key = String(payload?.address ?? payload?.wallet ?? payload?.username ?? payload?.query ?? '').trim().toLowerCase(); + if (!key) { + throw new Error('pm-profile requires address or username.'); + } + + return radarWithCache({ + cacheKey: `pm-profile|${key}`, + loadLive: () => assessPmProfileLive(payload), + loadFallback: () => buildPmProfileFallback(payload) + }); +} + +async function pmPnlAuditWithCache(payload) { + const key = String(payload?.address ?? payload?.wallet ?? payload?.username ?? payload?.query ?? '').trim().toLowerCase(); + if (!key) { + throw new Error('pm-pnl-audit requires address or username.'); + } + const mode = String(payload?.mode ?? 'quick').trim().toLowerCase() === 'full' ? 'full' : 'quick'; + const limit = Number.parseInt(payload?.positions_limit ?? payload?.limit, 10) || 100; + + return radarWithCache({ + cacheKey: `pm-pnl-audit|${key}|${mode}|${limit}`, + loadLive: () => assessPmPnlAuditLive(payload), + loadFallback: () => buildPmPnlAuditFallback(payload) + }); +} + +async function pmBrierWithCache(payload) { + const key = String(payload?.address ?? payload?.wallet ?? payload?.username ?? payload?.query ?? '').trim().toLowerCase(); + if (!key) { + throw new Error('pm-brier requires address or username.'); + } + const limit = Number.parseInt(payload?.limit, 10) || 200; + + return radarWithCache({ + cacheKey: `pm-brier|${key}|${limit}`, + loadLive: () => assessPmBrierLive(payload), + loadFallback: () => buildPmBrierFallback(payload) + }); +} + +async function polymarketRadarWithCache(payload) { + const query = String(payload?.market ?? payload?.topic ?? payload?.query ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `pm|${query}|${limit}`, + loadLive: () => assessPolymarketSmartMoneyLive(payload), + loadFallback: () => { + const service = getOkxAiDataServiceByPath('/polymarket-smart-money-radar'); + return assessOkxAiDataService(service, payload); + } + }); +} + +async function eventPriceDivergenceWithCache(payload) { + const asset = String(payload?.asset ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `divergence|${asset}|${limit}`, + loadLive: () => assessEventPriceDivergenceLive(payload), + loadFallback: () => buildEventPriceDivergenceFallback(payload) + }); +} + +async function cryptoMarketRegimeWithCache(payload) { + const focus = String(payload?.focus ?? payload?.asset ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `regime|${focus}|${limit}`, + loadLive: () => assessCryptoMarketRegimeLive(payload), + loadFallback: () => buildCryptoMarketRegimeFallback(payload) + }); +} + +async function financeCockpitWithCache(payload) { + const focus = String(payload?.focus ?? payload?.asset ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `finance-cockpit|${focus}|${limit}`, + loadLive: () => assessFinanceCockpitLive(payload), + loadFallback: () => buildFinanceCockpitFallback(payload) + }); +} + +async function sportsCockpitWithCache(payload) { + const sport = String(payload?.sport ?? 'all').trim().toLowerCase(); + const league = String(payload?.league ?? '').trim().toLowerCase(); + const query = String(payload?.query ?? payload?.market ?? 'all').trim().toLowerCase(); + const maxProb = String(payload?.max_prob ?? payload?.max_implied_probability ?? '0.35'); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `sports-cockpit|${sport}|${league}|${query}|${maxProb}|${limit}`, + loadLive: () => assessSportsCockpitLive(payload), + loadFallback: () => buildSportsCockpitFallback(payload) + }); +} + +const SCENARIO_LOADERS = { + weather_event_readout: { + live: assessWeatherEventReadoutLive, + fallback: buildWeatherEventReadoutFallback + }, + politics_event_readout: { + live: assessPoliticsEventReadoutLive, + fallback: buildPoliticsEventReadoutFallback + }, + macro_fed_readout: { + live: assessMacroFedReadoutLive, + fallback: buildMacroFedReadoutFallback + }, + football_match_card: { + live: assessFootballMatchCardLive, + fallback: buildFootballMatchCardFallback + }, + tennis_match_card: { + live: assessTennisMatchCardLive, + fallback: buildTennisMatchCardFallback + }, + nba_match_card: { + live: assessNbaMatchCardLive, + fallback: buildNbaMatchCardFallback + } +}; + +async function scenarioSkuWithCache(scenarioId, payload) { + const loader = SCENARIO_LOADERS[scenarioId]; + if (!loader) throw new Error(`Unknown scenario ${scenarioId}`); + const ref = String( + payload?.condition_id + ?? payload?.slug + ?? payload?.market_url + ?? payload?.query + ?? payload?.market + ?? 'default' + ).trim().toLowerCase(); + + return radarWithCache({ + cacheKey: `scenario|${scenarioId}|${ref}`, + loadLive: () => loader.live(payload), + loadFallback: () => loader.fallback(payload) + }); +} + +async function pmDecisionCardWithCache(payload) { + const ref = String( + payload?.condition_id + ?? payload?.slug + ?? payload?.market_url + ?? '' + ).trim().toLowerCase(); + if (!ref) { + throw new Error('pm-decision-card requires market_url, condition_id, or slug.'); + } + const side = String(payload?.side ?? 'yes').trim().toLowerCase(); + const includeEvent = payload?.include_event_context === false ? '0' : '1'; + + return radarWithCache({ + cacheKey: `decision-card|${ref}|${side}|${includeEvent}|${payload?.size_usd ?? ''}`, + loadLive: () => assessPmDecisionCardLive(payload), + loadFallback: () => buildPmDecisionCardFallback(payload) + }); +} + +async function pmMarketScanWithCache(payload) { + const query = String(payload?.query ?? payload?.q ?? '').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 10; + const minVolume = Number(payload?.min_volume ?? payload?.minVolume ?? 1000) || 1000; + + return radarWithCache({ + cacheKey: `pm-scan|${query || 'top'}|${limit}|${minVolume}`, + loadLive: () => assessPmMarketScanLive(payload), + loadFallback: () => buildPmMarketScanFallback(payload) + }); +} + +async function pmMarketHealthWithCache(payload) { + const ref = String( + payload?.event_slug + ?? payload?.eventSlug + ?? payload?.condition_id + ?? payload?.slug + ?? payload?.market_url + ?? '' + ).trim().toLowerCase(); + if (!ref) { + throw new Error('pm-market-health requires market_url, slug, condition_id, or event_slug.'); + } + + return radarWithCache({ + cacheKey: `pm-health|${ref}`, + loadLive: () => assessPmMarketHealthLive(payload), + loadFallback: () => buildPmMarketHealthFallback(payload) + }); +} + +async function pmWalletReportWithCache(payload) { + const key = String(payload?.address ?? payload?.wallet ?? payload?.username ?? payload?.query ?? '').trim().toLowerCase(); + if (!key) { + throw new Error('pm-wallet-report requires address or username.'); + } + const pnlMode = String(payload?.pnl_mode ?? payload?.mode ?? 'quick').trim().toLowerCase() === 'full' ? 'full' : 'quick'; + + return radarWithCache({ + cacheKey: `pm-wallet-report|${key}|${pnlMode}`, + loadLive: () => assessPmWalletReportLive(payload), + loadFallback: () => buildPmWalletReportFallback(payload) + }); +} + +async function pmUpdownReadoutWithCache(payload) { + const ref = String( + payload?.event_slug + ?? payload?.slug + ?? payload?.query + ?? payload?.q + ?? '' + ).trim().toLowerCase(); + if (!ref) { + throw new Error('pm-updown-readout requires event_slug/slug or query.'); + } + + return radarWithCache({ + cacheKey: `pm-updown|${ref}`, + loadLive: () => assessPmUpdownReadoutLive(payload), + loadFallback: () => buildPmUpdownReadoutFallback(payload) + }); +} + +async function worldCupUpsetAlertWithCache(payload) { + const market = String(payload?.market ?? payload?.market_id ?? payload?.query ?? 'all').trim().toLowerCase(); + const limit = Number.parseInt(payload?.limit, 10) || 5; + + return radarWithCache({ + cacheKey: `upset|${market}|${limit}`, + loadLive: () => assessWorldCupUpsetAlertLive(payload), + loadFallback: () => buildWorldCupUpsetAlertFallback(payload) + }); +} + +async function tokenDdVerdictWithCache(payload) { + const asset = String(payload?.asset ?? payload?.token ?? payload?.query ?? '').trim().toLowerCase(); + if (!asset) { + throw new Error('token-dd-verdict requires asset (ticker, contract address, or URL).'); + } + + return radarWithCache({ + cacheKey: `token-dd|${asset}`, + loadLive: () => assessTokenDdVerdictLive(payload), + loadFallback: () => buildTokenDdVerdictFallback(payload) + }); +} + +async function pmTradePreflightWithCache(payload) { + const ref = String( + payload?.condition_id + ?? payload?.slug + ?? payload?.market_url + ?? '' + ).trim().toLowerCase(); + const side = String(payload?.side ?? 'yes').trim().toLowerCase(); + if (!ref) { + throw new Error('pm-trade-preflight requires market_url, condition_id, or slug.'); + } + + return radarWithCache({ + cacheKey: `preflight|${ref}|${side}|${payload?.size_usd ?? ''}`, + loadLive: () => assessPmTradePreflightLive(payload), + loadFallback: () => buildPmTradePreflightFallback(payload) + }); +} + +async function pmEventReadoutWithCache(payload) { + const ref = String( + payload?.condition_id + ?? payload?.slug + ?? payload?.market_url + ?? '' + ).trim().toLowerCase(); + if (!ref) { + throw new Error('pm-event-readout requires market_url, condition_id, or slug.'); + } + + // Fixture / musk / tennis options change the enriched output — must be part of cache key. + const football = payload?.football && typeof payload.football === 'object' ? payload.football : null; + const tennis = payload?.tennis && typeof payload.tennis === 'object' ? payload.tennis : null; + const nba = payload?.nba && typeof payload.nba === 'object' ? payload.nba : null; + const nfl = payload?.nfl && typeof payload.nfl === 'object' ? payload.nfl : null; + const musk = payload?.musk && typeof payload.musk === 'object' ? payload.musk : null; + const optionKey = [ + football?.verified === true ? `fv:${football.market_fixture_match || 'yes'}` : 'fv:none', + tennis?.verified === true ? `tv:${tennis.market_fixture_match || 'yes'}` : 'tv:none', + nba?.verified === true ? `nv:${nba.market_fixture_match || 'yes'}` : 'nv:none', + nfl?.verified === true ? `nf:${nfl.market_fixture_match || 'yes'}` : 'nf:none', + musk?.current_count != null ? `mc:${musk.current_count}` : 'mc:none' + ].join('|'); + + return radarWithCache({ + cacheKey: `readout|${ref}|${optionKey}`, + loadLive: () => assessPmEventReadoutLive(payload), + loadFallback: () => buildPmEventReadoutFallback(payload) + }); +} + +function runContentVerifyClaims(payload) { + return assessContentVerifyClaims(payload); +} + +function runContentSlopCheck(payload) { + try { + return assessContentSlopCheck(payload); + } catch (error) { + if (String(error?.message || error).includes('required')) throw error; + return buildContentSlopCheckFallback(payload); + } +} + +function runAgentBudgetPreflight(payload) { + try { + return assessAgentBudgetPreflight(payload); + } catch (error) { + if (String(error?.message || error).includes('required')) throw error; + return buildAgentBudgetPreflightFallback(payload); + } +} + +function runPublishReadiness(payload) { + try { + return assessPublishReadiness(payload); + } catch (error) { + if (String(error?.message || error).includes('required')) throw error; + return buildPublishReadinessFallback(payload); + } +} + +// ---- Agent Delivery Audit Gate --------------------------------------------- +// Accepts either the full auditor schema ({task, delivery, context}) or the +// compact buyer shape {task, delivery_summary, artifacts, validation, +// hard_gates?, next_gate?} and adapts it before calling auditDelivery. +// Invalid input throws BEFORE x402 settle, so a bad request never burns a payment. +function runDeliveryAcceptanceAudit(payload) { + const input = normalizeAuditInput(payload); + return auditDelivery(input); +} + +function normalizeAuditInput(payload) { + if (!payload || typeof payload !== 'object') { + throw new Error('Audit input must be a JSON object with at least {task, delivery_summary}.'); + } + + // Full auditor schema passes through untouched (context defaulted). + if (payload.delivery && typeof payload.delivery === 'object') { + return { ...payload, context: payload.context ?? { repo_state: 'unknown' } }; + } + + const task = typeof payload.task === 'string' + ? { buyer_goal: payload.task } + : (payload.task && typeof payload.task === 'object' ? { ...payload.task } : null); + if (task && !task.buyer_goal) { + task.buyer_goal = task.goal ?? task.description ?? null; + } + if (!task?.buyer_goal || !payload.delivery_summary) { + throw new Error('Audit input requires task (string or {buyer_goal, ...}) and delivery_summary. Optional: artifacts[], validation[], changed_files[], hard_gates[], next_gate, context.'); + } + + return { + schema_version: '0.1', + mode: payload.mode ?? 'full', + task, + delivery: { + writeback_text: String(payload.delivery_summary), + artifact_paths: toStringArray(payload.artifacts), + changed_files: toStringArray(payload.changed_files), + validation: toStringArray(payload.validation), + validation_output: payload.validation_output ?? null, + rollback_plan: payload.rollback_plan ?? null, + hard_gates_declared: toStringArray(payload.hard_gates), + next_gate: payload.next_gate + ? String(payload.next_gate) + : 'Buyer manual review before acceptance (seller declared no next gate).' + }, + context: payload.context && typeof payload.context === 'object' + ? payload.context + : { repo_state: payload.repo_state ?? 'unknown' } + }; +} + +function toStringArray(value) { + if (value === null || value === undefined) return []; + const items = Array.isArray(value) ? value : [value]; + return items.map((item) => String(item)).filter((item) => item.trim().length > 0); +} + +async function radarWithCache({ cacheKey, loadLive, loadFallback }) { + const cached = radarCache.get(cacheKey); + if (cached && Date.now() - cached.storedAt < CACHE_TTL_MS) { + return { ...cached.payload, cache: 'hit' }; + } + + try { + const live = await loadLive(); + radarCache.set(cacheKey, { storedAt: Date.now(), payload: live }); + pruneCache(); + return live; + } catch (error) { + // Never 5xx a paid call: serve stale cache first, then degraded demo data. + if (cached) { + return { + ...cached.payload, + cache: 'stale', + mode: 'degraded_stale_cache', + capability_status: 'upstream_degraded', + caveats: [ + `Live upstream fetch failed (${error instanceof Error ? error.message : String(error)}); serving stale cached live data.`, + 'Treat this as stale evidence and retry shortly before acting.', + ...(cached.payload.caveats || []) + ] + }; + } + const fallback = loadFallback(); + fallback.mode = 'degraded'; + fallback.capability_status = 'upstream_degraded'; + fallback.action = fallback.action ?? 'retry_later'; + fallback.live_data_status = 'unavailable'; + fallback.caveats = [ + `Live upstream fetch failed (${error instanceof Error ? error.message : String(error)}); serving static fallback data only.`, + 'This is not a live market readout. Do not treat sample/static fields as current capability output; retry shortly for live data.', + ...fallback.caveats + ]; + fallback.source = { + ...(fallback.source || {}), + provider: fallback.source?.provider ?? 'static_fallback', + degraded_reason: 'upstream_fetch_failed' + }; + return fallback; + } +} + +function pruneCache() { + if (radarCache.size <= 32) return; + const oldestFirst = [...radarCache.entries()].sort((a, b) => a[1].storedAt - b[1].storedAt); + for (const [key] of oldestFirst.slice(0, radarCache.size - 32)) { + radarCache.delete(key); + } +} + +function samplePayloadForPath(pathname) { + switch (pathname) { + case '/agent-delivery-acceptance-audit': + return { + task: 'Ship a read-only health endpoint for the worker.', + delivery_summary: 'Added GET /health and npm test passes.', + artifacts: ['worker/index.mjs'], + validation: ['npm test'] + }; + case '/event-price-divergence-radar': + return { asset: 'bitcoin', limit: 2 }; + case '/polymarket-smart-money-radar': + return { query: 'bitcoin', limit: 2 }; + case '/world-cup-smart-money-radar': + case '/world-cup-upset-alert': + return { query: 'all', limit: 2 }; + case '/sports-smart-money-radar': + case '/sports-upset-alert': + return { sport: 'football', league: 'epl', limit: 2 }; + case '/pm-profile': + return { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a' }; + case '/pm-pnl-audit': + return { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a', mode: 'quick' }; + case '/pm-brier': + return { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a', limit: 50 }; + case '/agent-budget-preflight': + return { + budget_cap_usdt: 1, + spent_usdt: 0.2, + max_per_call_usdt: 0.5, + evidence_sufficient: false, + offer: { + provider: 'leo-labs', + price_usdt: 0.1, + resource_url: 'https://api.leolabs.me/pm-profile' + } + }; + case '/crypto-market-regime-radar': + return { focus: 'bitcoin', limit: 2 }; + case '/token-dd-verdict': + return { asset: '0x000000000000000000000000000000000000dead' }; + case '/pm-trade-preflight': + return { slug: 'will-argentina-win-the-2026-fifa-world-cup-245', side: 'yes' }; + case '/pm-event-readout': + return { + slug: 'fifwc-fra-mar-2026-07-09-fra', + football: { + verified: true, + market_fixture_match: 'yes', + scheduled_time_utc: '2026-07-09T20:00:00Z', + fixture_sources: ['caller_verified'] + } + }; + case '/content-verify-claims': + return { + claims: ['Platform has about 360 ASPs and roughly 3000 cumulative calls.'], + sources: [{ + text: 'Marketplace scan on 2026-07-07: 358 unique ASPs and about 2982 cumulative soldCount.' + }] + }; + case '/content-slop-check': + return { + text: 'In today\'s digital landscape, it is crucial to delve into synergy and leverage robust holistic frameworks. As an AI, I am excited to underscore the importance of this game-changer.' + }; + case '/publish-readiness': + return { + text: 'Marketplace scan on 2026-07-07 found 358 unique ASPs and 2982 cumulative soldCount.', + claims: ['Marketplace has 358 unique ASPs and 2982 cumulative soldCount.'], + sources: [{ text: 'Marketplace scan on 2026-07-07 found 358 unique ASPs and 2982 cumulative soldCount.' }] + }; + case '/finance-cockpit': + return { focus: 'bitcoin', limit: 2 }; + case '/sports-cockpit': + return { sport: 'football', league: 'epl', limit: 2 }; + case '/weather-event-readout': + return samplePayloadForScenario('weather_event_readout'); + case '/politics-event-readout': + return samplePayloadForScenario('politics_event_readout'); + case '/macro-fed-readout': + return samplePayloadForScenario('macro_fed_readout'); + case '/football-match-card': + return samplePayloadForScenario('football_match_card'); + case '/tennis-match-card': + return samplePayloadForScenario('tennis_match_card'); + case '/nba-match-card': + return samplePayloadForScenario('nba_match_card'); + case '/pm-decision-card': + return { + slug: 'will-argentina-win-the-2026-fifa-world-cup-245', + side: 'yes', + size_usd: 25, + include_event_context: true + }; + case '/pm-market-scan': + return { limit: 5, min_volume: 1000 }; + case '/pm-market-health': + return { slug: 'will-argentina-win-the-2026-fifa-world-cup-245' }; + case '/pm-wallet-report': + return { address: '0x63ce342161250d705dc0b16df89036c8e5f9ba9a', pnl_mode: 'quick' }; + case '/pm-updown-readout': + return { query: 'btc updown' }; + default: + return { limit: 2 }; + } +} + +async function readJson(request) { + const text = await request.text(); + if (!text.trim()) return {}; + if (text.length > 1_000_000) { + throw new Error('Request body too large'); + } + return JSON.parse(text); +} + +function isXAgentReviewEnabled(env) { + return String(env?.XAGENT_REVIEW_ENABLED ?? '').trim().toLowerCase() === 'true'; +} + +function xAgentDeploymentIdentity(env) { + const commit = String(env?.XAGENT_GIT_COMMIT ?? '').trim().toLowerCase(); + const slug = String( + env?.XAGENT_PROJECT_SLUG ?? 'runesleo-agent-acceptance-gate' + ).trim().toLowerCase(); + + if (!/^[0-9a-f]{40}$/.test(commit)) { + return { + ok: false, + slug, + commit: null, + error: 'XAGENT_GIT_COMMIT must be the exact 40-character deployed Git commit.' + }; + } + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) { + return { + ok: false, + slug, + commit: null, + error: 'XAGENT_PROJECT_SLUG must be a lowercase kebab-case slug.' + }; + } + return { ok: true, slug, commit }; +} + +function json(payload, status = 200, extraHeaders = undefined) { + return new Response(`${JSON.stringify(payload, null, 2)}\n`, { + status, + headers: extraHeaders ? { ...JSON_HEADERS, ...extraHeaders } : JSON_HEADERS + }); +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/service-catalog.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/service-catalog.mjs new file mode 100644 index 00000000..bd93b29d --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/service-catalog.mjs @@ -0,0 +1,646 @@ +/** Listed OKX.AI services: per-path fee (USDT string + x402 atomic units). + * okx_service_id synced to live Agent #3977 receipt 2026-07-24 (30207–30216). + */ +export const LISTED_SERVICE_PATHS = new Set([ + '/world-cup-smart-money-radar', + '/polymarket-smart-money-radar', + '/agent-delivery-acceptance-audit', + '/event-price-divergence-radar', + '/crypto-market-regime-radar', + '/world-cup-upset-alert', + '/token-dd-verdict', + '/pm-trade-preflight', + '/pm-event-readout', + '/content-verify-claims', + '/sports-smart-money-radar', + '/sports-upset-alert', + '/pm-profile', + '/pm-pnl-audit', + '/content-slop-check', + '/agent-budget-preflight', + '/pm-brier', + '/publish-readiness', + '/finance-cockpit', + '/sports-cockpit', + '/weather-event-readout', + '/politics-event-readout', + '/macro-fed-readout', + '/football-match-card', + '/tennis-match-card', + '/nba-match-card', + '/pm-decision-card', + '/pm-market-scan', + '/pm-market-health', + '/pm-wallet-report', + '/pm-updown-readout' +]); + +export const SERVICE_CATALOG = { + '/world-cup-smart-money-radar': { + service_id: 'world_cup_smart_money_radar', + okx_service_id: 30207, + title: 'World Cup Smart Money Radar', + category: 'world_cup', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/polymarket-smart-money-radar': { + service_id: 'polymarket_smart_money_radar', + okx_service_id: 30208, + title: 'Polymarket Smart Money Radar', + category: 'finance', + fee_usdt: '0.05', + fee_atomic: '50000', + mode: 'live' + }, + '/agent-delivery-acceptance-audit': { + service_id: 'agent_delivery_acceptance_audit', + okx_service_id: 30209, + title: 'Agent Delivery Audit Gate', + category: 'agent_ops', + fee_usdt: '0.2', + fee_atomic: '200000', + mode: 'live' + }, + '/event-price-divergence-radar': { + service_id: 'event_price_divergence_radar', + okx_service_id: 30210, + title: 'Event Price Divergence Radar', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/crypto-market-regime-radar': { + service_id: 'crypto_market_regime_radar', + okx_service_id: 30211, + title: 'Crypto Market Regime Radar', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/world-cup-upset-alert': { + service_id: 'world_cup_upset_alert', + okx_service_id: 30212, + title: 'World Cup Upset Alert', + category: 'world_cup', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/token-dd-verdict': { + service_id: 'token_dd_verdict', + okx_service_id: 30213, + title: 'Token DD Verdict', + category: 'finance', + fee_usdt: '0.05', + fee_atomic: '50000', + mode: 'live' + }, + '/pm-trade-preflight': { + service_id: 'pm_trade_preflight', + okx_service_id: 30214, + title: 'PM Trade Preflight', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-event-readout': { + service_id: 'pm_event_readout', + okx_service_id: 30215, + title: 'PM Event Analyst (Football-ready)', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/content-verify-claims': { + service_id: 'content_verify_claims', + okx_service_id: 30216, + title: 'Content Verify Claims', + category: 'agent_ops', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + // Code-ready; NOT on OKX listing until Leo authorize create/activate + '/content-slop-check': { + service_id: 'content_slop_check', + okx_service_id: 36664, + title: 'Content Slop Check', + category: 'agent_ops', + fee_usdt: '0.05', + fee_atomic: '50000', + mode: 'live' + }, + '/sports-smart-money-radar': { + service_id: 'sports_smart_money_radar', + okx_service_id: 36661, + title: 'Sports Smart Money Radar', + category: 'sports', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/sports-upset-alert': { + service_id: 'sports_upset_alert', + okx_service_id: 36662, + title: 'Sports Upset Alert', + category: 'sports', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-profile': { + service_id: 'pm_profile', + okx_service_id: 36663, + title: 'PM Profile', + category: 'finance', + fee_usdt: '0.05', + fee_atomic: '50000', + mode: 'live' + }, + '/pm-pnl-audit': { + service_id: 'pm_pnl_audit', + okx_service_id: 37157, + title: 'PM PnL Audit', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/agent-budget-preflight': { + service_id: 'agent_budget_preflight', + okx_service_id: 36666, + title: 'Agent Budget Preflight', + category: 'agent_ops', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-brier': { + service_id: 'pm_brier', + okx_service_id: 36668, + title: 'PM Brier', + category: 'finance', + fee_usdt: '0.05', + fee_atomic: '50000', + mode: 'live' + }, + '/publish-readiness': { + service_id: 'publish_readiness', + okx_service_id: 36669, + title: 'Publish Readiness', + category: 'agent_ops', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/finance-cockpit': { + service_id: 'finance_cockpit', + okx_service_id: 36670, + title: 'Finance Cockpit', + category: 'finance', + fee_usdt: '0.15', + fee_atomic: '150000', + mode: 'live' + }, + '/sports-cockpit': { + service_id: 'sports_cockpit', + okx_service_id: 36671, + title: 'Sports Cockpit', + category: 'sports', + fee_usdt: '0.15', + fee_atomic: '150000', + mode: 'live' + }, + '/weather-event-readout': { + service_id: 'weather_event_readout', + okx_service_id: 36675, + title: 'Weather Event Readout', + category: 'weather', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/politics-event-readout': { + service_id: 'politics_event_readout', + okx_service_id: 36676, + title: 'Politics Event Readout', + category: 'politics', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/macro-fed-readout': { + service_id: 'macro_fed_readout', + okx_service_id: 36677, + title: 'Macro Fed Readout', + category: 'macro', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/football-match-card': { + service_id: 'football_match_card', + okx_service_id: 36678, + title: 'Football Match Card', + category: 'sports', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/tennis-match-card': { + service_id: 'tennis_match_card', + okx_service_id: 36679, + title: 'Tennis Match Card', + category: 'sports', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/nba-match-card': { + service_id: 'nba_match_card', + okx_service_id: 36680, + title: 'NBA Match Card', + category: 'sports', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-decision-card': { + service_id: 'pm_decision_card', + okx_service_id: 36681, + title: 'PM Decision Card', + category: 'finance', + fee_usdt: '0.15', + fee_atomic: '150000', + mode: 'live' + }, + // Code-ready; NOT on OKX until Leo authorize create/activate + '/pm-market-scan': { + service_id: 'pm_market_scan', + okx_service_id: 37171, + title: '市场扫描 / PM Market Scan', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-market-health': { + service_id: 'pm_market_health', + okx_service_id: 37172, + title: '盘口健康 / PM Market Health', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + }, + '/pm-wallet-report': { + service_id: 'pm_wallet_report', + okx_service_id: 37173, + title: '钱包一页纸 / PM Wallet Report', + category: 'finance', + fee_usdt: '0.15', + fee_atomic: '150000', + mode: 'live' + }, + '/pm-updown-readout': { + service_id: 'pm_updown_readout', + okx_service_id: 37174, + title: '涨跌盘读出 / PM Up/Down Readout', + category: 'finance', + fee_usdt: '0.1', + fee_atomic: '100000', + mode: 'live' + } +}; + +export function getServiceCatalogEntry(pathname) { + return SERVICE_CATALOG[pathname] ?? null; +} + +export function getFeeAtomicForPath(pathname) { + return getServiceCatalogEntry(pathname)?.fee_atomic ?? '1000000'; +} + +/** OKX onchain listing copy (two lines, no URLs). Keys = live okx_service_id */ +export const OKX_LISTING_COPY = { + 30207: { + serviceName: 'World Cup Smart Money Radar', + serviceDescription: + 'Sports smart-money radar with World Cup preference: scans large public trades; if no active World Cup markets, auto-expands to football and returns live signals with scope_expanded.\n' + + '世界杯优先的聪明钱雷达:无活跃世界杯盘时自动扩展到足球并返回 live 信号(scope_expanded)。输入 market/all + limit 1-10。' + }, + 30208: { + serviceName: 'Polymarket Smart Money Radar', + serviceDescription: + 'Heuristic Polymarket wallet signals from recent large trades; topic search with limited coverage per call.\n' + + 'Polymarket 全市场聪明钱雷达。输入 market/topic 关键词(如 bitcoin 或 all)+ limit 1-10;数据信号,非投资建议。' + }, + 30209: { + serviceName: 'Agent Delivery Audit Gate', + serviceDescription: + 'Rule-based audit of agent task delivery vs goals and evidence; returns pass, needs review, or fail.\n' + + 'Agent 交付验收闸门:对照任务目标与证据,输出 pass/需复核/fail。输入 task、delivery_summary、artifacts、validation。' + }, + 30210: { + serviceName: 'Event Price Divergence Radar', + serviceDescription: + 'Flags where 24h prediction-market probability moves diverge from 24h OKX spot momentum on major crypto assets.\n' + + '事件概率与币价背离雷达:PM 24h 概率变动 vs OKX 现货 24h 动量。输入 asset(bitcoin/ethereum/solana 或省略查主流)。' + }, + 30211: { + serviceName: 'Crypto Market Regime Radar', + serviceDescription: + 'Blends OKX spot momentum, perp funding/premium and Polymarket drift into risk_on/off/neutral/mixed with explainable score.\n' + + '加密市场状态雷达:现货动量+资金费率+PM 情绪 → risk_on/off/neutral 及 0-100 分。输入 focus/asset 关键词 + limit。' + }, + 30212: { + serviceName: 'World Cup Upset Alert', + serviceDescription: + 'Upset alert with World Cup preference: flags profitable wallets on low-probability sides; if no active World Cup markets, auto-expands to football with scope_expanded.\n' + + '世界杯优先的冷门预警:无活跃世界杯盘时自动扩展到足球(scope_expanded)。输入 market/all + limit;非投注建议。' + }, + 30213: { + serviceName: 'Token DD Verdict', + serviceDescription: + 'Rule-based token research gate with DEX liquidity/volume heuristics for EVM contracts; returns avoid/watch/research buckets.\n' + + '代币尽调闸门:规则引擎输出 avoid/观望/可研究等分桶;EVM 合约查 DEX 流动性与活跃度。输入 asset(ticker 或合约地址)。' + }, + 30214: { + serviceName: 'PM Trade Preflight', + serviceDescription: + 'Read-only eligible/watch/skip gate before a Polymarket order; checks liquidity, price zone, spread, and decision-card lite fields. eligible ≠ buy tip.\n' + + '预测市场下单前检查:eligible/观望/跳过,只读不下单。输入 market_url 或 slug + side(yes/no),可选 size_usd。' + }, + 30215: { + serviceName: 'PM Event Readout', + serviceDescription: + 'Event evidence card: same-event matrix + football/tennis/NBA/politics/weather/macro-Fed/Musk plugins (fixture/hard_veto where applicable). Not a buy tip.\n' + + '预测市场事件解读卡:同场矩阵;足球/网球/NBA/政治/天气/美联储/Musk 等品类插件(含赛程/硬闸)。输入 market_url 或 slug;不下单。' + }, + 30216: { + serviceName: 'Content Verify Claims', + serviceDescription: + 'Rule-based check that publish claims overlap caller-supplied source excerpts; pass, needs_review, or fail.\n' + + '发布前断言核查:对照你提供的原文摘录核对数字/关键词。输入 claims[] + sources[].text;不抓网页。' + }, + 36661: { + serviceName: 'Sports Smart Money Radar', + serviceDescription: + 'Heuristic sports prediction-market wallet signals (football leagues, tennis, NBA, NFL, UFC, MLB, etc.); data only.\n' + + '体育预测市场聪明钱雷达:不绑死世界杯。输入 sport + 可选 league/query/tag_slug + limit。' + }, + 36662: { + serviceName: 'Sports Upset Alert', + serviceDescription: + 'Flags profitable wallets entering low-probability sports outcomes; optional max_prob threshold; data only.\n' + + '体育冷门预警:盈利钱包买低概率侧。输入 sport/league/query + 可选 max_prob(0.05-0.5);非投注建议。' + }, + 36663: { + serviceName: 'PM Profile', + serviceDescription: + 'Read-only Polymarket wallet snapshot: 7d leaderboard PnL + open positions sample. From public APIs / polymarket-toolkit lineage.\n' + + 'Polymarket 钱包画像:7日榜 PnL + 持仓抽样。输入 address 或 username;只读不下单。' + }, + 36664: { + serviceName: 'Content Slop Check', + serviceDescription: + 'Rule-based AI-slop / filler detection for draft text before publish; returns slop_score and flags. Not a rewrite service.\n' + + '发布前注水/AI 废话检测:输出 slop_score 与旗帜。输入 text;不改写、不发帖。' + }, + 36666: { + serviceName: 'Agent Budget Preflight', + serviceDescription: + 'Deterministic spend gate before an agent pays for an API/x402 call: buy / skip_sufficient / reject with reasons. No wallet, no settle.\n' + + 'Agent 付费调用前预算闸门:输出 buy/跳过/拒绝及原因。输入 budget_cap_usdt + offer.price_usdt;不签名、不结算。' + }, + 36668: { + serviceName: 'PM Brier', + serviceDescription: + 'Read-only Polymarket calibration score from settled positions (Brier); good/moderate/poor rating. From polymarket-toolkit lineage.\n' + + 'Polymarket 校准分(Brier):已结算持仓抽样。输入 address 或 username;只读不下单。' + }, + 36669: { + serviceName: 'Publish Readiness', + serviceDescription: + 'Pre-publish gate combining slop detection + claim/source overlap; returns ready / edit_first / block with Chinese buyer summary.\n' + + '发布就绪闸门:注水检测 + 断言核查 → ready/先改/别发。输入 text + 可选 claims[]/sources[];不改写、不发帖。' + }, + 36670: { + serviceName: 'Finance Cockpit', + serviceDescription: + 'Composed crypto co-pilot card: market regime score + event-price divergence signals in one JSON response. Data only.\n' + + '金融副驾驶组合卡:市场状态分 + 事件概率/现货背离。输入 focus/asset + limit;不下单。' + }, + 36671: { + serviceName: 'Sports Cockpit', + serviceDescription: + 'Composed sports co-pilot card: smart-money signals + upset alerts (+ cross-market wallet cohort) in one JSON response. Data only.\n' + + '体育副驾驶组合卡:聪明钱 + 冷门预警(含跨场钱包)。输入 sport/league/query + 可选 max_prob;非投注建议。' + }, + 36675: { + serviceName: 'Weather Event Readout', + serviceDescription: + 'Live weather temperature-ladder card: query/default discovery, bucket surface, hard_veto_gaps (station/obs/snapshot/ladder) + adjacent-ladder diagnostics; no_active_markets if none. Optional caller weather{}; no station scrape.\n' + + '天气温度阶梯卡:发现活跃盘、桶面、station/实况/快照硬闸、相邻桶诊断;无盘 no_active_markets。可选 weather{};不爬站、不下单。' + }, + 36676: { + serviceName: 'Politics Event Readout', + serviceDescription: + 'Live politics/election ladder card: query discovery or category default to an active market; exclusivity sanity; no_active_markets if none.\n' + + '政治选举盘口卡:query/品类默认发现活跃盘;无盘返回 no_active_markets。输入 query 或 slug;不下单。' + }, + 36677: { + serviceName: 'Macro Fed Readout', + serviceDescription: + 'Live Fed/FOMC rate card: query/default discovery + L1 rate-decision ladder / expected-move heuristic; honest external-anchor gaps; no_active_markets if none.\n' + + '美联储利率宏观卡:发现活跃盘 + L1 利率阶梯/隐含变动;外部锚定缺口如实标注。无盘 no_active_markets。输入 query/slug;不下单。' + }, + 36678: { + serviceName: 'Football Match Card', + serviceDescription: + 'Live football evidence card: discovery + same-event matrix, fixture gate, matrix_completeness/hard_veto_gaps, expression compare; match vs outright_season; no_active_markets if none. Not a buy tip.\n' + + '足球比赛卡:同场矩阵、赛程闸门、完备性硬闸、表达比较;区分单场/赛季 outright。无盘 no_active_markets。输入 query/slug;不下单。' + }, + 36679: { + serviceName: 'Tennis Match Card', + serviceDescription: + 'Live tennis evidence card: format-aware ML/set/totals, fixture gate, matrix_completeness/hard_veto_gaps, domination check; no_active_markets if none. Not a buy tip.\n' + + '网球比赛卡:赛制感知矩阵、赛程闸门、完备性硬闸、直落盘检查。无盘 no_active_markets。输入 query/slug;不下单。' + }, + 36680: { + serviceName: 'NBA Match Card', + serviceDescription: + 'Live NBA evidence card: query/default discovery; moneyline/spread/totals matrix; match vs outright_season filter; no_active_markets if none. Not a buy tip.\n' + + 'NBA 比赛卡:胜负/让分/总分矩阵;区分单场/赛季 outright。无盘 no_active_markets。输入 query/slug;不下单。' + }, + 36681: { + serviceName: 'PM Decision Card', + serviceDescription: + 'Pre-trade decision gate: preflight + optional event context → opportunity_state + skip/watch/eligible_for_manual_review; optional size_usd/bankroll_usd → order_quantity_shares (share-first). Replay before each order. Not a buy tip; no orders.\n' + + '预测市场决策卡:机械检查+事件上下文 → opportunity_state + 跳过/观望/可人工复核;可选 size/bankroll → shares。每次下单前重跑;非买点、不下单。' + }, + 37157: { + serviceName: 'PM PnL Audit', + serviceDescription: + 'Polymarket PnL trust gate: quick LB vs position cashPnL, or mode=full Worker-safe cashflow replay (TRADE/REDEEM/MERGE/SPLIT/REBATE/… ) with honest pagination_incomplete. Data only.\n' + + 'Polymarket PnL 审计:quick 对比排行榜与持仓 cashPnL;mode=full 做现金回流(含分页 incomplete 诚实标记)。输入 address/username;只读不下单。' + }, + 37171: { + serviceName: '市场扫描 / PM Market Scan', + serviceDescription: + 'Read-only Polymarket market scanner: rank active books by 24h volume and spread (toolkit pm scan). Optional query + min_volume + limit. Not a buy tip.\n' + + '市场扫描:按 24h 成交量与价差筛活跃盘(toolkit pm scan)。输入可选 query + min_volume + limit;只读不下单。' + }, + 37172: { + serviceName: '盘口健康 / PM Market Health', + serviceDescription: + 'Read-only book health: spread, depth proxies, yes/no overround for one market or event. Flags wide/thin/incoherent books. Not a buy tip.\n' + + '盘口健康:价差/深度/overround 快照(单盘或同场)。宽价差或 incoherent 会标出。输入 market_url/slug/event_slug;只读不下单。' + }, + 37173: { + serviceName: '钱包一页纸 / PM Wallet Report', + serviceDescription: + 'Composed wallet one-pager: profile + Brier calibration + PnL audit (quick/full) in one JSON. Copy-trust composite; data only.\n' + + '钱包一页纸:画像 + Brier 校准 + PnL 审计(quick/full)合成。输出 composite_action;输入 address/username;只读不下单。' + }, + 37174: { + serviceName: '涨跌盘读出 / PM Up/Down Readout', + serviceDescription: + 'Crypto up/down event surface from Gamma with resolution-source pitfalls (toolkit pm updown). Verify settlement rules before pricing. Not a buy tip.\n' + + '涨跌盘读出:Gamma 涨跌事件面 + 结算源陷阱提示(toolkit pm updown)。先核结算定义再谈价。输入 event_slug 或 query;只读不下单。' + } +}; + +/** Listing copy for services not yet on OKX (use with onchainos create). Keys = service_id */ +export const PENDING_OKX_LISTING_COPY = { + content_slop_check: { + serviceName: 'Content Slop Check', + serviceDescription: + 'Rule-based AI-slop / filler detection for draft text before publish; returns slop_score and flags. Not a rewrite service.\n' + + '发布前注水/AI 废话检测:输出 slop_score 与旗帜。输入 text;不改写、不发帖。' + }, + sports_smart_money_radar: { + serviceName: 'Sports Smart Money Radar', + serviceDescription: + 'Heuristic sports prediction-market wallet signals (football leagues, tennis, NBA, NFL, UFC, MLB, etc.); data only.\n' + + '体育预测市场聪明钱雷达:不绑死世界杯。输入 sport + 可选 league/query/tag_slug + limit。' + }, + sports_upset_alert: { + serviceName: 'Sports Upset Alert', + serviceDescription: + 'Flags profitable wallets entering low-probability sports outcomes across competitions; data only.\n' + + '体育冷门预警:盈利钱包买低概率侧。输入 sport/league/query;非投注建议。' + }, + pm_profile: { + serviceName: 'PM Profile', + serviceDescription: + 'Read-only Polymarket wallet snapshot: 7d leaderboard PnL + open positions sample. From public APIs / polymarket-toolkit lineage.\n' + + 'Polymarket 钱包画像:7日榜 PnL + 持仓抽样。输入 address 或 username;只读不下单。' + }, + pm_pnl_audit: { + serviceName: 'PM PnL Audit', + serviceDescription: + 'Polymarket PnL trust gate: quick LB vs position cashPnL, or mode=full Worker-safe cashflow replay (TRADE/REDEEM/MERGE/SPLIT/REBATE/… ) with honest pagination_incomplete. Data only.\n' + + 'Polymarket PnL 审计:quick 对比排行榜与持仓 cashPnL;mode=full 做现金回流(含分页 incomplete 诚实标记)。输入 address/username;只读不下单。' + }, + agent_budget_preflight: { + serviceName: 'Agent Budget Preflight', + serviceDescription: + 'Deterministic spend gate before an agent pays for an API/x402 call: buy / skip_sufficient / reject with reasons. No wallet, no settle.\n' + + 'Agent 付费调用前预算闸门:输出 buy/跳过/拒绝及原因。输入 budget_cap_usdt + offer.price_usdt;不签名、不结算。' + }, + pm_brier: { + serviceName: 'PM Brier', + serviceDescription: + 'Read-only Polymarket calibration score from settled positions (Brier); good/moderate/poor rating. From polymarket-toolkit lineage.\n' + + 'Polymarket 校准分(Brier):已结算持仓抽样。输入 address 或 username;只读不下单。' + }, + publish_readiness: { + serviceName: 'Publish Readiness', + serviceDescription: + 'Pre-publish gate combining slop detection + claim/source overlap; returns ready / edit_first / block with Chinese buyer summary.\n' + + '发布就绪闸门:注水检测 + 断言核查 → ready/先改/别发。输入 text + 可选 claims[]/sources[];不改写、不发帖。' + }, + finance_cockpit: { + serviceName: 'Finance Cockpit', + serviceDescription: + 'Composed crypto co-pilot card: market regime score + event-price divergence signals in one JSON response. Data only.\n' + + '金融副驾驶组合卡:市场状态分 + 事件概率/现货背离。输入 focus/asset + limit;不下单。' + }, + sports_cockpit: { + serviceName: 'Sports Cockpit', + serviceDescription: + 'Composed sports co-pilot card: smart-money signals + upset alerts (+ cross-market wallet cohort) in one JSON response. Data only.\n' + + '体育副驾驶组合卡:聪明钱 + 冷门预警(含跨场钱包)。输入 sport/league/query + 可选 max_prob;非投注建议。' + }, + weather_event_readout: { + serviceName: 'Weather Event Readout', + serviceDescription: + 'Live weather temperature-ladder card: query/default discovery, bucket surface, hard_veto_gaps (station/obs/snapshot/ladder) + adjacent-ladder diagnostics; no_active_markets if none. Optional caller weather{}; no station scrape.\n' + + '天气温度阶梯卡:发现活跃盘、桶面、station/实况/快照硬闸、相邻桶诊断;无盘 no_active_markets。可选 weather{};不爬站、不下单。' + }, + politics_event_readout: { + serviceName: 'Politics Event Readout', + serviceDescription: + 'Live politics/election ladder card: query discovery or category default; exclusivity sanity; no_active_markets if none.\n' + + '政治选举盘口卡:query/品类默认发现活跃盘;无盘返回 no_active_markets。输入 query 或 slug;不下单。' + }, + macro_fed_readout: { + serviceName: 'Macro Fed Readout', + serviceDescription: + 'Live Fed/FOMC rate card: query/default discovery + L1 rate-decision ladder / expected-move heuristic; honest external-anchor gaps; no_active_markets if none.\n' + + '美联储利率宏观卡:发现活跃盘 + L1 利率阶梯/隐含变动;外部锚定缺口如实标注。无盘 no_active_markets。输入 query/slug;不下单。' + }, + football_match_card: { + serviceName: 'Football Match Card', + serviceDescription: + 'Live football evidence card: discovery + same-event matrix, fixture gate, matrix_completeness/hard_veto_gaps, expression compare; match vs outright_season; no_active_markets if none. Not a buy tip.\n' + + '足球比赛卡:同场矩阵、赛程闸门、完备性硬闸、表达比较;区分单场/赛季 outright。无盘 no_active_markets。输入 query/slug;不下单。' + }, + tennis_match_card: { + serviceName: 'Tennis Match Card', + serviceDescription: + 'Live tennis evidence card: format-aware ML/set/totals, fixture gate, matrix_completeness/hard_veto_gaps, domination check; no_active_markets if none. Not a buy tip.\n' + + '网球比赛卡:赛制感知矩阵、赛程闸门、完备性硬闸、直落盘检查。无盘 no_active_markets。输入 query/slug;不下单。' + }, + nba_match_card: { + serviceName: 'NBA Match Card', + serviceDescription: + 'Live NBA evidence card: query/default discovery; moneyline/spread/totals matrix; match vs outright_season filter; no_active_markets if none. Not a buy tip.\n' + + 'NBA 比赛卡:胜负/让分/总分矩阵;区分单场/赛季 outright。无盘 no_active_markets。输入 query/slug;不下单。' + }, + pm_decision_card: { + serviceName: 'PM Decision Card', + serviceDescription: + 'Pre-trade decision gate: preflight + optional event context → opportunity_state + skip/watch/eligible_for_manual_review; optional size_usd/bankroll_usd → order_quantity_shares (share-first). Replay before each order. Not a buy tip; no orders.\n' + + '预测市场决策卡:机械检查+事件上下文 → opportunity_state + 跳过/观望/可人工复核;可选 size/bankroll → shares。每次下单前重跑;非买点、不下单。' + }, + pm_market_scan: OKX_LISTING_COPY[37171], + pm_market_health: OKX_LISTING_COPY[37172], + pm_wallet_report: OKX_LISTING_COPY[37173], + pm_updown_readout: OKX_LISTING_COPY[37174], + crypto_market_regime_radar: OKX_LISTING_COPY[30211], + world_cup_upset_alert: OKX_LISTING_COPY[30212], + token_dd_verdict: OKX_LISTING_COPY[30213], + pm_trade_preflight: OKX_LISTING_COPY[30214], + pm_event_readout: OKX_LISTING_COPY[30215], + content_verify_claims: OKX_LISTING_COPY[30216] +}; + +/** Map catalog path → pending copy entry */ +export function pendingListingCopyForPath(pathname) { + const entry = getServiceCatalogEntry(pathname); + if (!entry?.service_id) return null; + return PENDING_OKX_LISTING_COPY[entry.service_id] ?? null; +} + +export function okxListingFeeForServiceId(serviceId) { + const entry = Object.values(SERVICE_CATALOG).find((s) => s.okx_service_id === serviceId); + return entry?.fee_usdt ?? '0.1'; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/trial.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/trial.mjs new file mode 100644 index 00000000..59b05840 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/trial.mjs @@ -0,0 +1,55 @@ +const TRIAL_TTL_SECONDS = 90 * 24 * 60 * 60; // 90 days per client+service + +/** + * One free POST per client IP per service path, tracked in TRIAL_KV when bound. + * Falls back to an in-isolate Map (best-effort) for local tests. + * + * Default OFF. OKX.AI listing x402-check / review probes treat any unpaid HTTP 200 + * as "not a valid x402 service". Opt in only with X402_FREE_TRIAL=true after listing. + */ +const memoryTrials = new Map(); + +/** Free trial is opt-in. Anything except the string "true" keeps unpaid POSTs on 402. */ +export function isFreeTrialEnabled(env) { + return env?.X402_FREE_TRIAL === 'true'; +} + +export async function trialKeyForRequest(request, pathname) { + const ip = request.headers.get('cf-connecting-ip') + || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + || 'unknown'; + const raw = `${ip}|${pathname}`; + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw)); + const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); + return `trial:v1:${pathname}:${hex.slice(0, 32)}`; +} + +export async function hasUsedFreeTrial(env, request, pathname) { + const key = await trialKeyForRequest(request, pathname); + if (env?.TRIAL_KV) { + const value = await env.TRIAL_KV.get(key); + return value === '1'; + } + return memoryTrials.has(key); +} + +export async function markFreeTrialUsed(env, request, pathname) { + const key = await trialKeyForRequest(request, pathname); + if (env?.TRIAL_KV) { + await env.TRIAL_KV.put(key, '1', { expirationTtl: TRIAL_TTL_SECONDS }); + return; + } + memoryTrials.set(key, Date.now()); +} + +export function withTrialBilling(payload, { pathname, fee_usdt }) { + return { + ...payload, + billing: { + mode: 'free_trial', + service_path: pathname, + list_price_usdt: fee_usdt, + message: 'One-time free trial for this service. Subsequent calls require x402 payment at the listed fee.' + } + }; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/x402.mjs b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/x402.mjs new file mode 100644 index 00000000..8eccc1fb --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/worker/x402.mjs @@ -0,0 +1,510 @@ +// OKX x402 paywall for the acceptance-gate worker (plain module worker, no hono). +// +// Protocol/source-of-truth notes (verified against @okxweb3/x402-core@0.1.0 and +// @okxweb3/x402-evm@0.2.1 dist sources plus OKX seller-SDK docs): +// - x402 v2 challenge: HTTP 402 + `PAYMENT-REQUIRED` header = base64(JSON of +// { x402Version: 2, error, resource, accepts: [paymentRequirements] }). +// We additionally mirror the same JSON into the response body for humans. +// - Buyer payment arrives base64-encoded in `PAYMENT` (official buyer SDK), +// `PAYMENT-SIGNATURE` (x402-core server default) or `X-PAYMENT` — checked +// in that order. +// - Facilitator = OKX hosted, HMAC-SHA256 signed (OKXFacilitatorClient): +// POST https://web3.okx.com/api/v6/pay/x402/verify +// POST https://web3.okx.com/api/v6/pay/x402/settle (body carries syncSettle) +// GET https://web3.okx.com/api/v6/pay/x402/supported +// GET https://web3.okx.com/api/v6/pay/x402/settle/status?txHash=... +// prehash = timestamp + METHOD + path(+query) + body, timestamp = ISO-8601, +// headers OK-ACCESS-KEY / OK-ACCESS-SIGN / OK-ACCESS-TIMESTAMP / OK-ACCESS-PASSPHRASE. +// Responses use the OKX { code, msg, data } envelope; code !== "0" is a +// business error even on HTTP 200. +// - Settlement semantics (syncSettle=true): only `status: "success"` means +// funds confirmed on-chain. `pending` = broadcast but NOT confirmed and +// `timeout` = confirmation timed out — in both cases we keep polling +// GET settle/status and never deliver data until it reports success. +// - Settlement result is surfaced to the buyer via `PAYMENT-RESPONSE` header +// = base64(JSON settle response). +// +// Uses WebCrypto (crypto.subtle) for HMAC so no nodejs_compat flag is required. + +const FACILITATOR_BASE_URL = 'https://web3.okx.com'; +const X402_VERSION = 2; + +// X Layer mainnet + OKX default stablecoin USDT0 (EIP-3009, 6 decimals). +// Address/name/version copied from @okxweb3/x402-evm DEFAULT_STABLECOINS['eip155:196']. +export const X402_NETWORK = 'eip155:196'; +export const X402_SCHEME = 'exact'; +export const X402_PAY_TO = '0x1e1a2f7ac1bc6df29a1878c3f26b17dccdc16e15'; +export const X402_ASSET = { + address: '0x779ded0c9e1022225f8e0630b35a9b54be713736', + name: 'USD₮0', + version: '1', + decimals: 6 +}; +// Default price when no per-route override (legacy 1 USDT). +export const X402_PRICE_ATOMIC = '1000000'; +const MAX_TIMEOUT_SECONDS = 300; + +// Extra CORS surface needed only when the paywall is active. Kept out of the +// worker's base headers so X402_ENABLED=false responses stay byte-identical +// with the pre-paywall deployment. +export const X402_CORS_HEADERS = { + 'access-control-allow-headers': 'content-type, payment, payment-signature, x-payment', + 'access-control-expose-headers': 'payment-required, payment-response' +}; + +// Settlement polling budget. syncSettle usually confirms in-band; when it +// returns pending/timeout we keep polling settle/status for up to ~20s more +// (well inside Workers request limits). Tunable via env for tests. +const DEFAULT_SETTLE_POLL_BUDGET_MS = 20_000; +const DEFAULT_SETTLE_POLL_INTERVAL_MS = 1_000; + +// Best-effort receipt store so a buyer whose settlement came back +// pending/timeout can replay the SAME payment header and collect the data once +// the tx confirms, without being charged twice (the EIP-3009 nonce is burned, +// so a second settle would fail anyway). +// LIMITATION: this Map is per-isolate. Cloudflare may route the retry to a +// different isolate or recycle this one, in which case we fall back to the +// normal verify path (which will reject the burned nonce). Durable receipts +// would need KV/DO — out of scope for this stage. +const RECEIPT_TTL_MS = 10 * 60 * 1000; +const receiptCache = new Map(); // paymentHash -> { status, txHash, settleResponse, storedAt } + +/** Whether the paywall is switched on. Anything except the string "true" keeps + * the worker in its historical free mode (safe during marketplace review). */ +export function isX402Enabled(env) { + return env?.X402_ENABLED === 'true'; +} + +/** Payment requirements advertised for a paid endpoint. */ +export function buildPaymentRequirements({ amountAtomic = X402_PRICE_ATOMIC } = {}) { + return { + scheme: X402_SCHEME, + network: X402_NETWORK, + amount: amountAtomic, + asset: X402_ASSET.address, + payTo: X402_PAY_TO, + maxTimeoutSeconds: MAX_TIMEOUT_SECONDS, + // EIP-712 domain for the EIP-3009 transferWithAuthorization signature. + extra: { name: X402_ASSET.name, version: X402_ASSET.version } + }; +} + +function buildPaymentRequired({ resourceUrl, description, error, amountAtomic }) { + const paymentRequired = { + x402Version: X402_VERSION, + resource: { + url: resourceUrl, + description: description || '', + mimeType: 'application/json' + }, + accepts: [buildPaymentRequirements({ amountAtomic })] + }; + if (error) paymentRequired.error = error; + return paymentRequired; +} + +/** + * Gate a paid endpoint behind the OKX x402 flow. + * + * @param request incoming Request (body untouched unless payment verifies) + * @param env worker env (OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE) + * @param options { resourceUrl, description, deliver, respond } + * deliver: async () => payload object (only invoked after verify passes; + * responsible for parsing the request body itself) + * respond: (payload, status, extraHeaders) => Response + * @returns Response + */ +export async function handlePaidRequest(request, env, { resourceUrl, description, deliver, respond, priceAtomic = X402_PRICE_ATOMIC }) { + const challenge = (error) => { + const paymentRequired = buildPaymentRequired({ resourceUrl, description, error, amountAtomic: priceAtomic }); + return respond(paymentRequired, 402, { + 'PAYMENT-REQUIRED': base64EncodeUtf8(JSON.stringify(paymentRequired)) + }); + }; + + // Challenge is issued before any body parsing: unpaid probes cost nothing. + const rawHeader = request.headers.get('payment') + || request.headers.get('payment-signature') + || request.headers.get('x-payment'); + if (!rawHeader) { + return challenge('Payment required'); + } + + let paymentPayload; + try { + paymentPayload = JSON.parse(base64DecodeUtf8(rawHeader)); + } catch { + return challenge('Invalid payment header: expected base64-encoded JSON payment payload'); + } + if (!paymentPayload || paymentPayload.x402Version !== X402_VERSION || !paymentPayload.accepted) { + return challenge('Invalid payment payload: x402Version must be 2 with an accepted requirements object'); + } + + // Reject payments signed for a different resource (cross-endpoint reuse). + if (paymentPayload.resource?.url && paymentPayload.resource.url !== resourceUrl) { + return challenge('Payment resource mismatch: payment was created for a different resource URL'); + } + + const requirements = buildPaymentRequirements({ amountAtomic: priceAtomic }); + if (!requirementsMatch(requirements, paymentPayload.accepted)) { + return challenge('No matching payment requirements found'); + } + + // Replay path: same payment header seen before in this isolate. + const receiptKey = await sha256Hex(canonicalJson(paymentPayload)); + pruneReceipts(); + const receipt = receiptCache.get(receiptKey); + if (receipt?.status === 'success') { + // Already settled — deliver without contacting the facilitator again. + return deliverPayload(deliver, respond, receipt.settleResponse); + } + if (receipt?.status === 'pending' && receipt.txHash) { + return resolvePendingReceipt(env, receiptKey, receipt, { deliver, respond, requirements }); + } + + // 1) verify with the OKX hosted facilitator + let verifyResult; + try { + verifyResult = await facilitatorRequest(env, 'POST', '/api/v6/pay/x402/verify', { + x402Version: X402_VERSION, + paymentPayload, + paymentRequirements: requirements + }); + } catch (error) { + return facilitatorErrorResponse(respond, 'verify', error); + } + if (!verifyResult || verifyResult.isValid !== true) { + return challenge(verifyResult?.invalidReason || 'Payment verification failed'); + } + + // 2) produce the deliverable before settling so a data failure never burns a payment + let payload; + try { + payload = await deliver(); + } catch (error) { + return respond({ + error: 'internal_error', + message: error instanceof Error ? error.message : String(error) + }, 500); + } + + // 3) settle synchronously (syncSettle) — only hand over data once funds confirmed + let settleResult; + try { + settleResult = await facilitatorRequest(env, 'POST', '/api/v6/pay/x402/settle', { + x402Version: X402_VERSION, + paymentPayload, + paymentRequirements: requirements, + syncSettle: true + }); + } catch (error) { + return facilitatorErrorResponse(respond, 'settle', error); + } + + const outcome = await resolveSettlement(env, settleResult); + + if (outcome.state === 'settled') { + receiptCache.set(receiptKey, { + status: 'success', + txHash: outcome.settleResponse.transaction, + settleResponse: outcome.settleResponse, + storedAt: Date.now() + }); + return respond(payload, 200, paymentResponseHeader(outcome.settleResponse)); + } + + if (outcome.state === 'pending') { + receiptCache.set(receiptKey, { + status: 'pending', + txHash: outcome.settleResponse.transaction, + settleResponse: outcome.settleResponse, + storedAt: Date.now() + }); + return pendingTxResponse(respond, outcome.settleResponse); + } + + // Hard settlement failure (no confirmed tx): re-challenge. + const paymentRequired = buildPaymentRequired({ + resourceUrl, + description, + error: settleResult?.errorReason || 'settlement_failed', + amountAtomic: priceAtomic + }); + return respond(paymentRequired, 402, { + ...paymentResponseHeader(settleResult ?? {}), + 'PAYMENT-REQUIRED': base64EncodeUtf8(JSON.stringify(paymentRequired)) + }); +} + +/** + * Interpret a settle response under OKX syncSettle semantics and, when the tx + * is broadcast but unconfirmed (pending/timeout), poll settle/status until it + * confirms or the poll budget runs out. + * + * @returns { state: 'settled'|'pending'|'failed', settleResponse } + */ +async function resolveSettlement(env, settleResult) { + if (!settleResult) return { state: 'failed', settleResponse: {} }; + + // Only an explicit on-chain confirmation counts as settled. A bare + // success:true without the OKX status extension is also final per + // x402-core settleResponseSchema (status is optional). + if (settleResult.success === true + && (settleResult.status === undefined || settleResult.status === 'success')) { + return { state: 'settled', settleResponse: settleResult }; + } + + // pending = broadcast, unconfirmed; timeout = confirmation timed out. + // Both leave a real tx in flight: poll instead of failing or delivering. + if ((settleResult.status === 'pending' || settleResult.status === 'timeout') && settleResult.transaction) { + const polled = await pollSettleStatus(env, settleResult.transaction); + if (polled === 'success') { + return { + state: 'settled', + settleResponse: { ...settleResult, success: true, status: 'success' } + }; + } + if (polled === 'failed') { + return { state: 'failed', settleResponse: settleResult }; + } + return { + state: 'pending', + settleResponse: { ...settleResult, success: false, status: 'pending' } + }; + } + + return { state: 'failed', settleResponse: settleResult }; +} + +/** Buyer replays a payment whose tx was still confirming. Poll status again: + * confirmed → deliver (no second settle, nonce already spent); still pending → + * another 402 pending notice; failed on-chain → clear receipt, ask to re-pay. */ +async function resolvePendingReceipt(env, receiptKey, receipt, { deliver, respond, requirements }) { + const polled = await pollSettleStatus(env, receipt.txHash); + if (polled === 'success') { + const settleResponse = { ...receipt.settleResponse, success: true, status: 'success' }; + receiptCache.set(receiptKey, { ...receipt, status: 'success', settleResponse, storedAt: Date.now() }); + return deliverPayload(deliver, respond, settleResponse); + } + if (polled === 'failed') { + receiptCache.delete(receiptKey); + return respond({ + error: 'settlement_failed', + message: 'The on-chain transaction for this payment failed. A new payment is required.', + transaction: receipt.txHash, + network: requirements.network + }, 402, paymentResponseHeader({ ...receipt.settleResponse, success: false })); + } + return pendingTxResponse(respond, receipt.settleResponse); +} + +async function deliverPayload(deliver, respond, settleResponse) { + let payload; + try { + payload = await deliver(); + } catch (error) { + return respond({ + error: 'internal_error', + message: error instanceof Error ? error.message : String(error) + }, 500); + } + return respond(payload, 200, paymentResponseHeader(settleResponse)); +} + +function pendingTxResponse(respond, settleResponse) { + return respond({ + error: 'settlement_pending', + status: 'pending_tx', + message: 'Payment transaction is broadcast but not yet confirmed on-chain. Data is withheld until confirmation.', + transaction: settleResponse.transaction, + network: settleResponse.network || X402_NETWORK, + retry: { + retry_with_same_payment: true, + hint: 'Re-send the exact same request with the identical PAYMENT header. The service will check on-chain status and deliver the data without charging again.' + } + }, 402, paymentResponseHeader(settleResponse)); +} + +function paymentResponseHeader(settleResponse) { + return { 'PAYMENT-RESPONSE': base64EncodeUtf8(JSON.stringify(settleResponse ?? {})) }; +} + +/** Same matching semantics as x402ResourceServer.findMatchingRequirements: + * base fields must deep-equal; every server `extra` key must match buyer's. */ +function requirementsMatch(serverReq, buyerAccepted) { + const { extra: serverExtra, ...serverBase } = serverReq; + const { extra: buyerExtra, ...buyerBase } = buyerAccepted ?? {}; + if (!deepEqual(serverBase, buyerBase)) return false; + if (!serverExtra && !buyerExtra) return true; + if (!serverExtra) return true; + if (!buyerExtra) return false; + for (const [key, value] of Object.entries(serverExtra)) { + if (!deepEqual(buyerExtra[key], value)) return false; + } + return true; +} + +function deepEqual(a, b) { + return canonicalJson(a) === canonicalJson(b); +} + +function canonicalJson(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) { + return `[${value.map((item) => canonicalJson(item)).join(',')}]`; + } + const keys = Object.keys(value).sort(); + return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`; +} + +class FacilitatorError extends Error { + constructor(kind, detail) { + super(kind); + this.name = 'FacilitatorError'; + this.kind = kind; // generic machine-readable code, safe to expose + this.detail = detail; // full detail, logs only — never sent to clients + } +} + +function facilitatorErrorResponse(respond, operation, error) { + // Log full detail server-side; respond with generic codes only (no OKX body + // excerpts or upstream internals leak to buyers). + console.warn(`[x402] facilitator ${operation} error:`, error?.detail || error?.message || error); + return respond({ + error: 'facilitator_error', + operation, + code: error instanceof FacilitatorError ? error.kind : 'facilitator_unreachable' + }, 502); +} + +/** + * Signed request to the OKX hosted facilitator. + * Mirrors OKXFacilitatorClient: prehash = timestamp + method + path + body, + * HMAC-SHA256(secret) base64. `path` must include the query string when present. + * Unwraps the OKX { code, msg, data } envelope and treats code !== "0" as a + * business error even when HTTP status is 200. + */ +export async function facilitatorRequest(env, method, path, bodyObj) { + const apiKey = env?.OKX_API_KEY; + const secretKey = env?.OKX_SECRET_KEY; + const passphrase = env?.OKX_PASSPHRASE; + if (!apiKey || !secretKey || !passphrase) { + throw new FacilitatorError('credentials_missing', + 'OKX facilitator credentials are not configured (OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE)'); + } + + const body = bodyObj === undefined || bodyObj === null ? '' : JSON.stringify(bodyObj); + const timestamp = new Date().toISOString(); + const sign = await hmacSha256Base64(secretKey, timestamp + method + path + body); + + const response = await fetch(FACILITATOR_BASE_URL + path, { + method, + headers: { + 'OK-ACCESS-KEY': apiKey, + 'OK-ACCESS-SIGN': sign, + 'OK-ACCESS-TIMESTAMP': timestamp, + 'OK-ACCESS-PASSPHRASE': passphrase, + 'Content-Type': 'application/json' + }, + ...(body ? { body } : {}) + }); + + const text = await response.text(); + const endpoint = `${method} ${path.split('?')[0]}`; + if (!response.ok) { + throw new FacilitatorError(`okx_http_${response.status}`, + `OKX facilitator ${endpoint} failed (${response.status}): ${excerpt(text)}`); + } + let json; + try { + json = JSON.parse(text); + } catch { + throw new FacilitatorError('okx_invalid_json', + `OKX facilitator ${endpoint} returned invalid JSON: ${excerpt(text)}`); + } + if (json && typeof json === 'object' && 'code' in json) { + if (String(json.code) !== '0') { + throw new FacilitatorError(`okx_business_${json.code}`, + `OKX facilitator ${endpoint} business error code=${json.code}: ${excerpt(json.msg ?? '')}`); + } + return json.data; + } + return json; +} + +/** Poll GET settle/status until success/failed or the poll budget is spent. + * @returns 'success' | 'failed' | 'pending' */ +async function pollSettleStatus(env, txHash) { + const budgetMs = positiveInt(env?.X402_SETTLE_POLL_BUDGET_MS, DEFAULT_SETTLE_POLL_BUDGET_MS); + const intervalMs = positiveInt(env?.X402_SETTLE_POLL_INTERVAL_MS, DEFAULT_SETTLE_POLL_INTERVAL_MS); + const deadline = Date.now() + budgetMs; + for (;;) { + try { + const status = await facilitatorRequest( + env, + 'GET', + `/api/v6/pay/x402/settle/status?txHash=${encodeURIComponent(txHash)}` + ); + if (status?.success === false) return 'failed'; + if (status?.status === 'success') return 'success'; + } catch (error) { + // transient facilitator error — keep polling until the deadline + console.warn('[x402] settle/status poll error:', error?.detail || error?.message || error); + } + const remaining = deadline - Date.now(); + if (remaining <= 0) return 'pending'; + await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining))); + } +} + +function positiveInt(value, fallback) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function pruneReceipts() { + const now = Date.now(); + for (const [key, receipt] of receiptCache) { + if (now - receipt.storedAt > RECEIPT_TTL_MS) receiptCache.delete(key); + } +} + +async function sha256Hex(text) { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)); + return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +async function hmacSha256Base64(secret, message) { + const key = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); + return base64FromBytes(new Uint8Array(signature)); +} + +function base64FromBytes(bytes) { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary); +} + +function base64EncodeUtf8(text) { + return base64FromBytes(new TextEncoder().encode(text)); +} + +function base64DecodeUtf8(base64) { + const binary = atob(base64.trim()); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return new TextDecoder('utf-8').decode(bytes); +} + +function excerpt(text, limit = 200) { + const compact = String(text ?? '').trim().replace(/\s+/g, ' '); + if (!compact) return ''; + return compact.length <= limit ? compact : `${compact.slice(0, limit - 3)}...`; +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/wrangler.toml b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/wrangler.toml new file mode 100644 index 00000000..be738561 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/wrangler.toml @@ -0,0 +1,15 @@ +name = "agent-acceptance-gate-api" +main = "worker/index.mjs" +compatibility_date = "2024-11-01" + +routes = [ + { pattern = "api.leolabs.me", custom_domain = true } +] + +[[kv_namespaces]] +binding = "TRIAL_KV" +id = "68a475d8f7f345d3887b571a12d44e55" + +# PaymentJournalDurableObject was removed from this Worker historically. A +# deleted_classes migration for a class that no longer exists on the remote +# script fails closed under Wrangler 4.133.0, so do not redeclare it. diff --git "a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/\344\270\255\346\226\207\350\257\264\346\230\216.md" "b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/\344\270\255\346\226\207\350\257\264\346\230\216.md" new file mode 100644 index 00000000..6e6d3514 --- /dev/null +++ "b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/source/\344\270\255\346\226\207\350\257\264\346\230\216.md" @@ -0,0 +1,496 @@ +# OKX.AI ASP 产品包中文说明:Agent Deliverable Auditor + +创建时间:2026-07-02 +状态:本地产品包 / 未上线 / 未触发 OKX 账号、钱包、部署、上架动作 + +## 一句话结论 + +`Agent Deliverable Auditor` 是一个面向 OKX.AI Agent 市场的候选 A2MCP 服务: + +> 帮买家审计 AI Agent 的交付结果,判断这次任务是否可以接受、是否需要补证据、是否存在 hard gate 风险。 + +它不是交易工具,不碰钱包,不做投资建议,也不替用户部署或发布。它只读取 Agent 的任务说明、writeback、文件变更、验证结果、rollback、hard gates 和 next gate,然后输出结构化审计结果。 + +## 为什么这个方向可能能赚钱 + +OKX.AI 的核心是 Agent 经济市场:用户发任务,ASP 提供服务,Evaluator 处理争议。 + +这个市场天然会出现一个问题: + +```text +一个 Agent 说“我做完了”,买家怎么判断这活到底能不能收? +``` + +现在的机会不是写一篇观点,而是做一个可重复收费的小服务: + +- 买家付款前用它检查交付包; +- ASP 发货前用它自检; +- Evaluator 仲裁前用它整理事实; +- Agent 团队用它统一验收标准。 + +这比泛泛地说“AI workflow consulting”更窄,也更容易按次收费。 + +## 推荐产品形态 + +优先做 A2MCP,不优先做 A2A。 + +### A2MCP 的好处 + +- 输入输出固定; +- 可以按次收费; +- 不需要每单重新谈 scope; +- 适合做 API / MCP endpoint; +- 样例可以本地验证; +- 后续可以接 OKX Payment SDK 或 402 付费网关。 + +### 不先做 A2A 的原因 + +A2A 更像定制服务,会带来: + +- 需求沟通; +- 交付边界确认; +- 人工 QA; +- 争议处理; +- 难以自动化报价。 + +现在还没验证需求,不适合一上来做重服务。 + +### 不先做 Evaluator 的原因 + +Evaluator 需要至少 100 OKB stake,并且有 slashing / timeout 风险。现在更合理的路径是: + +```text +先做服务给 Evaluator / 买家 / ASP 用,而不是自己先成为 Evaluator。 +``` + +## 产品要审计什么 + +这个服务主要审计 5 件事。 + +### 1. 交付是否完整 + +检查: + +- 有没有明确 artifact; +- 改了哪些文件; +- 有没有说明任务 scope; +- 有没有 rollback; +- 有没有 next gate。 + +### 2. 验证是否可信 + +检查: + +- 是否列出验证命令或检查项; +- 是否给出实际结果,而不是只说“已验证”; +- 哪些验证被 deferred; +- 证据路径是否具体; +- source / generated / state 文件是否分清。 + +### 3. 是否跨越 hard gate + +重点检查: + +- 有没有未经批准 commit / push / deploy / public publish; +- 有没有动 credentials / API key / OAuth / proxy; +- 有没有钱包、签名、转账、staking; +- 有没有破坏性 cleanup; +- 有没有绕过 active writer / pathspec / repo lock。 + +### 4. 买家是否能直接决策 + +输出必须让买家知道: + +- 可以接受; +- 需要补证据; +- 不能接受; +- 下一步要谁批准、批准什么。 + +### 5. 是否适合争议场景 + +如果进入 dispute,输出要能帮助 Evaluator: + +- 分清事实和判断; +- 列出缺失证据; +- 提供可复查路径; +- 说明为什么 pass / needs_review / fail。 + +## API / MCP 输入输出草案 + +详细 schema 在: + +[service-spec.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/service-spec.md) + +简化输入: + +```json +{ + "task": { + "buyer_goal": "买家希望 Agent 完成什么", + "allowed_actions": ["允许做什么"], + "forbidden_actions": ["禁止做什么"], + "acceptance_criteria": ["验收标准"] + }, + "delivery": { + "writeback_text": "Agent 的交付说明", + "artifact_paths": ["产物路径"], + "changed_files": ["变更文件"], + "validation": ["验证项"], + "rollback_plan": "回滚方案", + "hard_gates_declared": ["已声明 hard gates"], + "next_gate": "下一步门槛" + } +} +``` + +简化输出: + +```json +{ + "verdict": "pass | needs_review | fail", + "score": 82, + "missing": ["缺什么证据"], + "risks": ["主要风险"], + "positive_evidence": ["支持接受的证据"], + "next_gate": "下一步该谁做什么", + "buyer_summary": "给买家的短结论", + "evaluator_notes": "给仲裁/复核者的说明" +} +``` + +## 评分规则 + +总分 100。 + +| 维度 | 分数 | 看什么 | +|---|---:|---| +| 交付完整性 | 30 | artifact、变更文件、scope、rollback、next gate | +| 验证与证据 | 25 | 验证命令、实际结果、deferred 项、证据路径 | +| 安全与 hard gates | 25 | 是否越权发布、动凭证、动钱包、cleanup、绕 repo lock | +| 买家可用性 | 10 | verdict 是否能支持验收决策 | +| 争议可用性 | 10 | 是否能被 Evaluator 复核 | + +判定建议: + +- `pass`:85 分以上,并且没有关键 hard gate breach; +- `needs_review`:65-84 分,或存在未完成 release / buyer decision / validation gap; +- `fail`:低于 65 分,或缺核心产物、无验证、scope 不清、跨 hard gate。 + +任何关键 hard gate breach 都应直接压到 `fail`。 + +## 定价假设 + +详细版在: + +[pricing.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/pricing.md) + +### Tier 1:快速验收检查 + +建议价格:`$0.05 - $0.25 / call` + +适合: + +- 小任务; +- 单一 artifact; +- 只需要快速判断 pass / needs_review / fail。 + +### Tier 2:完整交付审计 + +建议价格:`$0.50 - $2.00 / call` + +适合: + +- repo / 网站 / 数据类交付; +- 多文件变更; +- release gate、验证、rollback 都重要。 + +### Tier 3:Evaluator 支持包 + +建议价格:`$2.00 - $8.00 / call` + +适合: + +- 争议准备; +- 高价值任务; +- Evaluator 需要结构化事实图。 + +当前建议:先做 Tier 1 或 Tier 2,不急着做 Tier 3。 + +## 5 个样例说明 + +样例输入在: + +[sample-inputs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/sample-inputs) + +样例输出在: + +[sample-outputs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/sample-outputs) + +### 样例 1:PMQuant 文案改名 + +输出:`needs_review`,82 分。 + +原因: + +- 本地变更完整; +- pricing / payment 没动; +- syntax 和 diff check 通过; +- 但 full build deferred; +- 老买家的“终身更新”承诺文案需要 Leo 过目。 + +这个样例说明:不是所有本地完成的任务都能直接发布。 + +### 样例 2:T310 网站治理更新 + +输出:`needs_review`,76 分。 + +原因: + +- 单文件变更; +- 本地 route / tsc / diff 检查通过; +- 但 build / screenshot deferred; +- `/updates` 是否适合承载 release governance 有 protocol tension; +- 还需要发布前重新验 route。 + +这个样例说明:审计器需要能识别“内容做了,但策略上还没放行”。 + +### 样例 3:Alkanes campaign red-stop + +输出:`pass`,90 分。 + +原因: + +- 任务结果是 postrun fail,但这是 guard 正确触发; +- red limit 超过后 next window 没继续; +- repair window 干净; +- hard gates 边界清楚。 + +这个样例很重要:它说明审计器不能机械地把失败日志等同于 Agent 失败。 +有些任务的正确交付就是“安全停止”。 + +### 样例 4:leo-dashboard 大分支验收包 + +输出:`needs_review`,78 分。 + +原因: + +- 分支范围、dirty files、已跑 validation 都写清楚; +- 但 build 和 visual smoke deferred; +- dirty copy edits 还没 owner 决策; +- 这是 read-only acceptance packet,不是 release packet。 + +这个样例说明:宽分支验收最需要区分“可继续推进”和“可发布”。 + +### 样例 5:Claude Science 只读观察 + +输出:`pass`,88 分。 + +原因: + +- 只读边界明确; +- 没有新项目、私有文件、SSH/HPC/Modal、下载、账号权限变更; +- 观察结果具体; +- 后续 live test 需要 explicit approval。 + +这个样例说明:只读研究类任务也可以被验收,只要边界和 next gate 清楚。 + +## 当前 go / no-go + +详细版在: + +[go-no-go.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/go-no-go.md) + +### Go:继续做本地 prototype + +原因: + +- 问题真实:Agent 市场确实需要验收层; +- 产品一句话能讲清楚; +- schema 可以固定; +- 样例能覆盖真实风险; +- A2MCP 可以自动化,不必先做人肉服务。 + +### No-go:今天不上 OKX.AI 正式版 + +原因: + +- 还没有 public endpoint; +- OKX.AI 买方需求未验证; +- 上架会触发账号、Agentic Wallet、收款地址、endpoint deploy、listing review; +- 现在只有本地 prototype,不是正式 OKX.AI 服务。 + +## 已补:本地 prototype + +现在这个目录已经不只是产品文档,也有一个本地可运行的规则型 prototype。 + +新增文件: + +- [package.json](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/package.json):本地 npm scripts; +- [src/auditor.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/src/auditor.mjs):核心审计规则; +- [bin/audit-agent-deliverable.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/bin/audit-agent-deliverable.mjs):CLI; +- [test/run-samples.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/test/run-samples.mjs):5 个样例的回归测试; +- [prototype.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/prototype.md):prototype 使用说明。 + +运行全部样例: + +```bash +cd /Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp +npm run audit:samples +``` + +运行测试: + +```bash +npm test +``` + +当前测试结果: + +```text +PASS 01-pmquant-rename.json: needs_review score=84 +PASS 02-t310-governance-update.json: needs_review score=80 +PASS 03-alkanes-red-stop.json: pass score=96 +PASS 04-dashboard-curation-readout.json: needs_review score=84 +PASS 05-claude-science-readout.json: pass score=93 +All sample audit cases passed +PASS http smoke on http://127.0.0.1: +``` + +prototype 的核心不是“聪明”,而是“稳定”: + +- 同样输入每次给同样 verdict; +- 明确区分 `hard-gate breach` 和 `hard-gate still open`; +- 能识别 deferred build、visual smoke 缺口、dirty state、protocol tension、red-stop、read-only delivery; +- 分数采用“维度分 + 风险封顶”:材料完整可以拿高基础分,但 release gate / dirty state / protocol tension 会把最终分数压到 needs_review 区间。 + +这说明它已经能做第一层自动化验收,但还不是正式 OKX.AI 服务。 + +## 多模型验证结论 + +验证文档在: + +[multi-model-validation-20260702.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/multi-model-validation-20260702.md) + +三路验证结论都是 `Yellow`: + +- 买家需求成立,但不是大众工具,而是“Agent 验收门禁”; +- 按次收费可以成立,但必须绑定验收、付款、争议节点; +- 本地 artifact / 静态 demo 可以推进,直接公网发布或 OKX.AI 上架是 Red。 + +因此当前判断是: + +```text +方向成立,但直接发布正式版不成立。 +``` + +更准确的产品名: + +```text +Agent 交付验收器 +Agent Acceptance Gate +``` + +不建议继续叫泛泛的 `Agent Deliverable Auditor` 面向用户发布,因为它听起来像抽象审计工具,用户感知弱。 + +## 已补:买家可感知 demo + +本地 demo 在: + +[demo/index.html](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/demo/index.html) + +这个 demo 把 JSON 输出变成买家能看懂的验收卡片: + +- `Accept / Needs Review / Reject`; +- 验收分; +- 不能直接接受的原因; +- 需要 seller 补的证据; +- 下一步 gate; +- 机器 flags; +- 评分维度。 + +这比 CLI 更接近真实产品,因为用户真正付费的时刻不是“拿到 JSON”,而是: + +```text +我准备接受 Agent 交付 / 放款 / 发布前,它告诉我现在能不能收。 +``` + +## 已补:本地 HTTP API + +现在也有本地 HTTP stub: + +- [src/http-server.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/src/http-server.mjs):本地 HTTP API; +- [bin/serve-demo.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/bin/serve-demo.mjs):启动本地 demo/API; +- [test/http-smoke.mjs](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/test/http-smoke.mjs):HTTP smoke test。 + +启动: + +```bash +cd /Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp +npm run serve +``` + +本地地址: + +```text +http://127.0.0.1:8787/ +``` + +接口: + +```text +GET /health +GET /api/sample-audits +GET /.well-known/agent-service.json +GET /mcp-tool-manifest.json +GET /openapi.yaml +POST /audit-agent-deliverable +``` + +这一步让它从“CLI 工具”变成“未来 A2MCP / HTTP seller 可以承接的服务形状”,但仍然没有部署、没有收款、没有 OKX 上架。 + +## 已补:Agent 自动发现材料 + +你问的关键是“能不能放进市场,让别的 AI 自动发现并调用”。现在已经补了机器可读入口: + +- [discovery/agent-service.json](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/discovery/agent-service.json):服务发现 metadata,写明 `call_when`、`do_not_call_for`、计费事件和 tags; +- [discovery/mcp-tool-manifest.json](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/discovery/mcp-tool-manifest.json):MCP 风格 tool manifest,写明 `audit_agent_delivery` 的输入输出 schema; +- [openapi.yaml](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/openapi.yaml):HTTP API contract; +- [agent-market-ecosystem-analysis-cn.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/agent-market-ecosystem-analysis-cn.md):Agent 市场生态分析。 + +这让产品从“人打开页面使用”进一步变成: + +```text +Agent 可发现 +Agent 可判断何时调用 +Agent 可按 schema 调用 +人类只看验收结论 +``` + +## 已补:抢先发布材料 + +为了抢先发优势,已经补了两个公开前材料: + +- [billing-event-protocol-v0.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/billing-event-protocol-v0.md):定义未来什么事件才可计费,避免“请求即扣费”这种含糊收费; +- [buyer-facing-launch-packet-cn.md](/Users/zhangxu/Projects/_inventory/2026-07-02/okx-ai-agent-deliverable-auditor-asp/buyer-facing-launch-packet-cn.md):中文发布包草稿,可改成 X、leolabs brief 或 OKX listing 的前置文案。 + +## 下一步建议 + +下一步仍然不是马上上架,而是做真实需求验证和发布渠道选择: + +1. 用真实 worker writeback 再跑 10 个样例; +2. 把 demo 截图/录屏,验证用户是否看得懂“为什么不能收”; +3. 选择一个明确公开渠道:X post、leolabs `/updates` brief、GitHub repo、还是 OKX.AI listing; +4. 如果要公开,先走对应 hard gate,而不是直接部署或上架。 + +## 仍然关闭的 hard gates + +以下都没有做,也不应在没有 Leo 明确批准时做: + +- OKX account / Agentic Wallet login; +- API key / credential setup; +- receiving wallet address; +- wallet funding / signing / transaction / staking; +- public endpoint deploy; +- ASP listing submission; +- website publication; +- repo commit / push / deploy。 diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/submission.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/submission.json new file mode 100644 index 00000000..2dc373ea --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/submission.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "name": "Agent Acceptance Gate", + "slug": "runesleo-agent-acceptance-gate", + "sourceRepository": "https://github.com/runesleo/agent-acceptance-gate", + "reviewCommit": "15c007b4a785a1263df989bb72c6e10e4b60ddf4", + "apiBaseUrl": "https://api.leolabs.me", + "healthCheckUrl": "https://api.leolabs.me/health", + "deploymentProofUrl": "https://api.leolabs.me/.well-known/xagent-verification.json" +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/README.md b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/README.md new file mode 100644 index 00000000..4e5d929a --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/README.md @@ -0,0 +1,71 @@ +# Verification evidence + +Live evidence captured after owner-authorized public deploy of commit `15c007b4a785a1263df989bb72c6e10e4b60ddf4`. + +## Prerequisites + +- Review commit: `15c007b4a785a1263df989bb72c6e10e4b60ddf4` +- Public source: https://github.com/runesleo/agent-acceptance-gate/commit/15c007b4a785a1263df989bb72c6e10e4b60ddf4 +- API base URL: `https://api.leolabs.me` +- Authentication: none during the approved reviewer window + +## 1. Health check + +```bash +curl --fail --silent --show-error https://api.leolabs.me/health +``` + +Frozen live response: `verification/live-health.json` + +Required fields include: + +```json +{"status":"ok","commit":"15c007b4a785a1263df989bb72c6e10e4b60ddf4"} +``` + +## 2. Deployment proof + +```bash +curl --fail --silent --show-error https://api.leolabs.me/.well-known/xagent-verification.json +``` + +Frozen live response: `verification/live-proof.json` + +Expected response: + +```json +{"schemaVersion":1,"slug":"runesleo-agent-acceptance-gate","commit":"15c007b4a785a1263df989bb72c6e10e4b60ddf4"} +``` + +## 3. Capability call + +From this submission directory: + +```bash +curl --fail --silent --show-error \ + --request POST https://api.leolabs.me/xagent/agent-delivery-acceptance-audit \ + --header "content-type: application/json" \ + --data @verification/request.json +``` + +Frozen live response: `verification/example-success.json`. A `needs_review` verdict is expected for this fixture because its public-release gate text remains intentionally conservative in the sample input; the capability must not falsely mark that state as accepted. + +Safe invalid-input call: + +```bash +curl --silent --show-error \ + --request POST https://api.leolabs.me/xagent/agent-delivery-acceptance-audit \ + --header "content-type: application/json" \ + --data '{"task":"Missing delivery summary"}' +``` + +Expected status: HTTP 400. Frozen response: `verification/example-safe-error.json`. + +## 4. Offline project checks + +```bash +cd source +npm ci +npm test +npm run worker:check +``` diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-safe-error.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-safe-error.json new file mode 100644 index 00000000..348d6f15 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-safe-error.json @@ -0,0 +1,4 @@ +{ + "error": "bad_request", + "message": "Audit input requires task (string or {buyer_goal, ...}) and delivery_summary. Optional: artifacts[], validation[], changed_files[], hard_gates[], next_gate, context." +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-success.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-success.json new file mode 100644 index 00000000..a34365bf --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/example-success.json @@ -0,0 +1,49 @@ +{ + "schema_version": "0.1", + "service_id": "agent_delivery_acceptance_audit", + "verdict": "needs_review", + "score": 84, + "dimension_scores": { + "delivery_completeness": 30, + "validation_and_evidence": 16, + "safety_and_hard_gates": 24, + "buyer_usability": 9, + "dispute_readiness": 10 + }, + "missing": [ + "actual validation result" + ], + "risks": [ + "Public release gate remains open." + ], + "positive_evidence": [ + "Artifact or evidence path is named.", + "Changed files or touched surface are declared.", + "Task goal and writeback are understandable.", + "Rollback or non-impact path is declared.", + "Next gate is explicit.", + "Validation checks are listed.", + "Hard gates are explicitly declared.", + "No critical hard-gate breach is detected from the supplied delivery text.", + "Buyer-facing decision path is mostly clear.", + "Evidence is reasonably dispute-ready.", + "Read-only boundary is explicitly documented." + ], + "questions_for_seller": [], + "next_gate": "Owner explicitly approves public push and deployment.", + "buyer_summary": "this delivery: useful delivery, but needs review before acceptance. Main gap: actual validation result.", + "buyer_summary_zh": "本次交付:有用但需复核后再验收。主要缺口:actual validation result。", + "evaluator_notes": "Delivery is read-only and should be evaluated mainly on boundary clarity, observations, limitations, and next gate.", + "machine_flags": [ + "hard_gates_declared", + "public_release_gate", + "read_only_delivery" + ], + "value_loop": { + "why_pay_again": "Each delivery is a new artifact set; re-run on every submit before accept/pay.", + "stale_after_minutes": null, + "best_used_in": "buyer_acceptance_gate_per_task", + "paid_value_tier": "A_repeat_workflow", + "fulfillment": "edge_on_demand_no_llm" + } +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-health.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-health.json new file mode 100644 index 00000000..3a456dba --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-health.json @@ -0,0 +1,8 @@ +{ + "status": "ok", + "commit": "15c007b4a785a1263df989bb72c6e10e4b60ddf4", + "ok": true, + "service": "agent-acceptance-gate", + "mode": "edge_worker", + "launch_lane": "okx_ai_asp" +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-proof.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-proof.json new file mode 100644 index 00000000..e8b85ffa --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/live-proof.json @@ -0,0 +1,5 @@ +{ + "schemaVersion": 1, + "slug": "runesleo-agent-acceptance-gate", + "commit": "15c007b4a785a1263df989bb72c6e10e4b60ddf4" +} diff --git a/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/request.json b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/request.json new file mode 100644 index 00000000..fb96df19 --- /dev/null +++ b/submissions/mcp-hackathon/runesleo-agent-acceptance-gate/verification/request.json @@ -0,0 +1,40 @@ +{ + "task": { + "buyer_goal": "Review a read-only agent delivery before acceptance.", + "surface": "repo", + "acceptance_criteria": [ + "Version binding is explicit", + "Tests pass", + "No public action occurred" + ] + }, + "delivery_summary": "Prepared a local-only, commit-bound X-Agent review surface without publishing, deploying, or changing the existing paid route.", + "artifacts": [ + "worker/index.mjs", + "openapi.yaml", + "DEPLOYMENT.md" + ], + "changed_files": [ + "worker/index.mjs", + "openapi.yaml", + "DEPLOYMENT.md" + ], + "validation": [ + "npm test", + "npm run worker:check", + "official offline submission validator" + ], + "validation_output": "npm test exit_code=0; worker:check exit_code=0; focused X-Agent tests exit_code=0.", + "rollback_plan": "Before deployment, discard the isolated branch. After an approved deployment, redeploy the previous verified commit and disable XAGENT_REVIEW_ENABLED.", + "hard_gates": [ + "No public push, deployment, account change, or official PR without owner approval." + ], + "next_gate": "Owner explicitly approves public push and deployment.", + "context": { + "repo_state": "clean", + "public_publish_requested": false, + "payments_or_wallets_in_scope": false, + "credentials_in_scope": false, + "notes": "Read-only deterministic audit; no outbound calls on this route." + } +}