Developers Developer console

Product.ai MCP

Verified product truth for the agents you build

One MCP server for developers who build AI agents. Your agent checks product claims against Product.ai's graded record and gets promo codes SimplyCodes tested at real checkouts.

Endpointhttps://mcp.product.ai/mcp
# 1. Add the server
 claude mcp add --transport http productai https://mcp.product.ai/mcp

# 2. Your agent checks a claim, then polls its check_id
tools/call axioms_check
  { "claim": "Memory foam is made from polyurethane." }
tools/call axioms_check
  { "check_id": "chk_5f0e2a9c41d7b3e86a1f4c2d9b07e35a" }

# 3. The grade (example values, the contract's own fields)
{ "contract": "axioms/1", "status": "graded", "source": "fresh",
  "check_id": "chk_5f0e2a9c41d7b3e86a1f4c2d9b07e35a",
  "claims": [{
    "index": 0, "state": "verified", "tier": "forged",
    "text": "Memory foam is made from polyurethane.",
    "plain_truth": "Memory foam is viscoelastic polyurethane foam.",
    "what_would_change": null, "qualifier": null,
    "condition": null, "reason": null, "recheck_by": null,
    "reuse_until": "2027-01-26T19:04:11Z",
    "graded_at": "2026-10-28T19:04:11Z" }],
  "graded_at": "2026-10-28T19:04:11Z" }

Built for the people who build agents

Product.ai is the door an outside AI agent calls for verified truth. Keys go to developers and platforms, never to shoppers: whoever builds the agent holds the key.

01

One endpoint, seven tools

Fact checking and promo codes arrive through one MCP server and one Product.ai account.

02

Answers your agent can cite

Every answer says how it was verified and when, so your agent can show its work.

03

No guessing

When the record does not cover a question, the answer says so instead of making one up.

Two capabilities, one key

Each capability is an OAuth scope. An API key carries both.

Available

Fact checking

Ground your agent in a product category before it writes, using the laws Product.ai has verified for that category. Then grade the claims it makes, one atomic claim at a time.

axioms_groundaxioms_check
Available

Promo codes

Ask which codes work at one named merchant. SimplyCodes tests codes at real checkouts, ranks them by success, and dates every verdict.

axioms_resolve_checkoutverify_promo_codeget_merchant_codesget_merchant_verificationreport_code_outcome
Coming soon

Research

New verified answers, researched on demand. It is not available yet, and no tool or scope serves it today.

What a verified answer means

Every answer carries a state from a closed list. Here is what each one means.

A product claim

axioms_check splits text into atomic claims and grades each one against the Product.ai Truth Graph.

  • verifiedVerified
  • failed_checkFailed the check
  • not_checkableNot a checkable claim
  • checked_not_settledChecked, not settled
  • not_coveredNot covered
  • Each grade carries a plain_truth, the tier of evidence behind it, and what_would_change the answer.
  • A forged law means three or more independent research providers converged. A probable law means two agreed.
  • A fresh claim answers pending with a check_id. A pending answer is never a grade.

A promo code

verify_promo_code returns SimplyCodes' dated verdict. SimplyCodes tests codes at real checkouts.

  • VERIFIED_OKTested at checkout and working.
  • CONFIDENT_NOTested and NOT working, scoped to channels_observed; a real answer, not a failure.
  • UNTESTEDSimplyCodes has no checkout test for this code, so no claim is made either way.
  • WITHHELDThe merchant resolves but its verification page is not materialized yet, so nothing is asserted.
  • Every verdict carries the time it was verified.
  • A no names the channels SimplyCodes observed. It never claims that no codes exist anywhere.
  • Offers come ranked by real checkout success. Your agent keeps that order.

Documentation

Build with the Product.ai MCP

Connect your client, choose OAuth or an API key, and call the seven tools from any language.

Endpoint
mcp.product.ai/mcp
Transport
Streamable HTTP
Auth
OAuth or API key
Tools
Seven, in two scopes

Quickstart

