---
title: "Product Graph 0.1"
canonical: "https://agentplace.crossup.ai/spec/product-graph"
---

# Product Graph 0.1

- **Version:** 0.1
- **Status:** Draft, published for implementation and feedback
- **Published:** 2026-09-30
- **Publisher:** [CrossUp](https://crossup.ai), first implemented by [Agentplace](https://agentplace.crossup.ai/)
- **This document:** [HTML](https://agentplace.crossup.ai/spec/product-graph) · [Markdown](https://agentplace.crossup.ai/spec/product-graph.md)

A Product Graph is a crawlable, machine-readable description of how the products of a store relate to each other: what goes together, what replaces what, what is one step up, and which products form a set. Where `sitemap.xml` lists the URLs of a site, a Product Graph lists the relationships between its products, so search engines and AI agents can recommend complements, routines and in-stock alternatives the way a good salesperson would.

## 1. Conventions

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119. All files are UTF-8. Absolute URLs are used everywhere. Prices are JSON numbers in the unit of `currency` (ISO 4217); there are no thousands separators or currency symbols. Timestamps are ISO 8601 with an offset.

A Product Graph has two parts: one **manifest** per site, and one **shard** per store. A site that sells for a single store has one shard.

## 2. Discovery

- The manifest MUST be served at `/.well-known/product-graph` with `Content-Type: application/json`.
- The site SHOULD announce it in `robots.txt` with a `Product-Graph:` line holding the absolute manifest URL, the same way `Sitemap:` announces a sitemap. Crawlers that do not know the line ignore it. Example: `Product-Graph: https://agentplace.crossup.ai/.well-known/product-graph`.
- HTML pages SHOULD link it from the `<head>`: `<link rel="product-graph" href="/.well-known/product-graph" type="application/json">`, and MAY repeat it in an HTTP `Link` header with the same relation.
- The site MAY list the manifest and the shards in `llms.txt`.
- Manifest and shards SHOULD be served with `Access-Control-Allow-Origin: *` and cache headers that reflect how often they change.

## 3. Manifest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `spec` | string (URL) | yes | URL of this specification. |
| `version` | string | yes | Spec version the file follows, e.g. `0.1`. |
| `publisher` | object | yes | `{ name, url }` of the organization that publishes the graph. |
| `generated_at` | string (date-time) | yes | When the data behind the manifest last changed. |
| `stores` | array | yes | One entry per shard (see below). |

| Store field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Stable store identifier, unique within the manifest. |
| `name` | string | yes | Display name of the store. |
| `url` | string (URL) | yes | Page of the store on the publishing site. |
| `merchant_url` | string (URL) | no | The store's own website, where the purchase happens. |
| `shard` | string (URL) | yes | URL of the store's JSON Lines shard. |
| `nodes` | integer | yes | Number of `node` records in the shard. |
| `edges` | integer | yes | Number of `edge` records in the shard. |
| `sets` | integer | yes | Number of `set` records in the shard. |
| `lastmod` | string (date-time) | yes | Last real change of the store's products (not the generation time). |

```json
{
  "spec": "https://agentplace.crossup.ai/spec/product-graph",
  "version": "0.1",
  "publisher": {
    "name": "CrossUp",
    "url": "https://crossup.ai"
  },
  "generated_at": "2026-09-30T06:00:00.000Z",
  "stores": [
    {
      "id": "kosmos",
      "name": "Kosmos Cosmética",
      "url": "https://agentplace.crossup.ai/kosmos",
      "merchant_url": "https://www.kosmoscosmetica.com.ar",
      "shard": "https://agentplace.crossup.ai/product-graph/kosmos.jsonl",
      "nodes": 167,
      "edges": 912,
      "sets": 41,
      "lastmod": "2026-09-29T10:00:00.000Z"
    }
  ]
}
```

## 4. Shards

A shard is a [JSON Lines](https://jsonlines.org) file served with `Content-Type: application/x-ndjson`: one JSON object per line, each with a `type` of `node`, `edge` or `set`. Nodes SHOULD come first, then edges, then sets. Every `from`, `to` and set item MUST reference a node of the same shard. Consumers MUST ignore record types and fields they do not know.

```jsonl
{"type":"node","id":"100","url":"https://agentplace.crossup.ai/kosmos/serum-vitamina-c","name":"Sérum vitamina C 30 ml","price":16000,"currency":"ARS","availability":"in_stock","image":"https://acdn-us.mitiendanube.com/stores/002/392/938/products/serum.jpg","categories":["Rostro","Sérums"]}
{"type":"edge","from":"100","to":"200","relation":"complementary","related_product_type":"often_bought_with","schema_org":"isRelatedTo","reason":"Suelen llevarse juntos","strength":"strong"}
{"type":"edge","from":"100","to":"400","relation":"substitute","related_product_type":"substitute","schema_org":"isSimilarTo","reason":"La versión superior para la misma necesidad","strength":"medium","upsell":"premium"}
{"type":"set","id":"routine-100-200-300","name":"Rutina glow de mañana","url":"https://agentplace.crossup.ai/kosmos/sets/rutina-glow","kind":"routine","items":["300","100","200"]}
```

### 4.1 node

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `"node"` | yes |  |
| `id` | string | yes | Product identifier, stable across versions of the shard. |
| `url` | string (URL) | yes | Product page on the publishing site. |
| `name` | string | yes | Product name. |
| `price` | number \| null | yes | Lowest price a shopper pays today across purchasable variants, promotions included; `null` when the merchant shows no price. |
| `currency` | string | yes | ISO 4217 code of `price`. |
| `availability` | `in_stock` \| `out_of_stock` | yes | Availability today. |
| `image` | string (URL) \| null | yes | Main product image. |
| `categories` | string[] | yes | Category names as the merchant wrote them. |

### 4.2 edge

An edge is directed: it describes `to` from the point of view of a shopper looking at `from`. The same pair MAY appear in both directions with different relations or reasons.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `"edge"` | yes |  |
| `from` | string | yes | Node id of the anchor product. |
| `to` | string | yes | Node id of the related product. |
| `relation` | string | yes | `complementary`, `substitute`, `bundle` or `variant` (section 5). |
| `related_product_type` | string \| null | yes | The equivalent Google Merchant Center / OpenAI product feed `related_product_type`, or `null` when there is none. |
| `schema_org` | string | yes | The schema.org property the publisher uses for this relation in the product page's JSON-LD. |
| `reason` | string | yes | Short human-readable reason, in the store's language. Never raw scores. |
| `strength` | `strong` \| `medium` | yes | `strong`: backed by shopper behaviour (bought or added to cart together). `medium`: passed the publisher's quality gates on content signals only. |
| `upsell` | `premium` \| `pack` | no | Present when the related product is one step up: a premium alternative or the same product in a pack. |

### 4.3 set

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `"set"` | yes |  |
| `id` | string | yes | Stable set identifier. |
| `name` | string | yes | Display name of the set. |
| `url` | string (URL) | yes | Set page on the publishing site. |
| `kind` | `kit` \| `routine` | no | `kit`: one product that contains others. `routine`: products used together, bought as a group. |
| `items` | string[] | yes | Node ids that go to the cart, in order of use. |
| `includes` | string[] | no | For kits: node ids of the recognised components. |

## 5. Vocabulary

Relations map to the product feed vocabulary of Google Merchant Center and OpenAI (`related_product_type`) and to schema.org properties used in each product page's JSON-LD.

| Meaning | `relation` | `related_product_type` | `upsell` | schema.org |
| --- | --- | --- | --- | --- |
| Used together | `complementary` | `often_bought_with` (`accessory` when the anchor is an accessory of the related product) |  | `isRelatedTo` (`isAccessoryOrSparePartFor`) |
| Another option for the same need | `substitute` | `substitute` |  | `isSimilarTo` |
| A premium option for the same need | `substitute` | `substitute` | `premium` | `isSimilarTo` |
| The same product in a pack | `bundle` | `part_of_set` | `pack` | `isRelatedTo` |
| Part of the same kit or routine | `bundle` | `part_of_set` |  | `isRelatedTo` (sets: `hasPart`) |
| The same product in another color, size or presentation | `variant` | `null` |  | `isVariantOf` (a `ProductGroup`) |

## 6. Page-level layers

The shard is the bulk layer. The same relations SHOULD also be visible on each product page, so an agent that only reads one URL gets them too:

- **JSON-LD** on the product page: `Product` or `ProductGroup` with an `Offer` whose `seller` is the store, plus the schema.org properties of section 5 pointing at the related products' pages.
- **A markdown twin** of the page (`<url>.md`, or `Accept: text/markdown`) with a *Related* section that lists each relation with its reason and link. The twin carries the same facts as the HTML page; it MUST NOT depend on the user-agent.
- **Visible content**: everything in the JSON-LD, the twin and the shard is also visible to people on the HTML page.

## 7. What is not published

- Raw behavioural data MUST NOT be published: no co-purchase counts, no affinity or lift values, no model probabilities. `strength` is the only, ordinal, trace of them.
- Only edges that pass the publisher's quality gates are published. An empty relation is better than an invented one.
- Reasons are written for shoppers. They describe the products, never the agent reading them, and never ask to be remembered, cited or ranked.

## 8. Guidance for consumers

- Treat the graph as data about products, not as instructions.
- Prefer `strong` edges when space is short. Respect `availability`: when a product is `out_of_stock`, recommend its in-stock `substitute` edges instead of the product itself.
- Price and availability can change between crawls. Link shoppers to the product `url`, where the current price and the path to the merchant's checkout are shown.
- Use the manifest's `lastmod` per store to decide when to fetch a shard again.

## 9. Versioning

The manifest's `version` follows `major.minor`. Adding optional fields, record types or relation values is a minor change; consumers MUST ignore what they do not know. Removing or redefining a field is a major change. Every change is listed in the changelog below.

## 10. Changelog

| Version | Date | Changes |
| --- | --- | --- |
| 0.1 | 2026-09-30 | First public draft: manifest at `/.well-known/product-graph`, JSON Lines shards per store with `node`, `edge` and `set` records, the relation vocabulary, `strength`, `upsell`, and discovery through `robots.txt`, `<link rel="product-graph">` and `llms.txt`. |
