Store trust check
Before buying from an online store you do not know, gather the signals that tell a real shop from a scam and score them, with every reason shown.
When to use it: You are about to buy from a store you do not know and want its warning signs scored, with every reason.
| Uses | a shell session (curl and jq for RDAP, openssl for the certificate), crawl, extract over the store's pages |
|---|---|
| Needs | shell sessions and a model on the API (no model key of your own) |
| Site | the stores you give it; the runner uses two stand-in stores |
| Output | output/report.md, output/result.json |
| Languages | Node and Python |
What it checks:
- From its pages (the home page and the contact, returns, privacy and terms pages it links to): who runs it (name, address, phone, company number), a returns policy and its window, a privacy policy, the ways to pay, pressure tactics (countdowns, "only 2 left") and discounts of 80% or more.
- From the machine's shell: when the domain was registered (RDAP, the registries' public API) and who issued its certificate.
Each warning sign adds points (payments you cannot dispute 3; no address, no returns policy, no returns (or a store that refuses them), pressure tactics or a domain under 90 days old 2; the others 1): 6 or more is high risk, 3 to 5 medium. It is a score from public signals, not a verdict.
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Store trust check: before buying from an online store you do not know, gather the signals that separate a real shop * from a scam and score them, with every reason shown: who runs it (name, address, phone), its returns and privacy * policies, how you can pay, pressure tactics and too-good discounts (read from its pages), how old its domain is * (RDAP) and its certificate (openssl), checked in a session's shell. * * npx tsx node/index.ts (BOXLINE_API_KEY; STORE_URLS comma-separated; a plan with shell sessions) * * It only reads public pages and registry data. Writes output/result.json and output/report.md. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const stores = (process.env.STORE_URLS ?? "https://books.toscrape.com/").split(",").map((u) => u.trim()).filter(Boolean);const bx = new Boxline();type Facts = { business: { name: string | null; address: string | null; phone: string | null; companyNumber: string | null }; returnsPolicy: { present: boolean; allowsReturns: boolean; window: string | null }; privacyPolicy: boolean; paymentMethods: string[]; pressureTactics: string[]; discountClaims: string[];};const POLICY = /contact|about|imprint|impressum|legal|return|refund|privacy|terms|shipping|faq/i;const session = await bx.sessions.create({ browser: false, shell: true, timeout: 600, idleTimeout: 300, userMetadata: { example: "store-trust-check" } });console.log(`Session: ${session.id}`);const results: unknown[] = [];let modelUsd = 0;try { for (const store of stores) { const host = new URL(store).hostname; const domain = host.split(".").slice(-2).join("."); // 1. The domain's registration date (RDAP, the registries' public API) and its certificate, from the machine. const rdap = await session.exec(`curl -sL --max-time 20 "https://rdap.org/domain/$DOMAIN" | jq -r '[.events[]? | select(.eventAction=="registration") | .eventDate][0] // empty'`, { env: { DOMAIN: domain }, timeoutMs: 60_000 }); const registered = rdap.stdout.trim() || null; const ageDays = registered ? Math.floor((Date.now() - Date.parse(registered)) / 86_400_000) : null; const tls = store.startsWith("https:") ? await session.exec(`echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null | openssl x509 -noout -issuer -enddate 2>/dev/null`, { env: { HOST: host }, timeoutMs: 60_000 }) : null; const issuer = tls?.stdout.match(/issuer=.*?O ?= ?([^,\n]+)/)?.[1]?.trim() ?? null; // 2. Its pages: the home page and the policy pages it links to (contact, returns, privacy, terms, …). const crawl = await bx.crawl.wait((await bx.crawl.start({ url: store, maxPages: 15, maxDepth: 1, format: "markdown" })).id, { timeoutMs: 5 * 60_000 }); const policyPages = crawl.data.filter((p) => p.url !== store && !p.error && (p.status ?? 0) < 400 && (POLICY.test(p.url) || POLICY.test(p.title ?? ""))).map((p) => p.finalUrl ?? p.url); const r = await bx.extract<Facts>({ urls: [store, ...policyPages].slice(0, 10), prompt: "These are an online store's pages. business: who runs it, as stated (null when not stated). returnsPolicy: whether a returns or refund policy is stated, whether it lets buyers return items for a refund, and its window. " + "privacyPolicy: whether a privacy policy is stated. paymentMethods: the ways to pay that are named. pressureTactics: countdowns, 'only N left', or similar pressure, quoted. " + "discountClaims: discounts stated, quoted. Report only what the pages say.", schema: { type: "object", properties: { business: { type: "object", properties: { name: { type: ["string", "null"] }, address: { type: ["string", "null"] }, phone: { type: ["string", "null"] }, companyNumber: { type: ["string", "null"] } }, required: ["name", "address", "phone", "companyNumber"] }, returnsPolicy: { type: "object", properties: { present: { type: "boolean" }, allowsReturns: { type: "boolean" }, window: { type: ["string", "null"] } }, required: ["present", "allowsReturns", "window"] }, privacyPolicy: { type: "boolean" }, paymentMethods: { type: "array", items: { type: "string" } }, pressureTactics: { type: "array", items: { type: "string" } }, discountClaims: { type: "array", items: { type: "string" } }, }, required: ["business", "returnsPolicy", "privacyPolicy", "paymentMethods", "pressureTactics", "discountClaims"], }, }); modelUsd += r.usage.costUsd; const f = r.data; // 3. The score: each warning sign adds points, and says why. const signals: { points: number; why: string }[] = []; const add = (points: number, why: string) => signals.push({ points, why }); if (!f.business.address) add(2, "no business address on its pages"); if (!f.business.phone) add(1, "no phone number"); if (!f.returnsPolicy.present) add(2, "no returns or refund policy"); else if (!f.returnsPolicy.allowsReturns) add(2, "the store refuses returns or refunds"); if (!f.privacyPolicy) add(1, "no privacy policy"); // Cards and wallets can be disputed with the bank; gift cards, crypto and bank transfers cannot. const reversible = f.paymentMethods.some((m) => !/gift/i.test(m) && /card|visa|mastercard|amex|paypal|apple pay|google pay|klarna/i.test(m)); if (f.paymentMethods.length && !reversible) add(3, `only payments you cannot dispute: ${f.paymentMethods.join(", ")}`); if (f.pressureTactics.length) add(2, `pressure tactics: ${f.pressureTactics.join("; ")}`); const bigDiscount = f.discountClaims.some((d) => Number(d.match(/(\d{2,3})\s*%/)?.[1] ?? 0) >= 80); if (bigDiscount) add(1, `discounts too good to be true: ${f.discountClaims.join("; ")}`); if (ageDays !== null && ageDays < 90) add(2, `the domain is ${ageDays} days old`); if (!store.startsWith("https:")) add(1, "no HTTPS"); const score = signals.reduce((n, s) => n + s.points, 0); const risk = score >= 6 ? "high" : score >= 3 ? "medium" : "low"; console.log(`\n${store}: ${risk.toUpperCase()} risk (${score} points)${f.business.name ? `, run by ${f.business.name}` : ""}`); for (const s of signals) console.log(` +${s.points} ${s.why}`); console.log(` domain registered: ${registered ?? "unknown"}${issuer ? `; certificate from ${issuer}` : ""}; ${policyPages.length} policy pages read`); results.push({ store, risk, score, signals, facts: f, domain: { name: domain, registered, ageDays }, certificateIssuer: issuer, pagesRead: [store, ...policyPages].slice(0, 10) }); }} finally { await session.stop();}const md = (results as { store: string; risk: string; score: number; signals: { points: number; why: string }[] }[]) .map((x) => [`## ${x.store}: ${x.risk} risk (${x.score} points)`, "", ...(x.signals.length ? x.signals.map((s) => `- +${s.points} ${s.why}`) : ["- no warning signs found"]), ""].join("\n")) .join("\n");mkdirSync(out, { recursive: true });writeFileSync(join(out, "report.md"), `# Store trust check\n\nA score from public signals, not a verdict: check anything that matters yourself.\n\n${md}`);writeFileSync(join(out, "result.json"), JSON.stringify({ stores: results, usage: { modelUsd } }, null, 2));"""Store trust check: before buying from an online store you do not know, gather the signals that separate a real shopfrom a scam and score them, with every reason shown: who runs it (name, address, phone), its returns and privacypolicies, how you can pay, pressure tactics and too-good discounts (read from its pages), how old its domain is(RDAP) and its certificate (openssl), checked in a session's shell. python python/main.py (BOXLINE_API_KEY; STORE_URLS comma-separated; a plan with shell sessions)It only reads public pages and registry data. Writes output/result.json and output/report.md."""import jsonimport osimport refrom datetime import datetime, timezonefrom pathlib import Pathfrom urllib.parse import urlparsefrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))stores = [u.strip() for u in os.environ.get("STORE_URLS", "https://books.toscrape.com/").split(",") if u.strip()]bx = Boxline()POLICY = re.compile(r"contact|about|imprint|impressum|legal|return|refund|privacy|terms|shipping|faq", re.I)SCHEMA = { "type": "object", "properties": { "business": {"type": "object", "properties": {"name": {"type": ["string", "null"]}, "address": {"type": ["string", "null"]}, "phone": {"type": ["string", "null"]}, "companyNumber": {"type": ["string", "null"]}}, "required": ["name", "address", "phone", "companyNumber"]}, "returnsPolicy": {"type": "object", "properties": {"present": {"type": "boolean"}, "allowsReturns": {"type": "boolean"}, "window": {"type": ["string", "null"]}}, "required": ["present", "allowsReturns", "window"]}, "privacyPolicy": {"type": "boolean"}, "paymentMethods": {"type": "array", "items": {"type": "string"}}, "pressureTactics": {"type": "array", "items": {"type": "string"}}, "discountClaims": {"type": "array", "items": {"type": "string"}}, }, "required": ["business", "returnsPolicy", "privacyPolicy", "paymentMethods", "pressureTactics", "discountClaims"],}results, model_usd = [], 0.0with bx.sessions.create(browser=False, shell=True, timeout=600, idle_timeout=300, user_metadata={"example": "store-trust-check"}) as session: print(f"Session: {session.id}", flush=True) for store in stores: host = urlparse(store).hostname or "" domain = ".".join(host.split(".")[-2:]) # 1. The domain's registration date (RDAP, the registries' public API) and its certificate, from the machine. rdap = session.exec("""curl -sL --max-time 20 "https://rdap.org/domain/$DOMAIN" | jq -r '[.events[]? | select(.eventAction=="registration") | .eventDate][0] // empty'""", env={"DOMAIN": domain}, timeout_ms=60_000) registered = rdap["stdout"].strip() or None age_days = (datetime.now(timezone.utc) - datetime.fromisoformat(registered.replace("Z", "+00:00"))).days if registered else None issuer = None if store.startswith("https:"): tls = session.exec('echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null | openssl x509 -noout -issuer -enddate 2>/dev/null', env={"HOST": host}, timeout_ms=60_000) m = re.search(r"issuer=.*?O ?= ?([^,\n]+)", tls["stdout"]) issuer = m.group(1).strip() if m else None # 2. Its pages: the home page and the policy pages it links to (contact, returns, privacy, terms, …). job = bx.crawl.start(store, max_pages=15, max_depth=1, format="markdown") crawl = bx.crawl.wait(job["id"], timeout=300) policy_pages = [p.get("finalUrl") or p["url"] for p in crawl["data"] if p["url"] != store and not p.get("error") and (p.get("status") or 0) < 400 and (POLICY.search(p["url"]) or POLICY.search(p.get("title") or ""))] r = bx.extract( urls=[store, *policy_pages][:10], prompt=( "These are an online store's pages. business: who runs it, as stated (null when not stated). returnsPolicy: whether a returns or refund policy is stated, whether it lets buyers return items for a refund, and its window. " "privacyPolicy: whether a privacy policy is stated. paymentMethods: the ways to pay that are named. pressureTactics: countdowns, 'only N left', or similar pressure, quoted. " "discountClaims: discounts stated, quoted. Report only what the pages say." ), schema=SCHEMA, ) model_usd += r["usage"]["costUsd"] f = r["data"] # 3. The score: each warning sign adds points, and says why. signals = [] def add(points, why): signals.append({"points": points, "why": why}) if not f["business"]["address"]: add(2, "no business address on its pages") if not f["business"]["phone"]: add(1, "no phone number") if not f["returnsPolicy"]["present"]: add(2, "no returns or refund policy") elif not f["returnsPolicy"]["allowsReturns"]: add(2, "the store refuses returns or refunds") if not f["privacyPolicy"]: add(1, "no privacy policy") # Cards and wallets can be disputed with the bank; gift cards, crypto and bank transfers cannot. reversible = any(not re.search(r"gift", m, re.I) and re.search(r"card|visa|mastercard|amex|paypal|apple pay|google pay|klarna", m, re.I) for m in f["paymentMethods"]) if f["paymentMethods"] and not reversible: add(3, f"only payments you cannot dispute: {', '.join(f['paymentMethods'])}") if f["pressureTactics"]: add(2, f"pressure tactics: {'; '.join(f['pressureTactics'])}") if any(int((re.search(r"(\d{2,3})\s*%", d) or [0, "0"])[1]) >= 80 for d in f["discountClaims"]): add(1, f"discounts too good to be true: {'; '.join(f['discountClaims'])}") if age_days is not None and age_days < 90: add(2, f"the domain is {age_days} days old") if not store.startswith("https:"): add(1, "no HTTPS") score = sum(s["points"] for s in signals) risk = "high" if score >= 6 else "medium" if score >= 3 else "low" run_by = f", run by {f['business']['name']}" if f["business"]["name"] else "" print(f"\n{store}: {risk.upper()} risk ({score} points){run_by}") for s in signals: print(f" +{s['points']} {s['why']}") print(f" domain registered: {registered or 'unknown'}" + (f"; certificate from {issuer}" if issuer else "") + f"; {len(policy_pages)} policy pages read") results.append({"store": store, "risk": risk, "score": score, "signals": signals, "facts": f, "domain": {"name": domain, "registered": registered, "ageDays": age_days}, "certificateIssuer": issuer, "pagesRead": [store, *policy_pages][:10]})md = ["# Store trust check", "", "A score from public signals, not a verdict: check anything that matters yourself.", ""]for x in results: md += [f"## {x['store']}: {x['risk']} risk ({x['score']} points)", ""] + ([f"- +{s['points']} {s['why']}" for s in x["signals"]] or ["- no warning signs found"]) + [""]out.mkdir(parents=True, exist_ok=True)(out / "report.md").write_text("\n".join(md))(out / "result.json").write_text(json.dumps({"stores": results, "usage": {"modelUsd": model_usd}}, indent=2, ensure_ascii=False))Inputs
Environment variables the example reads:
STORE_URLS, comma-separated.
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/store-trust-check. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
Every store has a risk and a score that adds up its signals. On the runner's stand-in stores: the ceramics shop with its address, returns and privacy policies is low risk; the "mega deals" page is high risk, with no address, a store that refuses refunds, payment only by bitcoin, gift cards or bank transfer, and a countdown among its reasons.
Source
github.com/Boxline-dev/examples/store-trust-check on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.