getreadystack's picture
Upload index.html with huggingface_hub
e5bed20 verified
Raw History Blame Contribute Delete
47.9 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>BIPA Consent form — check biometric consent and retention</title>
<meta name="description" content="This browser tool audits code that captures biometric data for missing consent and destruction schedules, excess retention, and third party disclosure.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/biometric-consent-audit"></head>
<body>
<div class="wrap">
<h1>BIPA Consent form — check biometric consent and retention</h1>
<p class="lede">Audits code that captures faces, fingerprints or voiceprints: missing written consent, missing destruction schedule, retention past the Illinois BIPA three-year cap, third-party disclosure. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">"""Face-login enrolment service. Consent gate is in place - the rest is not."""
import requests
import face_recognition
import sqlite3
BIPA_POLICY_URL = "https://example.com/legal/biometric-retention"
RETENTION_DAYS = 1460
ENROLLED_AT = "2024-01-15"
VENDOR = "https://match.vendor-api.example/v2/verify"
def has_consent(user_id):
"""Signed written release, recorded at sign-up."""
return bool(LOOKUP.get(user_id))
def enroll(user_id, frame, db_path):
if not has_consent(user_id):
raise PermissionError("no signed release on file")
codes = face_recognition.face_encodings(frame)
if not codes:
return None
tpl = codes[0].tobytes()
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("INSERT INTO face_templates(user_id, blob) VALUES (?, ?)", (user_id, tpl))
conn.commit()
reply = requests.post(VENDOR, json={"template": tpl.hex(), "user": user_id}, timeout=8)
return reply.json()
def score_applicant(frame):
mood = detect_emotion(frame)
bucket = predict_ethnicity(face_recognition.face_encodings(frame)[0])
return {"mood": mood, "segment": bucket}
def build_training_set(seed):
rows = []
for url in scrape_face_urls(seed):
rows.append(url)
return rows</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="buy" style="margin:14px 0;padding:14px 16px;border:1px solid #e3d9cf;border-radius:10px;background:#fffdf9">
<strong>Full version: scan the whole workspace and export a dated evidence report (Markdown + CSV) per file and per statute subsection.</strong><br>
<a href="https://getreadystack.com/api/buy/cl/polar_cl_ymzWuO9GRzRMb8YULAoIhzs4TnxmfVlQxtxEI0ax9oq" style="display:inline-block;margin-top:8px;font:600 15px system-ui;padding:10px 18px;border-radius:8px;background:#1a1512;color:#fff;text-decoration:none">Get the full version &mdash; $29</a>
<div style="font:400 13px/1.5 system-ui;color:#5b524a;margin-top:8px">$29 once &middot; one licence key per person or team seat &middot; US privacy counsel bills $400-$600 an hour for a pre-launch review of biometric code.</div>
</div>
<div id="paid"><strong>Free here:</strong> Audits the file you have open and names every biometric consent, destruction-schedule and retention gap with the statute subsection for each finding.. <strong>In the editor:</strong> Scans the whole workspace and exports a dated evidence report (Markdown + CSV) per file and per statute subsection. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var BIO_RULES = [
{"check": "capture_without_consent", "sev": "high", "law": "740 ILCS 14/15(b)",
"msg": "Biometric identifier captured with no written-release gate anywhere in this file. BIPA 15(b) requires a signed written release BEFORE collection."},
{"check": "consent_after_capture", "sev": "high", "law": "740 ILCS 14/15(b)",
"msg": "The consent check runs after the capture call. BIPA 15(b) requires the written release before the identifier is collected, not after."},
{"check": "missing_destruction_schedule", "sev": "high", "law": "740 ILCS 14/15(a)",
"msg": "No retention or destruction constant in a file that captures biometrics. BIPA 15(a) requires a written schedule for permanent destruction."},
{"check": "retention_over_bipa_cap", "sev": "high", "law": "740 ILCS 14/15(a)",
"msg": "Declared retention exceeds the BIPA outer limit of 3 years (1095 days) after the last interaction.",
"bipa_max_days": 1095, "cubi_max_days": 365},
{"check": "retention_over_cubi_year", "sev": "med", "law": "Tex. Bus. \u0026 Com. Code 503.001(c-2)",
"msg": "Declared retention exceeds the Texas CUBI limit of 1 year (365 days) after the purpose expires."},
{"check": "destruction_window_elapsed", "sev": "high", "law": "740 ILCS 14/15(a)",
"msg": "A hardcoded enrolment date is already past the BIPA 3-year destruction window as of the audit date."},
{"check": "template_stored_unencrypted", "sev": "high", "law": "740 ILCS 14/15(e)",
"msg": "Biometric template written to storage with no encryption on or above this line. BIPA 15(e) requires the same care as other confidential data."},
{"check": "template_sent_third_party", "sev": "high", "law": "740 ILCS 14/15(d)",
"msg": "Biometric template leaves the process in a network call. BIPA 15(d) bars disclosure without a separate, specific consent."},
{"check": "profit_from_biometrics", "sev": "high", "law": "740 ILCS 14/15(c)",
"msg": "Code path sells, licenses or monetises biometric data. BIPA 15(c) bars profiting from it, with or without consent."},
{"check": "emotion_inference", "sev": "high", "law": "EU AI Act Art. 5(1)(f)",
"msg": "Emotion or affect inference from a biometric signal. Prohibited in workplace and education contexts in the EU since 2 February 2025."},
{"check": "sensitive_categorisation", "sev": "high", "law": "EU AI Act Art. 5(1)(g)",
"msg": "Biometric categorisation inferring race, religion, political views or sexual orientation. Prohibited in the EU since 2 February 2025."},
{"check": "untargeted_face_scraping", "sev": "high", "law": "EU AI Act Art. 5(1)(e)",
"msg": "Untargeted scraping of facial images to build a dataset. Prohibited in the EU since 2 February 2025."},
{"check": "employee_biometric_no_notice", "sev": "med", "law": "Colorado HB24-1130",
"msg": "Employee-facing biometric capture with no notice or consent record. Colorado has required both since 1 July 2025."},
{"check": "missing_public_policy_link", "sev": "med", "law": "740 ILCS 14/15(a)",
"msg": "No publicly available biometric retention policy referenced in this file. BIPA 15(a) requires that policy to be public."}
]
;</script>
<script>// Biometric Consent Audit - the whole brain lives here.
// The same file runs in Node (the VS Code extension) and in the browser (the free web tool).
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.BIO_RULES;
var RULE_COUNT = RULES.length;
var BY = {};
for (var i = 0; i < RULES.length; i++) { BY[RULES[i].check] = RULES[i]; }
var CAP = BY.retention_over_bipa_cap.bipa_max_days; // 1095 - BIPA 15(a) outer limit
var CUBI = BY.retention_over_bipa_cap.cubi_max_days; // 365 - Texas CUBI 503.001(c-2)
// snake_case and camelCase are both spelled out on purpose: the same code ships in .py and .ts
var RX = {
capture: /\b(face_recognition|face_encodings|faceEncodings|detect_faces|detectFaces|compare_faces|compareFaces|search_faces_by_image|searchFacesByImage|face_mesh|faceMesh|FaceDetection|face_landmarks|faceLandmarks|fingerprint_template|fingerprintTemplate|minutiae|voiceprint|voicePrint|speaker_id|speakerId|speaker_verify|speakerVerify|enroll_voice|enrollVoice|iris_scan|irisScan|retina_scan|retinaScan|palm_scan|palmScan)\b/,
consent: /\b(biometric_consent|biometricConsent|has_consent|hasConsent|consent_granted|consentGranted|written_release|writtenRelease|release_signed|releaseSigned|require_consent|requireConsent)\b/,
template: /(templates?|encodings?|embeddings?|faceprints?|voiceprints?|biometrics?|descriptors?|\btpl\b)/i,
store: /\b(open|write_bytes|writeFileSync|writeFile|save|dump|insert_one|insertOne|execute|setItem|put_object|putObject|to_csv|upload_file)\s*\(|\bINSERT\s+INTO\b/i,
encrypt: /\b(encrypt|encrypted|encrypt_bytes|Fernet|AES|GCM|KMS|kms_key|libsodium|nacl|sealed_box|argon2|bcrypt|hmac|sha256)\b/i,
send: /\b(requests\.(?:post|put)|httpx\.(?:post|put)|axios\.(?:post|put)|session\.post|urlopen|fetch)\s*\(/,
profit: /\b(sell|resell|monetize|monetise|ad_targeting|adTargeting|license_dataset|licenseDataset|data_broker|dataBroker|marketplace_upload)[A-Za-z0-9_]*/i,
emotion: /\b(emotion|emotions|detect_emotion|detectEmotion|affect_recognition|affectRecognition|mood_detect|moodDetect|stress_level|stressLevel|engagement_score|engagementScore)[A-Za-z0-9_]*/i,
sensitive: /\b(ethnicity|predict_ethnicity|race_prediction|racePrediction|predict_race|predictRace|religion|sexual_orientation|sexualOrientation|political_affiliation|politicalAffiliation)[A-Za-z0-9_]*/i,
scrape: /\b(scrape|scraper|scrape_faces|crawl|crawler|spider|bulk_download|bulkDownload)[A-Za-z0-9_]*/i,
images: /(faces?|photos?|images?|avatars?|selfies?|headshots?)/i,
employee: /\b(employee|employees|timeclock|time_clock|timeClock|attendance|shift_start|shiftStart|worker|workers|payroll)[A-Za-z0-9_]*/i,
notice: /\b(bipa_notice|bipaNotice|consent_record|consentRecord|notice_version|noticeVersion|release_signed_at|releaseSignedAt|hb1130_notice)\b/,
policy: /\b(BIPA_POLICY_URL|POLICY_URL|retention_policy_url|retentionPolicyUrl|biometric_policy|biometricPolicy|privacy_policy|privacyPolicy)\b/i,
retention: /\b([A-Za-z0-9_]*(?:retention|retain|destroy|destruction|purge|ttl|keep_)[A-Za-z0-9_]*)\s*[:=]\s*(\d[\d_]*)\b/i,
encAssign: /\b([A-Za-z0-9_]+)\s*=\s*[^=;]*\b(?:encrypt|encrypted|encrypt_bytes|Fernet|AES|GCM|KMS|kms_key|libsodium|nacl|sealed_box|argon2|bcrypt|hmac|sha256)/i,
noise: /^\s*(?:#|\/\/|\*|\/\*|from\s+[\w.]+\s+import|import\s+[\w.]+|require\s*\()/,
enrolled: /\b([A-Za-z0-9_]*(?:enrolled|enrol_|last_interaction|lastInteraction|captured_at|capturedAt|collected_at|collectedAt)[A-Za-z0-9_]*)\s*[:=]\s*['"](\d{4}-\d{2}-\d{2})['"]/i
};
function toDays(name, n) {
if (/year/i.test(name)) { return n * 365; }
if (/month/i.test(name)) { return n * 30; }
if (/hour/i.test(name)) { return Math.round(n / 24); }
if (/week/i.test(name)) { return n * 7; }
return n; // bare numbers are read as days
}
function daysBetween(fromIso, toIso) {
var a = Date.parse(fromIso + 'T00:00:00Z');
var b = Date.parse(toIso + 'T00:00:00Z');
if (isNaN(a) || isNaN(b)) { return null; }
return Math.round((b - a) / 86400000);
}
function check(text, opts) {
opts = opts || {};
var today = String(opts.today || '').slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(today) || isNaN(Date.parse(today + 'T00:00:00Z'))) {
today = '';
}
var lines = String(text == null ? '' : text).split(/\r?\n/);
var out = [];
function add(id, line, extra) {
var r = BY[id];
out.push({ check: id, sev: r.sev, line: line, msg: r.msg + (extra ? ' ' + extra : '') + ' [' + r.law + ']' });
}
var firstCapture = 0, firstConsent = 0, hasNotice = false, hasPolicy = false, employeeLine = 0;
var declared = null, declaredLine = 0, declaredName = '';
var encVars = [];
for (var i = 0; i < lines.length; i++) {
var L = lines[i], n = i + 1;
if (RX.noise.test(L)) { continue; }
if (!firstCapture && RX.capture.test(L)) { firstCapture = n; }
if (!firstConsent && RX.consent.test(L)) { firstConsent = n; }
if (RX.notice.test(L)) { hasNotice = true; }
if (RX.policy.test(L)) { hasPolicy = true; }
if (!employeeLine && RX.employee.test(L)) { employeeLine = n; }
var ev = RX.encAssign.exec(L);
if (ev) { encVars.push(ev[1]); }
var m = RX.retention.exec(L);
if (m && declared === null) {
declared = toDays(m[1], parseInt(String(m[2]).replace(/_/g, ''), 10));
declaredLine = n; declaredName = m[1];
}
}
if (!firstCapture) { return { findings: [], rule_count: RULE_COUNT }; }
if (!firstConsent) { add('capture_without_consent', firstCapture); }
else if (firstConsent > firstCapture) {
add('consent_after_capture', firstCapture, 'Capture is on line ' + firstCapture + ', the release check on line ' + firstConsent + '.');
}
if (declared === null) { add('missing_destruction_schedule', firstCapture); }
else if (declared > CAP) {
add('retention_over_bipa_cap', declaredLine, declaredName + ' = ' + declared + ' days, which is ' + (declared - CAP) + ' days over the 1095-day limit.');
} else if (declared > CUBI) {
add('retention_over_cubi_year', declaredLine, declaredName + ' = ' + declared + ' days, which is ' + (declared - CUBI) + ' days over the 365-day Texas limit.');
}
for (var j = 0; j < lines.length; j++) {
var ln = lines[j], num = j + 1;
var guarded = RX.encrypt.test(ln);
for (var k = 0; k < encVars.length && !guarded; k++) {
if (new RegExp('\\b' + encVars[k] + '\\b').test(ln)) { guarded = true; }
}
if (RX.store.test(ln) && RX.template.test(ln) && !guarded) {
add('template_stored_unencrypted', num);
}
if (RX.send.test(ln) && RX.template.test(ln)) {
add('template_sent_third_party', num);
}
if (RX.profit.test(ln) && RX.template.test(ln)) {
add('profit_from_biometrics', num);
}
if (RX.emotion.test(ln)) { add('emotion_inference', num); }
if (RX.sensitive.test(ln)) { add('sensitive_categorisation', num); }
if (RX.scrape.test(ln) && RX.images.test(ln)) { add('untargeted_face_scraping', num); }
var e = RX.enrolled.exec(ln);
if (e && today) {
var age = daysBetween(e[2], today);
if (age !== null && age > CAP) {
add('destruction_window_elapsed', num, e[1] + ' = ' + e[2] + ' is ' + age + ' days before ' + today + ', i.e. ' + (age - CAP) + ' days past destruction.');
}
}
}
if (employeeLine && !hasNotice) { add('employee_biometric_no_notice', employeeLine); }
if (!hasPolicy) { add('missing_public_policy_link', firstCapture); }
out.sort(function (a, b) { return a.line - b.line; });
return { findings: out, rule_count: RULE_COUNT };
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULE_COUNT };
if (typeof module !== 'undefined') { module.exports = API; }
if (typeof window !== 'undefined') { window.BIOENGINE = API; }
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var BIOREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = BIOREPORT;
else window.BIOREPORT = BIOREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.BIOENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.BIOREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.biometric-consent-audit" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'biometric-consent-audit',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/biometric-consent-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'biometric-consent-audit',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/biometric-consent-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'biometric-consent-audit',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://hub.docker.com/r/getreadystack/biometric-consent-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'biometric-consent-audit',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.biometric-consent-audit
npx @readystack/biometric-consent-audit &lt;file&gt;
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/biometric-consent-audit /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 14 rules Biometric Consent Audit (BIPA / CUBI / CO) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"biometric-consent-audit",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">14 rules over face, fingerprint and voiceprint code: written release, destruction schedule, the 1095-day cap and the three EU AI Act prohibitions</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">US privacy counsel bills roughly $400-$600 an hour for a pre-launch review of biometric code</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_ymzWuO9GRzRMb8YULAoIhzs4TnxmfVlQxtxEI0ax9oq" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-22">Updated 2026-09-22</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-22","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku175316_result_card.jpg" alt="Biometric Consent Audit (BIPA / CUBI / CO)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Six statute gaps sat in a 45-line Python file that a US app team had already fixed once: the consent gate was there, the written release was checked before the first capture, and the audit still returned six findings.</p>
<p>The file is the enrolment path of a face login: face_encodings() on a frame, keep the template, store it, send it to a matching vendor. Nothing exotic. All six findings are lines a statute reaches directly.</p>
<p>RETENTION_DAYS = 1460. BIPA 15(a) requires a written destruction schedule, and its outer limit is three years after the last interaction - 1095 days. The declared value is 365 days over it. Texas CUBI, Tex. Bus. &amp; Com. Code 503.001(c-2), requires destruction within one year, 365 days. One constant, two ceilings, and the code obeys neither.</p>
<p>The template is written to SQLite with no encryption on the value. BIPA 15(e) requires storage using the reasonable standard of care in the industry, and at least as protectively as other confidential and sensitive information. A raw face encoding in a table column is not that.</p>
<p>The template is then posted to a vendor endpoint. BIPA 15(d) bars disclosure or dissemination of a biometric identifier without consent that is specific to the disclosure. The release the user signed at sign-up covered collection, not onward transfer.</p>
<p>The last three findings are not American. The EU AI Act&#x27;s Article 5 prohibitions have applied since 2 February 2025. Art. 5(1)(f) prohibits inferring emotions from biometric data at work or in education - the file&#x27;s detect_emotion() call sits in an applicant-scoring function. Art. 5(1)(g) prohibits biometric categorisation inferring race, political opinions, religion or sexual orientation; predict_ethnicity() is that. Art. 5(1)(e) prohibits building face databases by untargeted scraping, which scrape_face_urls() does. These are prohibitions, not risk tiers.</p>
<p>None of this is a style question, so a formatter has no opinion on it: it has no concept of a written release and cannot tell a hashed template from a raw one. A chatbot will summarise BIPA fluently without reading your file, without line numbers, and drifts on which subsection and which effective date apply. The dates are the part that moves: Colorado&#x27;s HB24-1130 duties took effect 1 July 2025, the EU prohibitions on 2 February 2025.</p>
<p>What the numbers are worth: BIPA sets liquidated damages of $1,000 for each negligent violation and $5,000 for each intentional or reckless one, plus attorney&#x27;s fees, and Illinois courts read a violation per person. Texas CUBI is enforced by the Attorney General at up to $25,000 per violation. EU AI Act penalties for a prohibited practice reach EUR 35 million or 7% of worldwide annual turnover.</p>
<p>The audit is 14 rules, offline, on the file. Two are arithmetic rather than pattern matching: the retention rule reports how many days a constant is over each ceiling, and the destruction-window rule compares a hardcoded enrolment date with the audit date. The same file, audited as of 22 September 2026, returns six findings; as of 1 June 2027 it returns seven, because the enrolment date in the file has by then passed the three-year window.</p>
<p>On the fixed version of the same service the audit returns zero findings: retention at 365 days, the template encrypted before the insert, the vendor call removed, the prohibited calls gone. One file, six lines, each with its subsection, in the editor, before the review.</p>
<p>A pre-launch review of biometric code by US privacy counsel bills at roughly $400 to $600 an hour.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku175316_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku175316_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Biometric Consent Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a Python, JavaScript or TypeScript file that captures a face, fingerprint, iris or voiceprint and reports the lines a biometric-privacy statute reaches: no written release before capture, no destruction schedule, retention past the 1095-day BIPA cap, an unencrypted template write, a template sent to a vendor, and the three uses the EU AI Act prohibits. Fourteen rules, each printed with its subsection.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Developers and technical leads shipping face or voice login, biometric time clocks, or identity verification for users in Illinois, Texas, Colorado or the EU. It is built for the person who has to answer a security questionnaire or a demand letter about biometric handling, and who needs the answer stated in line numbers and subsections.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why will a linter or a chatbot not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">ESLint and Ruff score style; they have no concept of a written release or a destruction schedule. A chatbot will summarise BIPA but does not read your file, keeps no line numbers, and is confidently wrong about which subsection applies and which dates are in force. This audit runs on your actual file, offline, and cites the subsection per line.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs the key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free, with no key: audit the file you have open and see every finding, line number and statute subsection. That is a complete job for one file. The full version adds a different axis: it scans the whole workspace at once and exports a dated evidence report, Markdown and CSV, grouped per file and per statute subsection.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A pre-launch review of biometric code by US privacy counsel bills at roughly $400 to $600 an hour, and the exposure it prices is statutory: BIPA sets $1,000 per negligent violation and $5,000 per intentional or reckless one, plus fees. The full version is $29 once,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Biometric Consent Audit (BIPA / CUBI / CO) checks the file you open against 14 rules, each naming its legal or official basis, from a rule set dated 2026-09-22, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Biometric Consent Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a Python, JavaScript or TypeScript file that captures a face, fingerprint, iris or voiceprint and reports the lines a biometric-privacy statute reaches: no written release before capture, no destruction schedule, retention past the 1095-day BIPA cap, an unencrypted template write, a template sent to a vendor, and the three uses the EU AI Act prohibits. Fourteen rules, each printed with its subsection."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Developers and technical leads shipping face or voice login, biometric time clocks, or identity verification for users in Illinois, Texas, Colorado or the EU. It is built for the person who has to answer a security questionnaire or a demand letter about biometric handling, and who needs the answer stated in line numbers and subsections."}}, {"@type": "Question", "name": "Why will a linter or a chatbot not do this?", "acceptedAnswer": {"@type": "Answer", "text": "ESLint and Ruff score style; they have no concept of a written release or a destruction schedule. A chatbot will summarise BIPA but does not read your file, keeps no line numbers, and is confidently wrong about which subsection applies and which dates are in force. This audit runs on your actual file, offline, and cites the subsection per line."}}, {"@type": "Question", "name": "What is free and what needs the key?", "acceptedAnswer": {"@type": "Answer", "text": "Free, with no key: audit the file you have open and see every finding, line number and statute subsection. That is a complete job for one file. The full version adds a different axis: it scans the whole workspace at once and exports a dated evidence report, Markdown and CSV, grouped per file and per statute subsection."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "A pre-launch review of biometric code by US privacy counsel bills at roughly $400 to $600 an hour, and the exposure it prices is statutory: BIPA sets $1,000 per negligent violation and $5,000 per intentional or reckless one, plus fees. The full version is $29 once,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Biometric Consent Audit (BIPA / CUBI / CO) checks the file you open against 14 rules, each naming its legal or official basis, from a rule set dated 2026-09-22, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:175316,slug:"biometric-consent-audit",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/age-gate-lint"><span class="yj-rel-t">Age Gate Lint — UK Online Safety Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/model-card-lint-gpai"><span class="yj-rel-t">Model Card Lint - EU GPAI (AI Act Art. 53)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/huggingface-model-license-matrix"><span class="yj-rel-t">Hugging Face Model Licence Matrix — all 70 model-card `license:` identifiers ×</span><span class="yj-rel-p">$4.99</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>