Three steps from nothing to a working tool call.

  1. Get a key, or skip to OAuth

    A person makes an API key in the developer console, in a browser. Clients that support OAuth need no key; they ask you to approve access when they connect.

    Open the developer console
  2. Add the server

    The endpoint is https://mcp.product.ai/mcp. Pick your client.

    Claude Code

    Run this once. Claude Code opens a browser so you can approve access with your Product.ai account.

    shell
    claude mcp add --transport http productai https://mcp.product.ai/mcp
    
    # Or use an API key instead of the browser approval:
    claude mcp add --transport http productai https://mcp.product.ai/mcp \
      --header "Authorization: Bearer $PRODUCTAI_API_KEY"

    Cursor

    Add this to ~/.cursor/mcp.json, or to .cursor/mcp.json in one project.

    json
    {
      "mcpServers": {
        "productai": {
          "url": "https://mcp.product.ai/mcp",
          "headers": {
            "Authorization": "Bearer pai_live_..."
          }
        }
      }
    }

    VS Code

    Add this to .vscode/mcp.json. VS Code asks for the key once and stores it as a secret.

    json
    {
      "inputs": [
        {
          "type": "promptString",
          "id": "productai-key",
          "description": "Product.ai API key",
          "password": true
        }
      ],
      "servers": {
        "productai": {
          "type": "http",
          "url": "https://mcp.product.ai/mcp",
          "headers": {
            "Authorization": "Bearer ${input:productai-key}"
          }
        }
      }
    }

    Any client

    Any MCP client that speaks streamable HTTP connects by URL. Clients that support MCP authorization, such as Claude's custom connectors, need nothing else.

    text
    URL     https://mcp.product.ai/mcp
    Auth    OAuth, or the header  Authorization: Bearer pai_live_...
    Scopes  fact_checking promo_codes
  3. Ask something that needs a tool

    Your agent now sees seven tools. Ask it a question such as “Which promo codes work at nike.com right now?” or “Check this claim: this topper is natural latex.”

Connect with OAuth

Every call carries a verified Product.ai identity. There is no anonymous access.

Clients that support MCP authorization connect with the URL alone. Here is what happens when they do.

  1. 01

    Your client calls https://mcp.product.ai/mcp with no token. The server answers 401 and points to its metadata.

  2. 02

    The client reads the protected-resource metadata (RFC 9728) to find the authorization server, auth.product.ai.

  3. 03

    It opens a browser at auth.product.ai. You approve access with your Product.ai account.

  4. 04

    The client receives a token for the scopes it asked for. Ask for both, fact_checking and promo_codes, so every tool works.

API keys

Scripts, servers and headless agents use an API key.

A key starts with pai_live_ for production or pai_test_ for test data. Send it on every request as Authorization: Bearer pai_live_.... A key carries both scopes.

A person makes keys in a browser at the developer console. Scripts and coding agents cannot create keys, because the console refuses automated clients. Once a person has made a key, any script can use it.

Keep the key in an environment variable such as PRODUCTAI_API_KEY, and never commit it.

shell
export PRODUCTAI_API_KEY="pai_live_..."

Call it from code

The raw protocol, the two official SDKs, and the model APIs that call remote MCP servers for you.

curl

The raw protocol: JSON-RPC over streamable HTTP. Open a session, then list and call tools in it.

shell
export PRODUCTAI_API_KEY="pai_live_..."
MCP=https://mcp.product.ai/mcp

# 1. Open a session. Its id comes back in the mcp-session-id header.
SESSION=$(curl -sS -D - -o /dev/null "$MCP" \
  -H "Authorization: Bearer $PRODUCTAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"curl","version":"1.0.0"}}}' \
  | awk 'tolower($1) == "mcp-session-id:" { print $2 }' | tr -d '\r')

# 2. Tell the server the client is ready.
curl -sS "$MCP" \
  -H "Authorization: Bearer $PRODUCTAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. List the seven tools.
curl -sS "$MCP" \
  -H "Authorization: Bearer $PRODUCTAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 4. Call one.
curl -sS "$MCP" \
  -H "Authorization: Bearer $PRODUCTAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
        "name":"axioms_ground","arguments":{"category":"mattresses"}}}'

JavaScript

Plain fetch, no dependencies. Save as example.mjs and run it with Node 18 or later.

javascript
const MCP = "https://mcp.product.ai/mcp";
const KEY = process.env.PRODUCTAI_API_KEY;
let session;

