rudra-js

How it works

rudra-js turns one page view in your shop into one block of product recommendations. It is five packages, published together at one version.

The five packages

@rudra-js/core works out what goes in the block. You give it one page view, and it gives back a spec. A spec is a plain object that names the blocks to show, the words in them and the products in each one by SKU. If you give core a model, it asks the model to design the block. Either way, it checks the block against your catalog. It has no React in it.

@rudra-js/react renders the spec with React 18 or 19. Its components have no hooks and no state. They work as Server Components, with renderToString and in the browser.

@rudra-js/anthropic connects core to Claude through the Anthropic API. It's the only package that makes a billed call. It gives you a provider, the object core calls to reach a model. Any other model can be a provider too, through the ComponentProvider interface.

@rudra-js/attested checks copy against the facts you supply. Core runs it on every word the model writes. You can also call it yourself on copy from anywhere else. It has no dependencies.

@rudra-js/a2ui lets an A2UI agent place the block in the interfaces it builds. The agent hints at what the shopper is looking at. Your server still picks the products and builds the block with core.

From a page view to a block

Every page view goes through the same five steps.

  1. You describe the page view. parseTrackingInput takes the shopper, the page, the products you're willing to show and what the shopper has done. The products are the candidates. The things the shopper did, such as viewing, liking or buying a product, are the signals. Parsing fills in defaults and rejects any field that doesn't belong.
  2. Core picks the products this shopper can see. It leaves out any candidate that is out of stock, disliked, already bought, in the basket or on the page right now. It puts the rest in order by the shopper's signals. Pass rank: 'given' to keep the order you sent.
  3. The model designs the block, or core builds it. With a provider, core asks the model for a spec. The model picks the layout and writes the headings and the copy. By default it writes one spec for each cohort. With no provider, core builds the block itself.
  4. Core fits the block to the shopper and checks it. By default, core fills each grid and carousel with this shopper's products, best first. Then core's checks drop any product that isn't in your catalog and any line that makes a claim your data can't back.
  5. React renders it. RudraComponent turns the spec into HTML. It reads each product's title, price, image and link from your catalog as it renders.

The block core builds without a model

With no provider, core builds the block itself. It's one grid of the products core picked, best first. The same input always gives the same block. The headline is one of four:

  • “Popular right now”, for a shopper with no history.
  • “Goes with your cart”, when the basket has something in it.
  • “Picked for you”, when the shopper leans towards a category. The line under it names that category.
  • “You might also like”, for everyone else.

You also get this block when the model's answer can't be used. That happens when the model takes longer than modelTimeoutMs, the call fails, or the answer doesn't fit the schema. It also happens when core's checks leave the block with no headline or no product. In every case source is 'fallback'.

This block never holds a hero, a carousel, a banner, copy or a bundle. Those only come from a model. When no candidate is left to show, it holds nothing, and RudraComponent renders nothing.

Core's checks

Core checks every block a model wrote, each time it's served. There are two kinds of check.

The catalog check. The model names products by SKU and nothing else. It never writes a title, a price, an image or a link. Core drops any SKU that isn't one of your candidates, or is out of stock. It drops a product this shopper disliked or is looking at right now. Outside a bundle, it also drops one they bought or have in the basket. It drops a product placed twice. It stops adding products once the block holds context.maxItems. It keeps only the first four blocks.

The wording checks. Core reads every line the model wrote in three passes.

  • One looks for prices, discounts, ratings, delivery dates and stock counts.
  • One checks that every number in the line is in the tags or the category of a candidate the model was shown.
  • One looks for claims that carry no number, such as “top pick”.

A line that fails any pass is dropped. Two of the passes are word lists, so a reworded claim can get through. The last two passes are the number check and the word list from @rudra-js/attested. Checking the wording yourself covers what those two catch and what they miss.

Cohorts

A cohort is a group of page views that look the same to the model. Two page views are in the same cohort when they match on all of these:

  • the kind of page and the slot on it, context.surface and context.slot
  • the locale
  • the segment, when you set one
  • the category being browsed
  • the category the shopper leans towards most
  • whether the shopper has any history at all
  • how many products the block may hold
  • the set of candidate SKUs, in any order

In the default 'cohort' mode, the model writes one spec for each cohort. Core keeps it in the cache, and every page view in that cohort shares it. Core fills in each shopper's own products on every request. So one model call serves many page views. Pass generation: 'per-shopper' and the model writes a spec for every shopper instead.

Each new value in those fields makes a new cohort, and a new model call. Take the locale, the category and the segment from lists you control. Never copy them straight from a URL or a request header.

What the model decides

With a provider, the model and core split the work like this.

DecisionDefault modePer-shopper mode
Layout and block orderThe modelThe model
Headline, subheadline and copyThe modelThe model
Emphasis on each productThe modelThe model
Badge textCore drops itThe model
Which products, in what orderCore, for each shopperThe model, from your candidates
The line under each productCore, for each shopperThe model, checked against the shopper's signals
Which bundle, of the ones you passCore, for each shopperCore, for each shopper

In the default mode, the hero is the one block whose product the model picks. If this shopper can't be shown that product, core drops it from the hero and keeps the words.

With no provider, core decides everything. You get the block core builds without a model.

What the model sees

In the default mode, the model is sent the fields that make up the cohort. The candidates go as one line each, with the SKU, title, category, rating and tags. Core leaves out-of-stock candidates off that list. It sends at most 60, in the order you passed them.

In per-shopper mode, the model is also sent:

  • the SKU being looked at, and the current search
  • whether the shopper is returning
  • the SKUs they liked, disliked, bought and have in the basket
  • the SKUs they viewed most, each with its view count
  • their recent searches
  • up to six categories they lean towards, strongest first
  • the other kinds of interaction, each with a count

In both modes, the model is never sent:

  • user.id
  • a price or a currency
  • imageUrl
  • the timestamps (at) and dwell times (dwellMs) on signals
  • interaction.value or interaction.meta

For what this means for a privacy review, see Running it in a regulated shop.