Score any storefront 0-100 on how ready it is for AI shopping agents — Universal Commerce Protocol manifest, ACP product-feed conformance, structured data, AI-crawler policy — and track adoption over time. Cohort mode turns 500 merchants into an adoption curve you cannot buy retroactively.
UCP manifest probe against /.well-known/ucp, parsed defensively: no hard-coded schema, rawManifest stored verbatim, unknown keys logged as drift rather than thrown on. Verified live against real merchant manifests at version 2026-04-08.
Structured-data probe scoring JSON-LD Product/Offer/Organization/AggregateRating completeness across the homepage and sampled product pages.
AI-crawler policy parser implementing the RFC 9309 group model for 11 tracked agents, plus Cloudflare pay-per-crawl (HTTP 402) detection. Scoring input only — no standalone output.
Platform fingerprinting for Shopify and WooCommerce, used to select a readable catalog surface.
Composite 0–100 score with published weights and always-emitted sub-scores, grade bands, and a ranked topFixes list.
Snapshot and diff against a named key-value store, emitting eight typed change types. First scan of a domain is a baseline and emits nothing.
Cohort mode writing COHORT.json with adoption rates by capability, payment handler, platform and version, plus median score and leaderboard.
Standby mode and MCP compatibility, sharing one pipeline with batch runs.
Buyer-facing cost estimator written before work begins, and a developer-facing unit-economics harness that measures real platform cost and emits a verdict.
Optional Slack and generic webhook notifications; optional AI remediation narrative in reports.
131 tests covering scoring, ACP validation, robots parsing, UCP parsing against a captured real manifest, change detection, input handling, free-tier caps and SSRF screening.
Pricing
Hypothesis prices were validated against measured platform cost before release. All five events sit above their cost-covering floor at 3× headroom; product-validated is the thinnest line. The pay-per-event + usage toggle is deliberately not enabled. Full calibration table in README.md.
Notes on three bugs found by live data during the build
All three were found by running against real storefronts rather than fixtures, and all three are now pinned by tests:
ProductGroup was not counted as a product. Shopify emits ProductGroup for any product with variants, so a fully marked-up Shopify storefront scored 0 on structured data. allbirds.com went from 73.8 to 89.8 once fixed. This would have systematically understated the entire Shopify population.
Untyped JSON-LD nodes were discarded.allbirds.com publishes its rating as a bare { aggregateRating: … } node with no @type; a strict @type filter dropped it along with the points it had earned.
Unreachable domains scored full marks on crawler policy. "No robots.txt" was conflated with "the host never answered", so a domain that does not resolve scored 10/10 for permitting AI crawlers — and 10/100 overall. A live site answering 404 genuinely permits everything; a network-level failure is an absence of evidence. Dead domains now score 0.
Two bugs found by the first monetized platform run
Both were invisible locally, because both hinged on platform environment variables that do not exist in development — so local behaviour was accidentally correct while deployed behaviour was not. Both are now pinned by tests in tests/monetization-gating.test.ts.
Every run became a standby server that never exited.main.ts entered standby whenever ACTOR_STANDBY_PORT was set, but the platform sets that variable on ordinary runs too — and standbyUrl on the Actor was None, so standby was not even enabled. A batch run therefore finished its audit and then sat in the standby event loop until the run hit its timeout, burning compute and firing a spurious request-served charge. Standby is now selected by APIFY_META_ORIGIN === "STANDBY", the documented signal; ACTOR_STANDBY_PORT only supplies the port number.
Free-plan caps throttled paying customers.monetized required a positive ACTOR_MAX_PAID_DATASET_ITEMS, which belongs to the pay-per-RESULT model and is never set for a pay-per-event actor. So monetized was permanently false: the first genuinely monetized run charged domain-audited, manifest-parsed and product-validated while simultaneously logging "Free-plan run: capped to 1 target and 5 records", capping the cohort product to a single domain. monetized now follows pay-per-event pricing, and still honours the paid-items variable as an additional positive signal. Free-plan users are bounded by the platform's own total-charge ceiling, which chargeSafely already observes — that costs the developer nothing, since a run's compute is billed to whoever started it.
Note on output schemas and what a dataset view can actually do
Adding .actor/output_schema.json for publication surfaced a wrong assumption. An Apify dataset view selects columns; it does not filter rows. Verified against a live dataset: ?view=changes returned every record in the run, not just the change events, with the change columns blank on the rest. skipEmpty and clean do not rescue this, because the shared envelope fields (kind, target, collectedAt) are present on every record, so no row ever projects to an empty object.
Two fixes:
CHANGES.json is now written to the key-value store on every run, containing only the change events plus counts by type — an empty array on a baseline run, so a scheduled consumer polls one stable URL and never handles a 404 as a special case. The output schema's "Adoption changes" entry points here rather than at a view that cannot filter.
The change event's summary field is renamed changeSummary. The run-summary record carries a summaryobject, so two different shapes shared one key in the same dataset — the run summary leaked its whole object into the changes view. This is a breaking change to the change-record shape.
The overview, changes and productGaps view descriptions now state plainly that they are column presets over all rows.
Note on memory, found by the first platform run
Local estimates showed byte transfer dominating cost. The first real platform run reported mode: "platform-actual" with the actor allocated the full 4096 MB, where compute was 99.8% of platform cost. Re-running at 1024 MB produced identical output for 4.6× less: $0.001712 → $0.000373 per domain.
.actor/actor.json declares defaultRunOptions.memoryMbytes: 1024, but Apify applies that only at actor creation — apify push does not update it on an existing actor, and no CLI command does either. On an already-deployed actor it is a one-time Console change, documented in RUNBOOK.md.
Pricing was never at risk: even at the wasteful 4096 MB, domain-audited cleared its cost-covering floor by 3.1×.
Note on the cost driver
The initial hypothesis was that product-validated drives cost. Measuring at 1× and 10× scale disproved it: cost per domain falls with volume ($0.000896 → $0.000584) while cost per product rises. Every domain costs the same ~12 probe requests regardless of productSampleSize. costDriverEvent is domain-audited, and byte accounting is now split by category so product validation is charged only its own marginal cost (29% of bytes on the acceptance input) rather than all of it (91%).