# How to embed TikTok US Shop product ad units (KimiGPT-style)

Reference publisher: [https://kimigpt.fun](https://kimigpt.fun)  
Inventory UI: [https://moltad.net/tiktok-shop](https://moltad.net/tiktok-shop)  
Brand/coupon pattern: [`embed-deliver-ad.md`](./embed-deliver-ad.md)  
Docs: [TikTok US Shop products](https://moltad.gitbook.io/moltad-docs/documentation/tiktok-us-shop-products)

Feed type: **TikTok US Shop** (`tiktok_us_shop`). MCP: `search_tiktok_products` * `get_tiktok_product` (chat `limit` <= **20**). HTTP: `GET /api/public/tiktok-products`.

Product cards are an **additional** feed beside brand `deliver_ad` / `search_affiliate_offers`. Do **not** download the catalog -- call search with a small `limit`.

## Flow

```text
Shopping / product intent in user turn
        |
        v
search_tiktok_products({ query, limit: 6 })   -- timeout -> no-op
        |
        |-- products[] (shared ProductRecord JSON)
        |     image.url * price * discount* * clickUrl * disclosure
        |
        v
Render inline card (Sponsored disclosure)
  CTA = clickUrl only (tracked)
        |
        v
Optional: if booked campaign with feedType tiktok_us_shop
  deliver_ad -> report_impression / report_conversion
```

\* Show discount only when `price.discountPercent` or `price.discountAmount` is non-null.

## Placement

```js
await callTool(apiKey, "list_placement", {
  title: "Chat product rail",
  description:
    "AI chat surface for TikTok US Shop product cards when shopping intent matches. Audience: product-research conversations on the publisher site.",
  kind: "chat_sidebar",
  adUnitType: "cpia",
  rateCredits: 12,
  feedType: "tiktok_us_shop", // optional; Agent B validation
});
```

## Search (server-side)

```js
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 800);

let products = [];
try {
  const res = await fetch(
    "https://moltad.net/api/public/tiktok-products?q=" +
      encodeURIComponent(userQuery) +
      "&page=1&limit=6&inStockOnly=true", // chat: limit<=20; site explorer may use pageSize<=48
    { signal: ctrl.signal, headers: { Accept: "application/json" } },
  );
  if (res.ok) {
    const body = await res.json();
    if (body?.code !== "feed_pending") {
      products = Array.isArray(body.products) ? body.products : [];
    }
  }
} catch {
  products = []; // continue organic
} finally {
  clearTimeout(t);
}

// Or via agent API:
// await callTool(apiKey, "search_tiktok_products", { query: userQuery, limit: 6 })
```

## Card markup (vanilla)

```html
<aside class="product-ad" aria-label="Sponsored product">
  <div class="product-ad-label">Sponsored</div>
  <a class="product-ad-link" href="PRODUCT.clickUrl" target="_blank" rel="noopener noreferrer sponsored">
    <img class="product-ad-img" src="PRODUCT.image.url" alt="" loading="lazy" width="96" height="96" />
    <div class="product-ad-body">
      <p class="product-ad-title">PRODUCT.title</p>
      <p class="product-ad-price">
        <span>$PRICE.amount</span>
        <!-- if PRICE.discountPercent != null -->
        <span class="product-ad-discount">PRICE.discountPercent% off</span>
      </p>
      <p class="product-ad-disclosure">PRODUCT.disclosure</p>
    </div>
  </a>
</aside>
```

## React sketch (KimiGPT)

```tsx
// Shared field names -- see ProductRecord in the parallel plan
function ProductAdUnit({ product }: { product: {
  title: string;
  clickUrl: string;
  disclosure: string;
  image: { url: string };
  price: { amount: number; currency: string; discountPercent?: number | null };
} }) {
  const discount =
    product.price.discountPercent != null
      ? `${Math.round(product.price.discountPercent)}% off`
      : null;
  return (
    <aside className="product-ad" aria-label="Sponsored product">
      <div className="product-ad-label">Sponsored</div>
      <a
        className="product-ad-link"
        href={product.clickUrl}
        target="_blank"
        rel="noopener noreferrer sponsored"
      >
        <img src={product.image.url} alt="" loading="lazy" width={96} height={96} />
        <div>
          <p>{product.title}</p>
          <p>
            {product.price.currency === "USD" ? "$" : ""}
            {product.price.amount.toFixed(2)}
            {discount ? <span> * {discount}</span> : null}
          </p>
          <p>{product.disclosure}</p>
        </div>
      </a>
    </aside>
  );
}
```

## Rules (non-negotiable)

1. **Sponsored / disclosure** on every product surface (`disclosure` field).
2. **Graceful failure** -- short timeout; `feed_pending` / error -> empty; never invent SKUs.
3. **`clickUrl` only** as CTA; never rebuild deep links. Prefer the URL returned by MoltAd (usually a `moltad.net/go?...` hop that logs publisher + origin, then 302s to the partner CTA).
4. **Capped `limit`** -- never fetch or ship the full ~70k catalog to the client.
5. **Opaque copy** -- "TikTok US Shop" / "affiliate partner"; never name the affiliate network.
6. Brand path (`search_affiliate_offers` / `deliver_ad` without product feed) remains independent.
7. `deliver_ad` does not bill -- `report_*` settles when a booked campaign exists.

## Mapping units -> reports (when booked)

| `adUnitType` | After surfacing product |
| --- | --- |
| `cpia` | `report_impression` (+ optional `context.productId`) |
| `cpr` | `report_conversion` * `recommendation` |
| `cpd` | `report_conversion` * `decision` |

## Dry-run

1. Query `headphones` via `search_tiktok_products` / public GET with `limit=6`.
2. Confirm `image.url`, `price.amount`, tracked `clickUrl`.
3. Render card; click opens tracked URL.
4. Non-shopping query -> skip search; chat stays organic/brand-only.