async function rpc(method, params, id) {
  const res = await fetch(MCP, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json, text/event-stream",
      ...(session ? { "mcp-session-id": session } : {}),
    },
    body: JSON.stringify({ jsonrpc: "2.0", method, params, id }),
  });
  session ??= res.headers.get("mcp-session-id");
  const text = await res.text();
  if (id === undefined) return null; // a notification gets no reply
  // The server answers as a short event stream; the reply is its data line.
  const data = text.split("\n").find((line) => line.startsWith("data: "));
  return JSON.parse(data ? data.slice(6) : text);
}

await rpc("initialize", {
  protocolVersion: "2025-06-18",
  capabilities: {},
  clientInfo: { name: "my-agent", version: "1.0.0" },
}, 1);
await rpc("notifications/initialized");

const listed = await rpc("tools/list", {}, 2);
console.log(listed.result.tools.map((tool) => tool.name));

const called = await rpc("tools/call", {
  name: "axioms_ground",
  arguments: { category: "mattresses" },
}, 3);
console.log(called.result.structuredContent);

TypeScript

The official MCP SDK. Tested with @modelcontextprotocol/sdk 1.32.2: npm install @modelcontextprotocol/sdk, then run it as an ES module.

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const url = new URL("https://mcp.product.ai/mcp");
const transport = new StreamableHTTPClientTransport(url, {
  requestInit: {
    headers: { Authorization: `Bearer ${process.env.PRODUCTAI_API_KEY}` },
  },
});
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((tool) => tool.name));

const result = await client.callTool({
  name: "axioms_ground",
  arguments: { category: "mattresses" },
});
console.log(result.structuredContent);

await client.close();

Python

The official MCP SDK. Tested with mcp 2.3.0: pip install "mcp>=2.3", which installs the httpx2 client this uses.

python
import asyncio
import os

import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client

MCP_URL = "https://mcp.product.ai/mcp"


async def main() -> None:
    key = os.environ["PRODUCTAI_API_KEY"]
    headers = {"Authorization": f"Bearer {key}"}
    async with httpx2.AsyncClient(headers=headers) as http:
        transport = streamable_http_client(MCP_URL, http_client=http)
        async with Client(transport) as client:
            listed = await client.list_tools()
            print([tool.name for tool in listed.tools])
            result = await client.call_tool(
                "axioms_ground", {"category": "mattresses"}
            )
            print(result.structured_content)


asyncio.run(main())

Claude API

Anthropic's MCP connector: the Messages API calls the server for you. The key rides in authorization_token.

shell
curl https://api.anthropic.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: mcp-client-2025-11-20" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 1000,
    "messages": [{"role": "user", "content": "Which promo codes work at nike.com right now?"}],
    "mcp_servers": [
      {
        "type": "url",
        "url": "https://mcp.product.ai/mcp",
        "name": "productai",
        "authorization_token": "pai_live_..."
      }
    ],
    "tools": [
      { "type": "mcp_toolset", "mcp_server_name": "productai" }
    ]
  }'

OpenAI API

OpenAI's remote MCP tool: the Responses API calls the server for you. The key rides in authorization, without the Bearer prefix.

shell
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-6-astra",
    "tools": [
      {
        "type": "mcp",
        "server_label": "productai",
        "server_description": "Verified product truth and promo codes tested at real checkouts.",
        "server_url": "https://mcp.product.ai/mcp",
        "authorization": "pai_live_...",
        "require_approval": "never"
      }
    ],
    "input": "Which promo codes work at nike.com right now?"
  }'

Tools

Seven tools, in the order tools/list returns them. Each card is rendered from the server's own registry.

Fact checkingRead-only · Idempotent

axioms_ground

Ground in a category

Get the Truth Pack for one product category before you reason or write about it.

Scope fact_checking

Full description and input schema

Get the Truth Pack for one product category before you reason or write about it. A Truth Pack is a short, prompt-ready block of the category's laws from the Product.ai Truth Graph: up to 10 laws, each with its axiom id, its confidence tier (forged = three or more independent research providers converged; probable = two agreed) and its layer, plus up to 4 elimination gates where the category records a falsifier. Pass exactly one of category (a slug such as skincare-actives or mattresses; prefixes and plain names resolve) or topic (free text, resolved to the closest served category). Packs are pre-built, so this answers fast and makes no model call. If the category is not served you get status not_covered with reason shelf_not_served, and the miss is counted as demand. Use the pack as grounding, cite axiom ids when you rely on a law, and call axioms_check on any specific claim before you assert it to a person. Requires the fact_checking scope.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "category": {
      "description": "A category slug such as skincare-actives or mattresses. Prefixes and plain names resolve.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "topic": {
      "description": "Free text, resolved to the closest served category when no slug matches.",
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    }
  }
}
Fact checkingRead-only · Idempotent

