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.
- You describe the page view.
parseTrackingInputtakes the shopper, the page, the products you're willing to show and what the shopper has done. The products are thecandidates. The things the shopper did, such as viewing, liking or buying a product, are thesignals. Parsing fills in defaults and rejects any field that doesn't belong. - 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. - 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.
- 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.
- React renders it.
RudraComponentturns 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.surfaceandcontext.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.
| Decision | Default mode | Per-shopper mode |
|---|---|---|
| Layout and block order | The model | The model |
| Headline, subheadline and copy | The model | The model |
| Emphasis on each product | The model | The model |
| Badge text | Core drops it | The model |
| Which products, in what order | Core, for each shopper | The model, from your candidates |
| The line under each product | Core, for each shopper | The model, checked against the shopper's signals |
| Which bundle, of the ones you pass | Core, for each shopper | Core, 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.valueorinteraction.meta
For what this means for a privacy review, see Running it in a regulated shop.