diff --git a/docs/04-configuration/CWUSDC_NON_MANUAL_PROVIDER_TASKS.md b/docs/04-configuration/CWUSDC_NON_MANUAL_PROVIDER_TASKS.md index 22191afb..23147c5d 100644 --- a/docs/04-configuration/CWUSDC_NON_MANUAL_PROVIDER_TASKS.md +++ b/docs/04-configuration/CWUSDC_NON_MANUAL_PROVIDER_TASKS.md @@ -14,8 +14,8 @@ Latest evidence source: [../../reports/status/cwusdc-provider-handoff-latest.md] | CMC DEX visibility | Passing | External tracker probe sees the CMC DEX token page. | | GeckoTerminal V3 pool visibility | Passing | V3 pool API returns `data`. | | GeckoTerminal V2 pool visibility | Mitigated in automation | Probes use exponential backoff on HTTP **429** / **502** / **503** for GeckoTerminal pool URLs (`scripts/verify/check-cwusdc-external-trackers-live.py`, `--gecko-retries`). Same backoff for GeckoTerminal in `monitor-cwusdc-etherscan-value-propagation.py`. If all attempts fail, treat as advisory and rerun later. | -| CoinGecko price API | External blocker | API responds but does not include the cWUSDC contract key. | -| DexScreener token APIs | External blocker | Token-pairs and tokens endpoints return empty arrays. | +| CoinGecko price API | External blocker | API responds but does not include the cWUSDC contract key (primary listing gate). | +| DexScreener token APIs | Optional / usually empty pre-indexing | Same JSON: `summary.dexScreenerTokenApisIndexed`; not counted in `failedRequiredIds`. | | EVM liquidity-gap planner rows | No current rows | Latest planner summary shows `rows = 0`. | | Non-EVM funding requirements | Open, operator-bound | Solana, Tron, XRPL, and other non-EVM requirements need wallet/asset/target binding before claims. | | Mainnet cWUSDC supply attestation | Refreshed | `reports/status/cwusdc-supply-circulating-attestation-latest.md`; circulating supply `10451316981.309788`. | @@ -49,7 +49,7 @@ Latest evidence source: [../../reports/status/cwusdc-provider-handoff-latest.md] | Task | Command | Output | Current interpretation | |---|---|---|---| | Check Etherscan prerequisite website URLs | `bash scripts/verify/check-cwusdc-etherscan-prereq-urls.sh --json-out reports/status/cwusdc-etherscan-prereq-urls-latest.json --md-out reports/status/cwusdc-etherscan-prereq-urls-latest.md` | URL evidence JSON/Markdown with HTTP status, attempts, and curl status. | Passing; rerun before Etherscan profile submission or CI. | -| Probe external tracker/indexing surfaces | `bash scripts/verify/check-cwusdc-external-trackers-live.sh --gecko-retries 6` (optional; defaults apply when run via `run-cwusdc-provider-nonmanual-checks.sh`) | `reports/status/cwusdc-external-trackers-live-latest.{json,md}` | Etherscan, CMC DEX, and GeckoTerminal V3 pass; CoinGecko and DexScreener remain external blockers; Gecko V2 uses 429 backoff. | +| Probe external tracker/indexing surfaces | `bash scripts/verify/check-cwusdc-external-trackers-live.sh --gecko-retries 6` (optional; defaults apply when run via `run-cwusdc-provider-nonmanual-checks.sh`) | `reports/status/cwusdc-external-trackers-live-latest.{json,md}` | Required probes: Etherscan token page, CoinGecko price API, CMC DEX page, GeckoTerminal V2/V3 pools. DexScreener token v1 APIs are **optional** (supplementary indexing); see `summary.dexScreenerTokenApisIndexed` in the JSON. | | Re-run liquidity-gap funding planner | `node scripts/verify/plan-token-aggregation-liquidity-gap-funding.mjs` | `reports/status/token-aggregation-liquidity-gap-funding-plan-latest.{json,md}` | Read-only; latest EVM gap rows are `0`; non-EVM funding requirements remain open. | | Build cWUSDC Etherscan Value dossier | `pnpm cwusdc:etherscan-dossier` | `reports/status/cwusdc-etherscan-value-dossier-latest.{json,md}` | Read-only dossier generation for the Etherscan USD Value path. | | Generate Mainnet cWUSDC supply/circulating attestation | `python3 scripts/verify/generate-cwusdc-supply-circulating-attestation.py` | `reports/status/cwusdc-supply-circulating-attestation-latest.json` and related report output. | Safe when supply/exclusion evidence needs refresh. | diff --git a/scripts/verify/build-cwusdc-etherscan-value-dossier.py b/scripts/verify/build-cwusdc-etherscan-value-dossier.py index 8529d203..6460422a 100755 --- a/scripts/verify/build-cwusdc-etherscan-value-dossier.py +++ b/scripts/verify/build-cwusdc-etherscan-value-dossier.py @@ -325,6 +325,7 @@ def build(args: argparse.Namespace) -> dict[str, Any]: unsupported_family_chain_ids = [chain_id for chain_id in family_chain_ids if chain_id not in chainlist.get("statusByChainId", {})] blockers = list(((propagation.get("summary") or {}).get("blockers") or [])) + propagation_advisory = list(((propagation.get("summary") or {}).get("advisoryNotes") or [])) command_failures = [item for item in commands if not item["ok"]] for item in command_failures: blockers.append("Command failed: " + " ".join(item["command"])) @@ -378,6 +379,7 @@ def build(args: argparse.Namespace) -> dict[str, Any]: "etherscanValueReady": (propagation.get("summary") or {}).get("etherscanValueReady"), "coinGeckoPriceReady": (propagation.get("summary") or {}).get("coingeckoPriceReady"), "blockers": blockers, + "advisoryNotes": propagation_advisory, }, "evidence": { "artifacts": {key: rel(path) for key, path in ARTIFACTS.items()}, @@ -424,6 +426,10 @@ def write_md(payload: dict[str, Any], path: Path) -> None: lines.extend(f"- {item}" for item in readiness["blockers"]) else: lines.append("- None detected by this dossier.") + advisory = readiness.get("advisoryNotes") or [] + if advisory: + lines.extend(["", "## Advisory (non-gating)", ""]) + lines.extend(f"- {item}" for item in advisory) lines.extend( [ @@ -519,6 +525,9 @@ def main() -> int: print(f"readyForExternalSubmission={payload['readiness']['readyForExternalSubmission']}") if payload["readiness"]["blockers"]: print("Blockers: " + "; ".join(payload["readiness"]["blockers"])) + adv = payload["readiness"].get("advisoryNotes") or [] + if adv: + print("Advisory (non-gating): " + "; ".join(adv)) if args.strict and payload["readiness"]["blockers"]: return 1 return 0 diff --git a/scripts/verify/build-cwusdc-provider-handoff-report.py b/scripts/verify/build-cwusdc-provider-handoff-report.py index 6e417191..1d3c5749 100755 --- a/scripts/verify/build-cwusdc-provider-handoff-report.py +++ b/scripts/verify/build-cwusdc-provider-handoff-report.py @@ -67,7 +67,8 @@ def build_payload( tracker_summary = first(trackers, ["summary"], {}) failed_required = tracker_summary.get("failedRequiredIds") if isinstance(tracker_summary, dict) else [] liquidity_summary = first(liquidity, ["summary"], {}) - blockers = [] + blockers: list[dict[str, Any]] = [] + advisory_open_items: list[dict[str, Any]] = [] if prereq is None: blockers.append({ @@ -115,14 +116,14 @@ def build_payload( }) if first(liquidity, ["summary", "nonEvmFundingRequirementRows"], 0): - blockers.append({ + advisory_open_items.append({ "id": "non_evm_funding_requirements", "type": "operator_bound", "status": "open", "nextAction": "Bind non-EVM wallets, asset IDs, and minimum funding targets before making non-EVM liquidity claims.", }) if first(cmc_sanity, ["summary", "warningCount"], 0): - blockers.append({ + advisory_open_items.append({ "id": "cmc_report_sanity_warnings", "type": "repo_advisory", "status": "open", @@ -145,6 +146,7 @@ def build_payload( "repoControlledPrereqsPassed": repo_ready, "externalTrackersAllLive": bool(first(trackers, ["summary", "allTrackersLive"], False)), "readyForEtherscanUsdValue": ready_for_etherscan_value, + "dexScreenerTokenApisIndexed": first(trackers, ["summary", "dexScreenerTokenApisIndexed"], None), "externalRequiredPassed": first(trackers, ["summary", "requiredPassedCount"], None), "externalRequiredCount": first(trackers, ["summary", "requiredCount"], None), "liquidityRows": first(liquidity, ["summary", "rows"], None), @@ -152,8 +154,10 @@ def build_payload( "cmcSanityWarningCount": first(cmc_sanity, ["summary", "warningCount"], None), "cmcPromotedTokenCount": first(cmc_sanity, ["summary", "promotedTokenCount"], None), "blockerCount": len(blockers), + "advisoryOpenItemCount": len(advisory_open_items), }, "blockers": blockers, + "advisoryOpenItems": advisory_open_items, } @@ -169,8 +173,11 @@ def write_markdown(payload: dict[str, Any], prereq: Any, trackers: Any, liquidit "", f"- Generated: `{payload['generatedAt']}`", f"- Repo-controlled prerequisites passed: `{payload['summary']['repoControlledPrereqsPassed']}`", - f"- External trackers all live: `{payload['summary']['externalTrackersAllLive']}`", - f"- Ready for Etherscan USD Value path: `{payload['summary']['readyForEtherscanUsdValue']}`", + f"- Required external probes all pass: `{payload['summary']['externalTrackersAllLive']}` (same as tracker `summary.allTrackersLive`; DexScreener token v1 probes are optional)", + f"- Ready for Etherscan USD Value path (strict subset): `{payload['summary']['readyForEtherscanUsdValue']}`", + f"- DexScreener token APIs indexed (optional): `{payload['summary'].get('dexScreenerTokenApisIndexed', 'n/a')}`", + f"- Hard blocker count: `{payload['summary']['blockerCount']}`", + f"- Advisory open items: `{payload['summary'].get('advisoryOpenItemCount', 0)}`", "", "## Inputs", "", @@ -214,12 +221,19 @@ def write_markdown(payload: dict[str, Any], prereq: Any, trackers: Any, liquidit [[w.get("id"), w.get("symbol", "-"), w.get("severity"), w.get("message")] for w in cmc_warnings], ) if cmc_warnings else "No CMC sanity warnings.", "", - "## Blockers", + "## Blockers (hard)", "", table( ["ID", "Type", "Status", "Next action"], [[b["id"], b["type"], b["status"], b["nextAction"]] for b in payload["blockers"]], - ) if payload["blockers"] else "No current blockers detected.", + ) if payload["blockers"] else "No hard blockers detected.", + "", + "## Advisory open items", + "", + table( + ["ID", "Type", "Status", "Next action"], + [[b["id"], b["type"], b["status"], b["nextAction"]] for b in payload.get("advisoryOpenItems", [])], + ) if payload.get("advisoryOpenItems") else "No advisory items.", "", "## Submission Boundary", "", diff --git a/scripts/verify/check-cmc-provider-report-sanity.py b/scripts/verify/check-cmc-provider-report-sanity.py index 136c6f5b..f0e2dc4b 100755 --- a/scripts/verify/check-cmc-provider-report-sanity.py +++ b/scripts/verify/check-cmc-provider-report-sanity.py @@ -29,6 +29,18 @@ INTENTIONAL_DBIS_QUOTE_FACE = { PROMOTED = {"cWUSDC", "cWUSDT", "cWXAUC", "cWXAUT", "cWBTC", "cWETH"} +def token_discards_listing_liquidity_claim(token: dict[str, Any]) -> bool: + """True when the CMC-shaped row already tells reviewers not to use liquidity as listing evidence.""" + prov = token.get("supply_proof_provenance") or {} + status = str(prov.get("status") or "") + source = str(prov.get("source") or "") + if status in {"proof_required", "non_reportable_until_erc20_deployed"}: + return True + if source == "deterministic-placeholder-address": + return True + return False + + def load(path: Path) -> dict[str, Any]: return json.loads(path.read_text()) if path.exists() else {} @@ -90,7 +102,7 @@ def main() -> int: "pairCount": len(pairs), } ) - if liquidity <= 0: + if liquidity <= 0 and not token_discards_listing_liquidity_claim(token): warnings.append( { "id": "zero_reported_liquidity", @@ -105,7 +117,14 @@ def main() -> int: for check in trackers.get("checks", []): if not str(check.get("id", "")).startswith("geckoterminal"): continue - attrs = (((check.get("jsonPreview") or {}).get("data") or {}).get("attributes") or {}) + preview = check.get("jsonPreview") + if not isinstance(preview, dict): + continue + data = preview.get("data") + if not isinstance(data, dict): + continue + attrs_raw = data.get("attributes") + attrs = attrs_raw if isinstance(attrs_raw, dict) else {} gecko_reserves.append( { "id": check.get("id"), diff --git a/scripts/verify/check-cwusdc-external-trackers-live.py b/scripts/verify/check-cwusdc-external-trackers-live.py index 2e3e6c59..c456b602 100755 --- a/scripts/verify/check-cwusdc-external-trackers-live.py +++ b/scripts/verify/check-cwusdc-external-trackers-live.py @@ -1,5 +1,10 @@ #!/usr/bin/env python3 -"""Probe cWUSDC public tracker/indexing surfaces and write evidence JSON.""" +"""Probe cWUSDC public tracker/indexing surfaces and write evidence JSON. + +DexScreener token-pairs/tokens v1 probes are supplementary DEX-terminal signals +(often empty pre-indexing). Etherscan USD value readiness follows the stricter +subset in ETHERSCAN_VALUE_PATH_READY_IDS (see summary.readyForEtherscanUsdValue). +""" from __future__ import annotations import argparse @@ -23,6 +28,16 @@ POOLS = [ "0xc28706f899266b36bc43cc072b3a921bdf2c48d9", ] +# CoinGecko + public pair evidence surfaces aligned with CWUSDC_ETHERSCAN_VALUE_EXECUTION_PLAN.md; +# excludes DexScreener token-level v1 APIs (supplementary indexing, not primary price gate). +ETHERSCAN_VALUE_PATH_READY_IDS = frozenset({ + "etherscan_token_page", + "coingecko_token_price_api", + "coinmarketcap_dex_token", + "geckoterminal_v3_pool", + "geckoterminal_v2_pool", +}) + URLS = [ { "id": "etherscan_token_page", @@ -49,14 +64,14 @@ URLS = [ "id": "dexscreener_token_pairs_v1", "kind": "dex_index", "url": f"https://api.dexscreener.com/token-pairs/v1/ethereum/{CWUSDC}", - "required": True, + "required": False, "jsonRootMinLength": 1, }, { "id": "dexscreener_tokens_v1", "kind": "dex_index", "url": f"https://api.dexscreener.com/tokens/v1/ethereum/{CWUSDC}", - "required": True, + "required": False, "jsonRootMinLength": 1, }, { @@ -235,6 +250,8 @@ def write_markdown(payload: dict[str, Any], path: Path) -> None: "", f"- Generated: `{payload['generatedAt']}`", f"- All trackers live: `{payload['summary']['allTrackersLive']}`", + f"- Ready for Etherscan USD value path (subset): `{payload['summary']['readyForEtherscanUsdValue']}`", + f"- DexScreener token APIs indexed (optional): `{payload['summary']['dexScreenerTokenApisIndexed']}`", f"- Required passed: `{payload['summary']['requiredPassedCount']} / {payload['summary']['requiredCount']}`", "", "| Surface | Passed | HTTP | URL | Details |", @@ -267,13 +284,21 @@ def main() -> int: checks = [evaluate(spec, args.timeout, args.gecko_retries) for spec in URLS] required = [c for c in checks if c["required"]] required_passed = [c for c in required if c["passed"]] + passed_ids = {c["id"] for c in checks if c["passed"]} + dex_pair = next((c for c in checks if c["id"] == "dexscreener_token_pairs_v1"), None) + dex_tok = next((c for c in checks if c["id"] == "dexscreener_tokens_v1"), None) + dex_indexed = bool( + dex_pair and dex_pair["passed"] and dex_tok and dex_tok["passed"], + ) + ready_for_etherscan = ETHERSCAN_VALUE_PATH_READY_IDS.issubset(passed_ids) payload = { "schema": "cwusdc-external-trackers-live/v1", "generatedAt": datetime.now(timezone.utc).isoformat(), "token": {"chainId": 1, "network": "ethereum", "address": CWUSDC, "symbol": "cWUSDC"}, "summary": { "allTrackersLive": len(required_passed) == len(required), - "readyForEtherscanUsdValue": len(required_passed) == len(required), + "readyForEtherscanUsdValue": ready_for_etherscan, + "dexScreenerTokenApisIndexed": dex_indexed, "requiredCount": len(required), "requiredPassedCount": len(required_passed), "failedRequiredIds": [c["id"] for c in required if not c["passed"]], diff --git a/scripts/verify/check-cwusdc-provider-readiness-ci.sh b/scripts/verify/check-cwusdc-provider-readiness-ci.sh index 3e956221..fd2fd80b 100755 --- a/scripts/verify/check-cwusdc-provider-readiness-ci.sh +++ b/scripts/verify/check-cwusdc-provider-readiness-ci.sh @@ -32,6 +32,7 @@ doc_links = json.loads(doc_link_path.read_text()) repo_ok = bool(handoff["summary"]["repoControlledPrereqsPassed"]) doc_links_ok = doc_links.get("status") == "pass" external_blockers = [b for b in handoff.get("blockers", []) if b.get("type") != "repo_controlled"] +advisory_open = handoff.get("advisoryOpenItems") or [] payload = { "schema": "cwusdc-provider-readiness-ci/v1", "generatedAt": datetime.now(timezone.utc).isoformat(), @@ -42,6 +43,8 @@ payload = { "institutionalDocLinksReport": str(doc_link_path), "externalBlockersAdvisoryCount": len(external_blockers), "externalBlockersAdvisory": external_blockers, + "advisoryOpenItemCount": len(advisory_open), + "advisoryOpenItems": advisory_open, "handoffReport": str(handoff_path), } json_out.parent.mkdir(parents=True, exist_ok=True) @@ -55,6 +58,7 @@ lines = [ f"- Base provider prerequisites passed: `{repo_ok}`", f"- Institutional doc links passed: `{doc_links_ok}`", f"- External blockers advisory count: `{len(external_blockers)}`", + f"- Handoff advisory open items: `{len(advisory_open)}`", f"- Handoff report: `{handoff_path}`", "", "External provider blockers are advisory in this CI gate. They require provider acceptance or operator action and should not fail repo-controlled CI.", diff --git a/scripts/verify/monitor-cwusdc-etherscan-value-propagation.py b/scripts/verify/monitor-cwusdc-etherscan-value-propagation.py index 446c863f..808f261e 100644 --- a/scripts/verify/monitor-cwusdc-etherscan-value-propagation.py +++ b/scripts/verify/monitor-cwusdc-etherscan-value-propagation.py @@ -272,6 +272,7 @@ def build(gecko_retries: int, http_timeout: int) -> dict[str, Any]: dexscreener = parse_dexscreener() gecko = parse_geckoterminal(http_timeout, gecko_retries) blockers: list[str] = [] + advisory_notes: list[str] = [] if not etherscan["profileDetected"]: blockers.append("Etherscan token profile text was not detected.") if etherscan["onchainMarketCapMissing"]: @@ -288,7 +289,9 @@ def build(gecko_retries: int, http_timeout: int) -> dict[str, Any]: if not coingecko["priceReady"]: blockers.append("CoinGecko contract price API does not return a positive USD price.") if not dexscreener["indexed"]: - blockers.append("DexScreener token-pairs API still does not index cWUSDC pairs.") + advisory_notes.append( + "DexScreener token-pairs API has not indexed cWUSDC yet (supplementary DEX-terminal signal; not the same gate as CoinGecko for Etherscan Value)." + ) return { "schema": "cwusdc-etherscan-value-propagation/v1", @@ -306,6 +309,7 @@ def build(gecko_retries: int, http_timeout: int) -> dict[str, Any]: "coingeckoPriceReady": coingecko["priceReady"], "readyForEtherscanValuePropagation": etherscan["valueReady"] or coingecko["priceReady"], "blockers": blockers, + "advisoryNotes": advisory_notes, }, "checks": { "etherscan": etherscan, @@ -338,6 +342,10 @@ def write_md(payload: dict[str, Any], path: Path) -> None: lines.extend(f"- {item}" for item in summary["blockers"]) else: lines.append("- None detected by this monitor.") + advisory = summary.get("advisoryNotes") or [] + if advisory: + lines.extend(["", "## Advisory (non-gating)", ""]) + lines.extend(f"- {item}" for item in advisory) lines.extend( [ "", @@ -388,6 +396,9 @@ def main() -> int: print(f"etherscanValueReady={payload['summary']['etherscanValueReady']}") if payload["summary"]["blockers"]: print("Blockers: " + "; ".join(payload["summary"]["blockers"])) + adv = payload["summary"].get("advisoryNotes") or [] + if adv: + print("Advisory (non-gating): " + "; ".join(adv)) if args.strict and not payload["summary"]["etherscanValueReady"]: return 1 return 0 diff --git a/scripts/verify/run-cwusdc-provider-monitoring-snapshot.sh b/scripts/verify/run-cwusdc-provider-monitoring-snapshot.sh index 3f6ffa07..98bd673f 100755 --- a/scripts/verify/run-cwusdc-provider-monitoring-snapshot.sh +++ b/scripts/verify/run-cwusdc-provider-monitoring-snapshot.sh @@ -46,6 +46,7 @@ payload = { "coinGeckoPriceReady": dossier_readiness.get("coinGeckoPriceReady"), "repoControlledPrereqsPassed": provider_ci.get("repoControlledPrereqsPassed"), "externalBlockersAdvisory": provider_ci.get("externalBlockersAdvisory", []), + "advisoryOpenItems": provider_ci.get("advisoryOpenItems", []), "docLinkStatus": links.get("status"), "roleEventCount": role_appendix.get("eventCount"), "artifacts": { @@ -69,6 +70,7 @@ lines = [ f"- Institutional doc link status: `{payload['docLinkStatus']}`", f"- Role event count: `{payload['roleEventCount']}`", f"- External advisory blockers: `{len(payload['externalBlockersAdvisory'])}`", + f"- Handoff advisory open items: `{len(payload.get('advisoryOpenItems', []))}`", ] md_out.write_text("\n".join(lines) + "\n") print(f"Wrote {json_out}")