Home » Free ACP Payments Module » Product Feed

The ACP Product Feed: JSONL and CSV

The product feed is how agent platforms learn what your store sells before any conversation happens: a flat file of items with prices, availability, images, and seller details, published in the ACP feed format. The ACP Payment Module generates it directly from your products file, serving it live at GET /feed in JSONL or CSV, or printing it from a CLI for static publishing. Products with variants expand to one row per variant grouped by product, and rows missing platform-required fields are skipped with a logged warning instead of poisoning the feed.

Why the Feed Matters

Checkout endpoints make you buyable, the feed makes you findable. ACP platforms ingest merchant feeds ahead of time so their assistants can answer product questions, compare options, and quote prices without a live call to every merchant on every query. A merchant with working checkout endpoints but no feed is a store with no shelf: technically open, practically invisible. The feed is also where merchant-level trust signals live, your seller name, site, terms, and privacy policy, which platforms surface to shoppers and weigh when deciding whether a merchant is eligible for search placement and instant checkout.

The module's design keeps the feed honest by construction: it is generated from the same products file that prices carts and sessions, so the price a platform indexed and the price your checkout charges can only drift within your catalog cache window, not across separate systems. That single-source property is the quiet advantage of keeping the catalog in one file, as described in the products file reference.

Row Format

Each feed row is one sellable item. The per-item fields come from the product, the merchant fields from the feed block of your config:

{"item_id":"pro-team","title":"Pro License (Team (5 seats))",
 "description":"Full license with all features unlocked.",
 "url":"https://yourtool.com/pro","brand":"Acme",
 "image_url":"https://yourtool.com/img/pro.png",
 "price":"199.99 USD","availability":"in_stock",
 "seller_name":"Acme Tools Inc","seller_url":"https://yourtool.com",
 "target_countries":["US"],"store_country":"US",
 "is_eligible_search":true,"is_eligible_checkout":true,
 "group_id":"pro-license","listing_has_variations":true,
 "seller_tos":"https://yourtool.com/terms",
 "seller_privacy_policy":"https://yourtool.com/privacy"}

Note the price format: the feed uses decimal-plus-currency strings like 199.99 USD, per the feed specification, while carts and checkouts use integer cents, the module converts at the boundary so each consumer sees its native format. Availability maps from the product's available flag to in_stock or out_of_stock. The item_id is the sellable id, exactly the id a platform later puts into a checkout session, so feed and checkout agree by definition.

Variants Become Grouped Rows

A product with variants emits one row per variant, each priced from the variant, titled as the product plus variant name, and tied together with group_id set to the parent product id plus listing_has_variations: true. Platforms use the grouping to present one listing with options rather than three competing listings. A product without variants emits a single row under its own id and no group fields. This expansion mirrors precisely how purchasing works, variants are bought by variant id, so nothing a platform learns from the feed needs translation at checkout time.

Required Fields and the Skip Rule

Platforms reject feeds with incomplete rows, so the generator enforces requirements per row: a url (from the product, or built from your configured productUrlBase when the product has none), an image_url, a brand (from a per-product brand field, falling back to the feed config's brand), and, when the feed declares checkout eligibility, a seller terms-of-service URL. A row missing any of these is skipped, and a warning names the item and the exact missing fields, so the feed that ships is always fully valid and the gaps are visible in your logs rather than in a platform rejection email. The fix is usually one line in the catalog: add the image, set the brand once in config, or give the product a URL. Because unknown product fields pass through the catalog schema untouched, you can also enrich rows with extra feed columns, gtin for example, directly from the products file.

Serving It: The /feed Endpoint

The standalone server and Lambda handler expose the feed at GET /feed, returning JSONL by default and CSV with ?format=csv, content-typed accordingly. It reads through the same cached catalog source as everything else, so it is cheap to poll, and it requires no authentication, a product feed is public marketing data by nature, the sensitive surfaces (checkout, webhooks) are the authenticated ones, per the security model. If the feed config block is absent the endpoint returns a clear not-configured error and nothing else changes, the feed is strictly opt-in.

For platforms or workflows that want a file rather than an endpoint, the CLI prints the same output:

npm run feed          # JSONL to stdout
npm run feed -- --csv # CSV to stdout

Piping that into a file in CI and publishing it to a bucket or your CDN gives you a static feed URL that survives even when your server is down, a reasonable hardening step once a platform is actively pulling it. Either way, hand the URL to the platform's merchant onboarding, the file format is what the ACP specification's product feed spec defines.

CSV Details

The CSV output writes a fixed header of all feed columns, in a stable order, with proper quoting for commas, quotes, and newlines, and arrays (like target_countries) joined with a pipe character. Optional fields that a row lacks are empty cells rather than omitted columns, so every row parses against the same header, the property spreadsheet-based review processes care about. JSONL omits absent optional fields instead, which is the natural JSON convention, both encodings carry identical information.

Operating the Feed Over Time

The feed needs no maintenance beyond catalog hygiene, but two habits pay off. First, watch the skip warnings after catalog edits, a renamed image path or a new product missing its URL shows up there immediately, and a product silently missing from the feed is a product agents cannot recommend. Second, treat availability as a merchandising tool: flipping available to false pulls the row to out_of_stock on the next cache refresh while keeping the listing's history, the right move for pausing sales, as opposed to deleting the product and orphaning its id. Sellers thinking about how product titles and descriptions perform inside AI assistants are really doing a new kind of listing optimization, and the fundamentals from our ecommerce SEO guide, clear naming, specific descriptions, honest availability, transfer directly.

Eligibility Flags: Search vs Checkout

Two booleans in the feed config deserve a deliberate choice rather than defaults accepted blindly. is_eligible_search declares that platforms may surface your items in assistant answers, and is_eligible_checkout declares that they may complete purchases against your ACP endpoints. Running search-eligible but not checkout-eligible is a legitimate launch posture: your products become discoverable, assistants link shoppers to your product pages, and you turn on programmatic checkout only after your endpoint integration has been exercised with a platform partner. Declaring checkout eligibility also raises the bar on the row requirements (the terms-of-service URL becomes mandatory), which is the generator enforcing what platforms will demand anyway. Whichever posture you pick, keep the flags truthful, a feed that advertises checkout eligibility against endpoints that reject the platform's token is the fastest way to fail a merchant review.