Rudra-JS, LLM-designed product recommendations for React stores The open-source TypeScript library rudra-js, published at version 0.8.3 under the MIT license on npm, lets a language model design a product-recommendation block for React storefronts by returning a constrained spec rather than HTML, with @rudra-js/react rendering titles, prices, images and links from the merchant's own catalog at serve time. Because the model is never given a price and the spec has no field for one, rudra-js relies on that structural limit as its main defense, backed by three text checks — pattern matching for prices, discounts, ratings, delivery dates and stock counts, a digit check, and a list of numberless claims like "top pick" — that drop any failing field, though the project acknowledges rewordings such as "Four and a half stars from other hikers" pass all three. On the project's 500-shopper benchmark, the default mode makes 460 model calls per 1,000 page views, and the block ships in the first HTML response without client-side JavaScript; the public API may change before 1.0, and leaving the provider out lets @rudra-js/core build the block with no model and no API key. rudra-js is an open-source TypeScript library MIT you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it. It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for. If a language model writes raw HTML, it can make up a price or show a product you don't sell. So rudra-js never lets the model write HTML. The model returns a spec instead: a short description of the block, using only fields rudra-js defines. The model picks the layout. It writes the headline and the copy. There is no field for a price, a product name, an image or a link. The model names products only by SKU, and only from the list you give it. @rudra-js/react turns the spec into HTML. It reads each product's title, price, image and link from your catalog as the page is served. Anything the model wrote is rendered as escaped text. rudra-js also checks every word the model writes, in three passes: - Patterns for prices, discounts, ratings, delivery dates and stock counts. - A digit check. Every digit in a sentence must come from your own input, as a category or a tag on one of the products the model was shown. - A list of claims that carry no number to check, like "top pick" and "customer favourite". Any field that fails a pass is dropped. Two of the passes are word lists, so a rewording that's on neither list gets through. The digit check only reads digits, so it misses a number spelled out in words. "Four and a half stars from other hikers" gets past all three passes. The tests keep a list of rewordings that still pass, so you can go and read how big the gap is. That's why the real defence is structural. The model is never told a price, and the spec has nowhere to put one. The block is in the first HTML response and needs no client-side JavaScript. So a crawler that never runs JavaScript still reads it. On our 500-shopper benchmark, the default mode makes 460 model calls per 1,000 page views. Generating for every shopper would make 1,000. Status: 0.8.3 , early. It installs and it works. The Getting started below runs as a test on every commit. The public API might still change between minor versions before 1.0 . The changelog says when it does. You don't need an API key to start. Leave the provider out and @rudra-js/core builds the block itself, with no model. That's a fully supported setting, and the right one until you've settled on a model. npm install @rudra-js/core@latest @rudra-js/react@latest @rudra-js/attested@latest Use this one command for a new app and for an app that already uses rudra-js. It's also how you upgrade. If your app has @rudra-js/anthropic , add it to the command with @latest too. If it has @rudra-js/a2ui , use the install line in that package's README instead. That line also names the two A2UI packages. The @rudra-js packages are published together. Each one's peer ranges on the others start at its own version. With a bare package name, npm stays inside the range your package.json already has. So without @latest , an app on an older core can get an ERESOLVE error. Every package is under the @rudra-js scope. The unscoped rudra-js package on npm belongs to someone else, so include the @ . You don't need to install zod. @rudra-js/core brings zod 4 as a dependency. If your app is already on zod 4, it shares that copy. On zod 3, npm puts a zod 4 under core, and another under @rudra-js/anthropic if you use it. Your zod 3 can't mix with the schemas core exports. productSchema.array works, but your z.array productSchema won't. You need React 18 or 19. You don't need Server Components. @rudra-js/react is plain React with no hooks. It works with renderToString and in a client-rendered app. Where your framework supports Server Components, it works as one too. Render it on the server to put the block in the first HTML response. You also need Node 22.12 or later. Node 20 is end of life, so we don't build or test on it. js import { createComponentGenerator, parseTrackingInput } from '@rudra-js/core'; import { RudraComponent } from '@rudra-js/react'; const catalog = { sku: 'A-1', title: 'Cast iron skillet', category: 'Cookware', price: 39, currency: 'USD' }, { sku: 'A-2', title: 'Enamel dutch oven', category: 'Cookware', price: 89, currency: 'USD' }, { sku: 'A-3', title: 'Chef knife', category: 'Knives', price: 55, currency: 'USD' }, ; async function recommendations { // Passing no provider means no API key and no spend. // Core builds the block itself, and the same input always gives the same block. const generator = createComponentGenerator { provider: null } ; // Parsing fills in what you left out and rejects anything that doesn't belong. // Always pass the parsed candidates to the renderer, not your raw objects. const input = parseTrackingInput { user: { id: 'shopper-1' }, context: { surface: 'pdp', currentSku: 'A-1', currentCategory: 'Cookware' }, candidates: catalog, signals: { cart: { sku: 'A-2', at: Date.now } }, } ; const spec = await generator.generate input ; return