# Zeekend for AI agents

Sponsored product offers for shopping agents. Your agent asks for what the
shopper wants; Zeekend answers with labeled offers from brands that pay when a
purchase is made, each with the exact variant, the store's real price and a
checkout link. You earn a share of every order your agent sends.

Call it alongside your normal search. It answers in tens of milliseconds and
returns nothing when no brand fits, so it never slows your agent down or
crowds out better results.

Base URL: `https://agents-bench.zeekend.com`

---

## Try it in 30 seconds

`agt_test` is the sandbox key: real offers, nothing recorded, nothing billed.

    curl -s -X POST https://agents-bench.zeekend.com/v1/agent/search \
      -H "authorization: Bearer agt_test" \
      -H "content-type: application/json" \
      -d '{"query":"charles leclerc tshirt under ₹1000","constraints":{"country":"IN"},"limit":3}'

For a live key (`agt_live_...`) that earns, contact hello@zeekend.com.
Keep it on your server: it is a secret, not a browser key.

---

## Two ways to connect

**MCP**, if your agent or framework supports remote MCP servers (Claude,
ChatGPT developer mode, Cursor, the OpenAI Agents SDK, LangChain, Mastra and
most others). One URL, no code:

    https://agents-bench.zeekend.com/mcp

Authenticate with `Authorization: Bearer agt_live_...`. Clients that only take
a URL (ChatGPT and Claude connectors) can use `https://agents-bench.zeekend.com/mcp?key=agt_live_...`
instead; treat that URL as a secret.

Tools: `search_offers`, `get_offer`, `check_availability`, `get_checkout_link`.
The server's instructions tell the model when to call them and how to label
the results.

**REST**, if you would rather own the tool definition. One call, shown below
in each major SDK.

---

## When to call it

On any turn that could end in someone buying something, even if they never
say "buy": product searches, gift ideas, "what should I get", recommendations
and comparisons. Call it **in parallel** with your web or catalog search and
merge the results.

