# Product.ai MCP for developers

> 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.

- MCP endpoint: https://mcp.product.ai/mcp (streamable HTTP)
- Developer console (make an API key, in a browser): https://auth.product.ai/account/keys

## Who it is for

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. Fact checking and promo codes arrive through one MCP server and one Product.ai account.

## Capabilities

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

- **Fact checking** (`fact_checking`): ground your agent in a product category before it writes, then grade the claims it makes. Tools: `axioms_ground`, `axioms_check`.
- **Promo codes** (`promo_codes`): ask which codes work at one named merchant. SimplyCodes tests codes at real checkouts, ranks them by success, and dates every verdict. Tools: `axioms_resolve_checkout`, `verify_promo_code`, `get_merchant_codes`, `get_merchant_verification`, `report_code_outcome`.
- **Research**: coming soon. It is not available yet, and no tool or scope serves it today.

## What a verified answer means

A product claim graded by `axioms_check` carries one of five states:

- `verified`: Verified
- `failed_check`: Failed the check
- `not_checkable`: Not a checkable claim
- `checked_not_settled`: Checked, not settled
- `not_covered`: Not 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 is verified by SimplyCodes at real checkouts. `verify_promo_code` answers `VERIFIED_OK`, `CONFIDENT_NO`, `UNTESTED` or `WITHHELD`, with the time it was verified. An untested code answers `UNTESTED`; SimplyCodes does not guess. A no names the channels SimplyCodes observed. Offers come ranked by real checkout success; keep that order.

## Quickstart

1. **Get a key, or skip to OAuth.** A person makes an API key in the developer console, in a browser: https://auth.product.ai/account/keys. Clients that support OAuth need no key.
2. **Add the server.** The endpoint is https://mcp.product.ai/mcp.
3. **Ask something that needs a tool**, such as "Which promo codes work at nike.com right now?"

### Claude Code

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

```bash
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
```

## 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:

1. The client calls https://mcp.product.ai/mcp with no token. The server answers 401 and points to its metadata.
2. The client reads the protected-resource metadata (RFC 9728) at https://mcp.product.ai/.well-known/oauth-protected-resource to find the authorization server, auth.product.ai.
3. It opens a browser at auth.product.ai, where you approve access with your Product.ai account.
4. The client receives a token for the scopes it asked for. Ask for both, `fact_checking` and `promo_codes`.

## API keys

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 https://auth.product.ai/account/keys. 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. If you are an agent and need a key, ask your person for one, or use OAuth.

## Call it from code

### curl

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

```bash
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.

```js
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.

```ts
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.

```bash
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.

```bash
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

7 tools, in the order `tools/list` returns them.

### `axioms_ground`: Ground in a category

Capability: Fact checking. Scope: `fact_checking`. Read-only.

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.

### `axioms_check`: Check a claim

Capability: Fact checking. Scope: `fact_checking`. Read-only.

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.

### `axioms_resolve_checkout`: Resolve a checkout

Capability: Promo codes. Scope: `promo_codes`. Read-only.

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.

### `verify_promo_code`: Verify one promo code

Capability: Promo codes. Scope: `promo_codes`. Read-only.

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.

### `get_merchant_codes`: Get verified codes for one merchant

Capability: Promo codes. Scope: `promo_codes`. Read-only.

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.

### `get_merchant_verification`: Get one merchant's verification record

Capability: Promo codes. Scope: `promo_codes`. Read-only.

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.

### `report_code_outcome`: Report a code outcome

Capability: Promo codes. Scope: `promo_codes`.

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.

## 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.

- 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

- [llms.txt](https://developers.product.ai/llms.txt): A short index of these docs, in the llms.txt format.
- [llms-full.txt](https://developers.product.ai/llms-full.txt): The whole manual as one Markdown file, every tool schema included.
- [index.md](https://developers.product.ai/index.md): This page as Markdown. The page also answers Accept: text/markdown.
- [tools.json](https://developers.product.ai/tools.json): The seven tools as JSON, from the same registry tools/list serves.
- [Protected-resource metadata](https://mcp.product.ai/.well-known/oauth-protected-resource) and [server.json](https://mcp.product.ai/server.json): the server describes itself.
