Checking the wording yourself
Core checks the model's words with @rudra-js/attested. This page is about calling it on your own copy: what it catches, what it misses, and which facts to give it.
It has no dependencies and does no I/O, so it runs in Node, in an edge runtime and in a browser.
npm install @rudra-js/attestedThe two checks
verify runs two checks and reports each one apart. Core's checks run both on every line the model writes, after a pass of core's own that verify doesn't include. To check every field on a card at once, use verifyFields.
The number check
Every number written in digits has to be one you supplied, in any language. The check errs towards failing. “Only 2 left” fails unless a fact carries a 2, even when 2 is also the number of colours.
It reads:
- whole numbers and decimals, with a dot or a comma, such as
4.8and4,8 - currency symbols, attached or apart, and percentages
- digits in any script. Eastern Arabic
٤٫٨, Devanagari४.८and maths𝟰.𝟴all read as 4.8. - grouping written the Western way (
12,345,678), the Indian way (1,29,999), with spaces (1 299) or the Swiss way (1'299)
A dot or a comma with exactly three digits after it is always grouping. So 1,299 and 1.299 both read as 1299. A rating of 4.8 can't stand behind a written 4.800. Where grouping can't apply, the decimal reading stays, so 1234,567 is 1234.567. When a number uses both marks, the last one is the decimal point, so 1.299,50 is 1299.5.
It refuses to read three things, and each one fails:
- a number that isn't a digit, such as
½,²,②orⅢ - a multiplier stuck to the number, such as
4.8万,12Mor4.8k - a run no locale reads as a number, such as
24.12.2026. Only a fact written exactly the same way stands behind it.
Characters that take no room on the page are dropped first. So a zero-width space can't split 213 into a 2 and a 13 that you supplied apart. A character that does take room, such as a tab or a line break, still splits two numbers. “Only 2”, a line break and “3 left” is two numbers, and a fact of 23 doesn't stand behind it.
The word list
The second check looks for claims with no number in them, such as “bargain”, “selling fast”, “in stock” and “free delivery”. Every built-in phrase is English.
The list also holds generic environmental claims, such as “eco-friendly”, “carbon neutral” and “sustainable”. EU Directive 2024/825 applies through each country's own law from 27 September 2026. Under it, a generic environmental claim is unfair unless you can show recognised excellent environmental performance. A “neutral” claim based on offsetting is unfair outright. The bare word “green” isn't on the list, because it's so often a colour.
Matching ignores case and treats hyphens as spaces. It reads fullwidth letters, and the few Cyrillic and Greek letters that look Latin, as plain Latin. An accented letter matches however it was typed. Spaces don't matter, so 送料 無料 still matches 送料無料. A phrase that starts or ends with an ASCII letter or digit won't match inside a longer word, so “cheap” misses “cheapskate”. A trailing “s” still matches, so “discount” catches “discounts”. List any other form you want caught.
Adding your language
Add phrases of your own with bannedPhrases. They go on top of the built-in list.
import { verify } from '@rudra-js/attested';
const GERMAN_CLAIMS = ['nur noch', 'ausverkauft', 'solange der vorrat reicht', 'gratis versand'];
export function checkGermanCopy(copy: string, price: number) {
return verify(copy, { values: [price], bannedPhrases: GERMAN_CLAIMS });
}“Nur noch wenige” and “Fast ausverkauft” now fail. Without the list, “Gratis Versand” would pass.
Allowing wording you stand behind
Name the wording you stand behind in allowedPhrases. Three rules decide what an allowance clears:
- It works one way. Allowing “in stock” doesn't clear “back in stock”, which claims a restock on top.
- Allowances don't combine.
['sold', 'out']still leaves “sold out” caught. - It doesn't cross a line break. The list reads straight through one, so “Free”, a blank line and “shipping” is still caught. An allowance of “free shipping” won't clear it.
Wording you stand behind is caught shows an allowance at work.
Choosing the facts
Every number in your facts can stand behind every number in the copy. So give the check only the numbers a shopper may read.
import { verify } from '@rudra-js/attested';
const shoe = { sku: 'AT-2199', price: 174, rating: 4.8, listedAt: '2026-09-13' };
// wrong
export function checkedAgainstTheRecord(copy: string) {
return verify(copy, { values: [shoe.sku, shoe.price, shoe.rating, shoe.listedAt] });
}
// right
export function checkedAgainstWhatShoppersRead(copy: string) {
return verify(copy, { values: [shoe.price, shoe.rating, 5] });
}- Against the whole record, “Was $2,199, now $174” passes, because the SKU supplies 2199. “13 sold today” passes too, because the listing date supplies 13.
- The second list holds the price, the rating and the 5 it's out of. Both of those lines fail against it. “Rated 4.8 out of 5, for $174” passes.
A few more things decide what a fact supplies:
- A
nullorundefinedfact supplies nothing, and doesn't throw. - A
Datehanded straight through goes throughString(), so it supplies the day, the year and every digit of the clock. - Past 2^53, a JavaScript number has already lost digits. Pass a long number as a bigint or a string.
What it can't catch
The number check proves a number came from you. It doesn't prove the sentence uses it right, and it never sees a number written as a word. verify's caveats cover both. It also misses:
- a multiplier written as a word, such as
4,8 Millionenor4.8 लाख - the unit on a number.
¥39.99passes against a price of $39.99. - a number inside a fact. A fact of
'2026-09-13'supplies 2026, 9 and 13 everywhere, even as a stock count. - where a number was. A finding names the token, not its place in the text.
The word list misses:
- a careful rewording, such as “Half of what it was” for half price
- any language you haven't given it phrases for
- a character that folds into an allowance. Allow “no sale”, and “№ SALE” passes, because № folds to “no”.
- a letter stuck to the end of a phrase. “FREE SHIPPINGᵃ see terms” isn't reported, because the superscript folds to an “a” and the phrase reads as a longer word.
Two more things are out of its reach:
- A font that disagrees with Unicode about what takes no room. U+3164 HANGUL FILLER is the one people paste as a blank. The check reads 1, U+3164, 3 as 13. A font that gives it width shows the shopper “1 3”.
- Any claim that isn't a number or a phrase, such as one about materials, a certification or a warranty.
True copy that fails
The check errs towards failing, and some true copy pays for that:
- Every number has to be in your facts, even one that makes no claim. A true sentence fails because of a number in it has the fix.
- Three digits after a dot or a comma always read as grouping, so
1.299 kgreads as 1299. Supply a three-decimal value, and write it, the same way. 2 m²and½ inchfail.- A multiplier letter stuck to a digit fails, as in
4K display.4 K displayand1kgpass, because a letter followed by another letter reads as a unit. - A number in exponent form never passes in the copy. A fact of
1e-7is laid out as0.0000001, and1e-7 greads as the two numbers 1 and 7. Write0.0000001 ginstead.