axioms_check

Check a claim

Check a factual product claim against the Product.ai Truth Graph and get a grade per atomic claim.

Scope fact_checking

Full description and input schema

Check a factual product claim against the Product.ai Truth Graph and get a grade per atomic claim. Pass exactly one of claim (1 to 6000 characters of text), check_id (to poll a check you started), url or sku. Each graded claim carries one of five states: verified, failed_check, not_checkable, checked_not_settled or not_covered, with plain_truth, tier, what_would_change, qualifier, condition and reuse_until. A claim graded before answers at once from an exact-text cache (status graded, source cache). An unchecked claim answers status pending with a check_id and retry_after_ms: grading runs in the background, so poll with that check_id until the status is graded. pending is never a grade, and claims is empty until it is. check_failed means grading failed after retries; resubmit after retry_after_ms. budget_deferred means the day's grade budget is spent. url and sku answer not_covered with reason spec_index_unbuilt today. Builder tier and above. Requires the fact_checking scope.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "claim": {
      "description": "The claim text to grade, 1 to 6000 characters.",
      "type": "string",
      "minLength": 1,
      "maxLength": 6000
    },
    "check_id": {
      "description": "Poll a check you started: the check_id from a pending answer.",
      "type": "string",
      "pattern": "^chk_[0-9a-f]{32}$"
    },
    "url": {
      "description": "A product page URL (https). Answers not_covered today: the spec index is unbuilt.",
      "type": "string",
      "maxLength": 2048
    },
    "sku": {
      "description": "A product SKU. Answers not_covered today: the spec index is unbuilt.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "category": {
      "description": "Optional category slug, a hint only. An unknown category is dropped, never a reason to skip grading.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    }
  }
}
Promo codesRead-only

axioms_resolve_checkout

Resolve a checkout

Hand a shopper's merchant question to SimplyCodes, the Product.ai promo-code verifier, and get its verified answer plus a checkout link.

Scope promo_codes

Full description and input schema

Hand a shopper's merchant question to SimplyCodes, the Product.ai promo-code verifier, and get its verified answer plus a checkout link. Pass merchant (a domain such as nike.com, or the merchant's name) and optionally limit (1 to 10 offers, default 5). A sku alone is not resolved yet and answers status unresolved. The simplycodes field is SimplyCodes' own get_merchant_codes answer, unchanged: offers ranked by real checkout tests, a verification capsule, provenance meta, and either a literal code (paid tiers) or a reveal_url (free tier). checkout_link is the first offer's reveal_url or store link, copied verbatim; relay it unchanged so attribution survives. price is always null: no live merchant price source exists, and price_status says so. SimplyCodes' refusals (keyed_only, rate_capped, access_pending with apply_url, merchant_not_found, principal_suspended, retrieval_failure) come back as errors with its own reason. Requires the promo_codes scope.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "merchant": {
      "description": "The merchant, by domain (nike.com) or name (Nike). Required today.",
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "sku": {
      "description": "A product SKU. Alone, it answers unresolved today.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "limit": {
      "description": "How many offers to return, 1 to 10 (default 5).",
      "type": "integer",
      "minimum": 1,
      "maximum": 10
    }
  }
}
Promo codesRead-only · Idempotent

verify_promo_code

Verify one promo code

Check whether one promo code works at a merchant right now and get SimplyCodes' dated verdict.

Scope promo_codes

Full description and input schema

Check whether one promo code works at a merchant right now and get SimplyCodes' dated verdict. Returns verdict (VERIFIED_OK, CONFIDENT_NO, UNTESTED, or WITHHELD), verified_at, ttl_seconds, the verification capsule, and channels_observed for a NO. Use this when a shopper already has a code and asks if it is real; an untested code returns UNTESTED with no guess, never a fabricated verdict.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "merchant": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120,
      "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name."
    },
    "promo_code": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "description": "The exact promo code to check, as the shopper has it. Case and surrounding spaces are ignored."
    }
  },
  "required": [
    "merchant",
    "promo_code"
  ]
}
Promo codesRead-only · Idempotent

get_merchant_codes

