Skip to content

NAP v3 — Verified Product Offers ​

NAP v3 („Name, Address, Product") is CatyAI's signed product-offer envelope. Every offer served to AI crawlers and API consumers carries an Ed25519 signature over a canonicalized payload, verifiable offline against our public JWKS — no trust in the transport required.

  • Issuer: catyai/payaix — PayAi-X FZE (Dubai, UAE)
  • Protocol version: nap_version: 3.0 (present in every envelope)
  • Algorithm: EdDSA / Ed25519 (RFC 8032), payload canonicalization RFC 8785 JCS
  • Key discovery: JWKS, multi-kid (active + previous during rotation — always select the key by kid, never assume a fixed one)

Quick check: public compliance endpoint ​

The fastest way to verify a domain end-to-end (llms.txt presence through redirects, widget resolution, live Ed25519 chain):

bash
curl -s -X POST "https://shop.catyai.io/api/public/nap-verify" \
  -H "Content-Type: application/json" \
  -d '{"domain": "gotrendy.ro"}'

Returns nap_v3_score (0–100), nap_v3_ready, and a per-check report (llms_txt, widget_resolution, jwks, …). No auth required.

Where signed envelopes live ​

All endpoints are live on https://shop.catyai.io unless noted.

SurfacePathWhat you get
Public catalog lookupGET /geo/v2/lookup?q=… or ?gtin=, ?sku=, ?pid= (combinable with max_price, category, merchant + q)Full envelope per product: tso, ai_knowledge_layer, json_ld (with embedded catyai:tso), trust. Serves the affiliate catalog + direct clients.
Per-widget lookupGET /geo/v2/lookup?widget_id=<UUID>&sku=… (or ?pid=)Direct-client catalog as JSON-LD. For bot-classified user agents the signature is served in the X-AKL-Signature response header.
AI catalogGET /geo/v2/catalog?domain=<domain> (+ optional ?category=<name>&limit=&page=)Category sitemap (no category param) or product listing, with a response-level trust block (issuer, kid, signature).
Agentic indexGET /geo/v2/catalog/agentic-index.txt?widget_id=<UUID>Plain-text product index. The full path with widget_id is required — /agentic-index.txt in the root does not exist (404 by design). Rate limit 10/h per IP, no auth.
llms.txtGET https://<client-domain>/llms.txtServed through the client's Trust Gateway injection; links are attribution-tagged (?caty_source=ai_geo&caty_agent=llms_txt).
AI contextGET /.well-known/ai-context?domain=<domain>Machine-readable capability manifest of the domain.
UCP manifestGET https://api.catyai.io/.well-known/ucpUniversal Commerce Protocol manifest (org identity + service specs per ucp.dev/2026-04-08).
MCP descriptorGET https://api.catyai.io/.well-known/mcpMCP server descriptor — streamable_http endpoint at /api/mcp, protocol versions, capabilities.
JWKSGET https://api.catyai.io/.well-known/jwks.jsonPublic Ed25519 keys (kty: OKP, crv: Ed25519), one per kid.

Envelope format ​

Real example (captured live, abbreviated), from GET /geo/v2/lookup?sku=RICHTER-MSP1:

json
{
  "success": true,
  "protocol": "catyai/akl-affiliate",
  "version": "2.0",
  "nap_version": "3.0",
  "count": 1,
  "products": [
    {
      "name": "Purificator de Aer Portabil cu UV-C RiCHTER MSP1 …",
      "price": 403.42,
      "currency": "RON",
      "gtin": null,
      "merchant_name": "gotrendy.ro",
      "tso": { "…": "trusted source object — the signed payload" },
      "ai_knowledge_layer": { "…": "optional semantic enrichment" },
      "json_ld": {
        "@context": ["https://schema.org", { "catyai": "https://catyai.io/ns/akl#" }],
        "@type": "Product",
        "…": "schema.org Product (validators see only standard fields)",
        "catyai:tso": {
          "product_id": "d88f4d1b-50fb-438d-93f1-c8848859c2e0",
          "sku": "RICHTER-MSP1",
          "name": "Purificator de Aer Portabil cu UV-C RiCHTER MSP1 …",
          "price": 403.42,
          "currency": "RON",
          "availability": "https://schema.org/InStock",
          "condition": "https://schema.org/NewCondition",
          "url": "https://shop.catyai.io/aff/6acf01d025f220816e2dd01d?caty_source=ai_geo&caty_agent=other_bot",
          "image": "https://gomagcdn.ro/…/purificator-de-aer-….jpg",
          "gtin": null,
          "brand": "RiCHTER",
          "merchant_name": "gotrendy.ro",
          "category": "Casă și Grădină > Aparate de climatizare > …",
          "verified_at": "2026-10-14T04:42:38.644Z",
          "widget_id": "caty_gotrendy_client",
          "signature": "0_T7lxve8zkQdbIXcMbo2U00AsQZtp8GP8GUw4npouGh4JZF0mJ3glWhyWqa3-cpBB7flYkY9NesmOylpnGjAg",
          "kid": "catyai-akl-v2-2026-05-05",
          "alg": "EdDSA",
          "canonicalization": "RFC8785-JCS",
          "content_hash": "sha256:b917bab9309ab867129d384c867b98333303c54b51e3425bca7150d7e82a9a12",
          "signed_at": "2026-10-14T15:53:51.663Z",
          "jwks_uri": "https://api.catyai.io/.well-known/jwks.json"
        }
      },
      "trust": {
        "issuer": "catyai/payaix",
        "issued_by": "PayAi-X FZE (Dubai, UAE)",
        "algorithm": "EdDSA",
        "kid": "catyai-akl-v2-2026-05-05",
        "signature": "0_T7lxve8zkQdbIXcMbo2U00AsQZtp8GP8GUw4npouGh4JZF0mJ3glWhyWqa3-cpBB7flYkY9NesmOylpnGjAg",
        "content_hash": "sha256:b917bab9309ab867129d384c867b98333303c54b51e3425bca7150d7e82a9a12",
        "canonicalization": "RFC8785-JCS",
        "signed_at": "2026-10-14T15:53:51.663Z",
        "jwks_uri": "https://api.catyai.io/.well-known/jwks.json"
      }
    }
  ]
}

Field semantics:

  • tso (Trusted Source Object) — the offer facts: identity (sku, gtin, brand, merchant_name), commercial (price, currency, availability, condition, url), classification (category), provenance (widget_id, verified_at = when the offer was last validated, not merely written).
  • signature, kid, alg, canonicalization, content_hash, signed_at, jwks_uri — the signature frame. These seven fields are NOT part of the signed message.
  • content_hash — sha256: + hex of the RFC 8785-canonicalized TSO. Recompute it to detect accidental normalization differences on your side.
  • Absence of the signature frame (e.g. signing key unreachable) is itself a signal: the offer is served unsigned rather than withheld.

Verification procedure ​

The signed message is the TSO without the seven signature-frame fields, canonicalized with RFC 8785 JCS, signed with Ed25519.

Steps:

  1. Extract the catyai:tso object (or the tso / trust pair).
  2. Remove signature, kid, alg, canonicalization, content_hash, signed_at, jwks_uri.
  3. Canonicalize the remainder with RFC 8785 JCS (UTF-8 bytes).
  4. Fetch JWKS from jwks_uri; select the key whose kid matches the envelope's kid; decode x from base64url → 32 raw bytes.
  5. Decode signature from base64url → 64 bytes; verify Ed25519 over the canonical bytes.
  6. Recompute content_hash and compare. Check verified_at freshness for your use case.

Reference implementation (Node.js):

js
import canonicalize from 'canonicalize';
import * as ed from '@noble/ed25519';

const SIGNATURE_FIELDS = ['signature', 'kid', 'alg', 'canonicalization', 'content_hash', 'signed_at', 'jwks_uri'];

export async function verifyNapV3(tsoSigned) {
  const payload = Object.fromEntries(
    Object.entries(tsoSigned).filter(([k]) => !SIGNATURE_FIELDS.includes(k))
  );
  const canonical = canonicalize(payload);                    // RFC 8785 JCS
  const jwks = await (await fetch(tsoSigned.jwks_uri)).json();
  const jwk = jwks.keys.find((k) => k.kid === tsoSigned.kid); // select BY kid
  if (!jwk) return { valid: false, reason: 'unknown kid' };

  const publicBytes = Buffer.from(jwk.x, 'base64url');
  const sigBytes = Buffer.from(tsoSigned.signature, 'base64url');
  const message = new TextEncoder().encode(canonical);
  const valid = await ed.verifyAsync(sigBytes, message, publicBytes);
  return { valid, kid: tsoSigned.kid };
}

Negative test: modifying any single field (e.g. price) must make verification fail. If it doesn't, your canonicalization differs from RFC 8785 JCS — the content_hash recomputation in step 6 will show it.

Stability contract & key rotation ​

  • Fields in the TSO are append-only between major versions: existing fields are never renamed or removed without a nap_version major bump.
  • JWKS publishes multiple kids (active + previous) so keys rotate without verification downtime. Verifiers must key off kid, never hardcode a key.
  • Envelopes are re-verifiable offline indefinitely: JWKS + payload is all you need.

Auth behavior (as deployed) — per endpoint ​

Auth is not uniform across the surface. Verified live (14 Oct 2026):

EndpointBrowser UA, no credentialsNotes
/geo/v2/lookup — public catalog (?q=/?gtin=/?sku=/?pid=, cu sau fără widget_id)200Public by design — this is the AI-agent-facing surface. It serves public-safe product fields only (identity, price, availability, category, URLs). No X-API-Key, no crawler whitelist; protection is the per-IP rate limit. Same behaviour with or without widget_id.
/geo/v2/catalog with widget_id/domain/category403 AUTH_REQUIREDReal catalog data requires a whitelisted AI crawler UA or a valid X-API-Key.
/geo/v2/catalog without parameters, browser UA200Service metadata only (protocol, version, status: ok) — a deliberate discovery probe for UCP checkers. No catalog data is returned.
/geo/v2/catalog without parameters, whitelisted crawler UA400 VALIDATION_ERRORAsymmetric with the browser case by design: the crawler passes auth before the probe branch, then fails parameter validation.
/geo/v2/answerauth requiredSee rate limits below.

Security note: the direct-client catalog behind /geo/v2/lookup?widget_id=… is intentionally public the same way a merchant's product pages are public — the envelope adds provenance and integrity (signature + verified_at), not secrecy. If a client ever needs a private catalog, that is a separate product decision, not something the current gate implies.

Real per-IP rate limits (fixed windows, Redis-backed):

EndpointLimit
/geo/v2/lookup (public, no widget_id)30/min
/geo/v2/lookup (per-widget)120/h
/geo/v2/catalog60/h (+ valid key or crawler UA)
/geo/v2/answer60/h
/geo/v2/catalog/agentic-index.txt10/h
/api/public/nap-verifypublic, no auth

Feed Automation · Google CSS Partner · AI Visibility