Search with product or interest words ("charles leclerc tshirt", "trail
running shoes"), not the occasion. Words like "gift", "birthday" or "for my
brother" are ignored anyway.

Always pass the shopper's `country`: it decides which brands ship to them and
which currency price limits are in.

## How to show results

These are the rules your agent must follow. They are what keep sponsored
results trustworthy, and a condition of a live key.

1. **Label every offer as sponsored**, visibly, next to the offer.
2. **Link `cta.url` (the `checkout_url`)** for buying. It opens the store's
   cart with the exact variant already added. Never link `url`, the plain
   product page: a purchase through it cannot be credited.
3. **Show the store's price** (`price_display`) and, when present, the "was"
   price (`compare_at_display`). Never restate a price you have not been given.
4. **Let relevance win.** If an organic result is a better fit, say so.
   Zeekend ranks by relevance before money, and your agent should too.
5. **An empty `offers` array is normal.** Show your other results.

---

## Code

### TypeScript, Anthropic SDK (tool use)

```ts
const zeekendTool = {
  name: "search_sponsored_offers",
  description:
    "Find products to buy or gift, as sponsored offers from brands. Use for shopping, gift ideas and " +
    "recommendations, alongside web search. Results are ads: label them as sponsored and link cta.url to buy.",
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Product or interest words" },
      country: { type: "string", description: "Shopper's ISO country code, e.g. US, IN" },
      price_max: { type: "number" },
      currency: { type: "string", description: "Currency of price_max, e.g. USD, INR" },
    },
    required: ["query", "country"],
  },
};

async function searchSponsoredOffers(input: { query: string; country: string; price_max?: number; currency?: string }) {
  const r = await fetch("https://agents-bench.zeekend.com/v1/agent/search", {
    method: "POST",
    headers: { authorization: `Bearer ${process.env.ZEEKEND_AGENT_KEY}`, "content-type": "application/json" },
    body: JSON.stringify({
      query: input.query,
      constraints: { country: input.country, price_max: input.price_max, currency: input.currency },
      limit: 3,
    }),
    signal: AbortSignal.timeout(1500),
  });
  if (!r.ok) return { offers: [] };            // never let ads break your agent
  return r.json();
}
```

Pass `zeekendTool` in `tools` on `client.messages.create`, and when the model
returns a `tool_use` block for it, call `searchSponsoredOffers` and send the
JSON back as the `tool_result`.

### TypeScript, Vercel AI SDK

```ts
import { tool } from "ai";
import { z } from "zod";

export const searchSponsoredOffers = tool({
  description:
    "Find products to buy or gift, as sponsored offers from brands. Results are ads: label them as " +
    "sponsored and link cta.url to buy.",
  inputSchema: z.object({
    query: z.string(),
    country: z.string().length(2),
    price_max: z.number().optional(),
    currency: z.string().length(3).optional(),
  }),
  execute: async ({ query, country, price_max, currency }) => {
    const r = await fetch("https://agents-bench.zeekend.com/v1/agent/search", {
      method: "POST",
      headers: { authorization: `Bearer ${process.env.ZEEKEND_AGENT_KEY}`, "content-type": "application/json" },
      body: JSON.stringify({ query, constraints: { country, price_max, currency }, limit: 3 }),
      signal: AbortSignal.timeout(1500),
    });
    return r.ok ? r.json() : { offers: [] };
  },
});
```

### Python, OpenAI SDK (function calling)

```python
import os, requests

ZEEKEND_TOOL = {
    "type": "function",
    "function": {
        "name": "search_sponsored_offers",
        "description": "Find products to buy or gift, as sponsored offers from brands. "
                       "Results are ads: label them as sponsored and link cta.url to buy.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "country": {"type": "string", "description": "ISO country code, e.g. US, IN"},
                "price_max": {"type": "number"},
                "currency": {"type": "string"},
            },
            "required": ["query", "country"],
        },
    },
}

def search_sponsored_offers(query, country, price_max=None, currency=None):
    try:
        r = requests.post(
            "https://agents-bench.zeekend.com/v1/agent/search",
            headers={"authorization": f"Bearer {os.environ['ZEEKEND_AGENT_KEY']}"},
            json={"query": query, "constraints": {"country": country, "price_max": price_max, "currency": currency},
                  "limit": 3},
            timeout=1.5,
        )
        return r.json() if r.ok else {"offers": []}
    except requests.RequestException:
        return {"offers": []}
```

### Calling it directly, without the model deciding

The most reliable integration: on every turn your agent classifies as
shopping, call Zeekend from your own code in parallel with your search, and
give both result sets to the model. Then sponsored offers appear whenever one
fits, not only when the model thinks to ask.

```ts
const [web, sponsored] = await Promise.all([webSearch(q), searchSponsoredOffers({ query: q, country })]);
```

---

## Reference

### POST /v1/agent/search

    {
      "query": "black oversized hoodie under $80",
      "constraints": {
        "country": "US",
        "currency": "USD",
        "price_max": 80,
        "price_min": null,
        "categories": ["apparel"],
        "attributes": { "color": "black", "size": "M" },
        "exclude_advertisers": ["example.com"]
      },
      "limit": 5,
      "session_id": "your-conversation-id"
    }

| Field | |
| --- | --- |
| `query` | Required. Plain words. Prices ("under ₹1000", "$50-$80"), a colour and "size M" are read from it when not given as constraints. |
| `constraints.country` | Required. ISO 3166 code of the shopper. |
| `constraints.currency` | Currency of the price limits. Defaults to the currency the query names, else the country's. |
| `constraints.price_max`, `price_min` | Limits, checked against the exact variant returned. |
| `constraints.categories` | Any of: apparel, footwear, accessories, jewelry, bags-luggage, beauty, skincare, haircare, fragrance, health-wellness, home, kitchen, furniture, bedding-bath, electronics, audio, sports-outdoors, fitness, food-drink, pets, baby-kids, toys-games, books-media, office-stationery, garden, automotive, arts-crafts. |
| `constraints.attributes` | `color` (black, navy, white...) and `size` (XS-4XL, numbers, 32x30). |
| `limit` | 1 to 10. Default 5. At most 2 offers per brand. |

Response:

    {
      "search_id": "srch_...",
      "status": "ok",
      "inferred": { "price_max": 80, "currency": "USD", "color": "black" },
      "offers": [{
        "offer_id": "zko_...",
        "sponsored": true,
        "advertiser": "Example Brand",
        "title": "Oversized Heavyweight Hoodie",
        "description": "...",
        "price": 64, "currency": "USD", "price_display": "$64.00",
        "compare_at_price": 80, "compare_at_display": "$80.00", "discount_percent": 20,
        "approx_price": { "amount": 5340, "currency": "INR" },
        "variant": { "id": "4482...", "options": { "Color": "Black", "Size": "M" } },
        "availability": "in_stock",
        "price_as_of": "2026-10-06T04:00:00Z",
        "url": "https://store.example/products/hoodie?variant=4482...",
        "checkout_url": "https://agents-bench.zeekend.com/v1/go/zko_...",
        "cta": { "label": "Buy at Example Brand for $64.00", "url": "https://agents-bench.zeekend.com/v1/go/zko_..." },
        "image": "https://...",
        "match_reason": ["black", "oversized", "hoodie", "size_M_in_stock", "under_$80", "ships_US"]
      }],
      "disclosure": "Sponsored. Zeekend is paid if a purchase is made through checkout_url. ...",
      "ms": 31
    }

`price` is what the shopper pays, in the store's currency. `approx_price` is an
estimate in the shopper's currency, present only when the two differ.
`compare_at_*` appear only when the store shows the item on sale.

| `status` | Meaning |
| --- | --- |
| `ok` | Offers returned. |
| `no_match` | The search ran; nothing qualified. Normal. |
| `degraded` | Some offers may be missing (a secondary pass ran out of time); `degraded_reason` says why. |

### Other endpoints

| | |
| --- | --- |
| `GET /v1/agent/offers/{offer_id}` | Full details and every variant, with "was" prices. |
| `GET /v1/agent/offers/{offer_id}/availability` | Live price and stock from the store (400 ms limit; falls back to the index). |
| `GET /v1/agent/categories?country=IN` | Which categories have offers for a country. Use it to skip calls you know will be empty. |
| `GET /v1/go/{offer_id}` | The checkout link. Redirects to the store's cart. Offer ids expire after 7 days. |

All take `Authorization: Bearer <key>`, except `/v1/go`, which shoppers open.

### Errors

| Status | | What to do |
| --- | --- | --- |
| 400 | Bad request; the body says which field and why | Fix the call |
| 401 | Missing or unknown key | Check the key |
| 403 | Country not served yet | Skip Zeekend for that shopper |
| 429 | Rate limited; `retry-after` says when | Back off |
| 503 | `retryable: true`. Temporarily unavailable or overloaded | Retry once after a second, or skip |

Treat Zeekend as optional: on any error, show your other results. It is built
to fail fast (every request has a 650 ms server deadline) rather than make
your agent wait.

### Limits

60 requests a minute per key by default; ask for more. Every response carries
a `Server-Timing` header with the time spent in each stage.

---

## How you earn

Brands pay per order, at a price they set. When a shopper buys through an
offer's checkout link within 7 days, the order is credited to your key and you
earn a share of what the brand pays, held for the store's return window before
it is paid out.

The program is in beta: earnings reporting and payouts are being finalized
with the first partners. Contact hello@zeekend.com.

## For coding agents

Instructions an AI coding agent can follow to add Zeekend to a project:
`https://agents-bench.zeekend.com/agents/skill.md`