Get verified codes for one merchant

Get the promo codes SimplyCodes has verified as working at one merchant, ranked by success rate.

Scope promo_codes

Full description and input schema

Get the promo codes SimplyCodes has verified as working at one merchant, ranked by success rate. Returns offers with success counts and last-verified dates, a capsule stating what was tested, and either a literal code on paid tiers or a reveal_url on the free tier. Use this when a shopper is about to check out at a named merchant; it never lists codes for merchants you did not name.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "merchant": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120,
      "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name."
    },
    "limit": {
      "default": 10,
      "description": "How many verified offers to return, 1 to 20. Defaults to 10. This bounds one merchant, not a merchant walk.",
      "type": "integer",
      "minimum": 1,
      "maximum": 20
    }
  },
  "required": [
    "merchant"
  ]
}
Promo codesRead-only · Idempotent

get_merchant_verification

Get one merchant's verification record

Get SimplyCodes' verification record for one merchant without any codes: how many public codes were tested, how many worked, the working rate, and when the last test ran.

Scope promo_codes

Full description and input schema

Get SimplyCodes' verification record for one merchant without any codes: how many public codes were tested, how many worked, the working rate, and when the last test ran. Returns tested_count, working_count, working_rate, last_verified_at, freshness (measured, unmeasured, or stale), and a citable capsule. Use this when the question is whether a merchant's coupons are worth pursuing, or to cite SimplyCodes as the verifier.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "merchant": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120,
      "description": "One merchant, by domain (nike.com) or full name (Nike). Required — this tool never answers for a merchant you did not name."
    }
  },
  "required": [
    "merchant"
  ]
}
Promo codesWrites · Idempotent

report_code_outcome

Report a code outcome

Report what happened when a shopper used a code SimplyCodes served: worked, failed, expired, or not applicable, with the order value if known.

Scope promo_codes

Full description and input schema

Report what happened when a shopper used a code SimplyCodes served: worked, failed, expired, or not applicable, with the order value if known. Returns recorded (true), the outcome as stored, and a standing_note. Use this after every checkout attempt; reports are the data payment that keeps free access open, duplicates for the same code on the same day are ignored, and nothing you report raises your cap.

inputSchema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "merchant": {
      "type": "string",
      "minLength": 2,
      "maxLength": 120,
      "description": "The merchant the code was used at, as domain or name — the same identifier you looked it up with."
    },
    "code_ref": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "description": "The `code_ref` SimplyCodes returned with the offer. Not the code string itself."
    },
    "outcome": {
      "type": "string",
      "enum": [
        "worked",
        "failed",
        "expired",
        "not_applicable"
      ],
      "description": "What happened at checkout. worked — the code applied and discounted the order. failed — the code was rejected. expired — the merchant said the code has expired. not_applicable — the shopper never attempted it (wrong cart, changed their mind)."
    },
    "issue": {
      "description": "Why it failed. Send this only with outcome failed; it is the folded code-feedback vote.",
      "type": "string",
      "enum": [
        "expired",
        "minimum_not_met",
        "excluded_items",
        "invalid",
        "other"
      ]
    },
    "order_value": {
      "description": "Order total in the store's currency, if the shopper shared it. Optional, never required.",
      "type": "number",
      "minimum": 0,
      "maximum": 1000000
    },
    "discount_value": {
      "description": "How much the code took off, if known. Optional.",
      "type": "number",
      "minimum": 0,
      "maximum": 1000000
    },
    "notes": {
      "description": "One short human note, 500 characters max. No personal data.",
      "type": "string",
      "maxLength": 500
    }
  },
  "required": [
    "merchant",
    "code_ref",
    "outcome"
  ]
}

Access and fair use

The four SimplyCodes tools are in early access. Anyone can make an account and a key. SimplyCodes approves developers by hand. Until it approves you, those tools answer access_pending with a link to apply, and retrying does not help.

Rules of the road

  • Keys are per developer. Do not share or resell one.
  • Say who is calling. Attribute calls to your own key and platform, and never mask the origin.
  • Cite the verifier. Name SimplyCodes and keep the date with the answer.
  • Keep the serving order. Do not re-rank offers.
  • Ask about one merchant per lookup. There is no bulk export.

For AI agents

The same docs, in formats a model reads without a browser.

The server describes itself too: protected-resource metadata and server.json.