{"service":"FairPrint","version":"1.0","description":"FairPrint publishes independent, evidence-backed directories for consequential services and evaluates Terms & Conditions and usage policies against 10 consumer-rights criteria. Certification is free.","url":"https://fairprint.app","llms_txt":"https://fairprint.app/llms.txt","discovery":{"index":"https://fairprint.app/llms.txt","sitemap":"https://fairprint.app/sitemap.xml","robots":"https://fairprint.app/robots.txt","well_known_trust_manifest":"https://fairprint.app/.well-known/fairprint.json","agent_guidance_page":"https://fairprint.app/ai","note":"The sitemap is generated from the live registry, so certificate pages appear there as soon as a record is public. The sitemap is the authoritative list of crawlable pages; `public/llms.txt` is the annotated index that explains what each URL contains."},"public_surfaces":{"note":"Every public surface an agent can follow, grouped by the question it answers. All members are bare routes with no query string — pair `handles.verify` / `handles.renew` / `handles.lookup` with an identifier you already hold, and use the per-record `verifyUrl` / `renewUrl` fields where a record publishes them instead of building a prefill yourself. `format` is what the fetch will return: html, json, atom, or text.","groups":[{"key":"service","note":"What FairPrint is and how an agent should describe, submit to, or cite it.","members":[{"surface":"https://fairprint.app/api/llms-info","format":"json","methods":["GET","OPTIONS"],"self":true},{"surface":"https://fairprint.app/llms.txt","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/.well-known/fairprint.json","format":"json","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/ai","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/standards","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/apply","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/apply/ai","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/review","format":"json","methods":["POST","OPTIONS"]},{"surface":"https://fairprint.app/api/review-stream","format":"json","methods":["POST","OPTIONS"]},{"surface":"https://fairprint.app/api/analyze","format":"json","methods":["POST","OPTIONS"]},{"surface":"https://fairprint.app/analyze","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/extract-terms","format":"json","methods":["POST","OPTIONS"]}]},{"key":"registry","note":"The live public record of what has been reviewed: verdicts, freshness, the routes that qualify them, and the validator that answers 'is this claim valid now?'.","members":[{"surface":"https://fairprint.app/directory","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/recently-certified","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/report","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/changelog","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/feed","format":"json","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/api/report","format":"json","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/review","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/verify","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/certified/{slug}","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/certified-lookup","format":"json","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/api/badge/{id}","format":"json","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/badge-kit","format":"html","methods":["GET"]}]},{"key":"remedy","note":"The two actors a detected condition routes to: the seal holder who can refresh a record, the consumer who can nominate one.","members":[{"surface":"https://fairprint.app/renew","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/request","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/request","format":"json","methods":["GET","OPTIONS"]}]},{"key":"catalog","note":"The service-industry directory and its machine-readable reconciliation against the live registry.","members":[{"surface":"https://fairprint.app/industries","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/industries/{slug}","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/industries","format":"json","methods":["GET","OPTIONS"]}]},{"key":"discovery","note":"The indexes an agent should read before scanning pages, and the crawl policy that governs them.","members":[{"surface":"https://fairprint.app/sitemap.xml","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/robots.txt","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/api/blog/feed","format":"atom","methods":["GET","OPTIONS"]},{"surface":"https://fairprint.app/blog","format":"html","methods":["GET"]},{"surface":"https://fairprint.app/blog/{slug}","format":"html","methods":["GET"]}]}],"handles":{"verify":"https://fairprint.app/verify","renew":"https://fairprint.app/renew","lookup":"https://fairprint.app/api/certified-lookup","request":"https://fairprint.app/request"}},"service_directory":{"endpoint":"https://fairprint.app/industries","status":"launch categories open for provider nominations; provider assessments are being researched","methodology":{"stage":"research — published framework for the broader service assessments still in progress; not the certification scoring method","dimensions":[{"label":"Price","summary":"Total expected cost, required fees, renewals, financing, and common add-ons."},{"label":"Service","summary":"What is actually delivered, by whom, on what timeline, and with which limitations."},{"label":"Experience","summary":"Documented customer patterns, complaint handling, guarantees, and provider responses."},{"label":"Fine print","summary":"Cancellation, refunds, privacy, arbitration, liability, and unilateral policy changes."}],"perCategoryDetail":"Each category additionally publishes its own review_areas (for example, binding vs. non-binding estimates and post-loading fees for moving companies, or coverage exclusions and renewal price changes for home warranties).","note":"Domain-level directory transparency. Directory inclusion is not a recommendation and does not imply certification. Terms & Conditions certification is scored separately on the 10 criteria and 0-10 per-criterion scale documented under `scoring`."},"industries":[{"slug":"moving-companies","name":"Moving Companies","url":"https://fairprint.app/industries/moving-companies","description":"Compare movers on estimates, deposits, damage claims, cancellation terms, and the fees that can appear after loading day.","review_areas":["Binding vs. non-binding estimates","Deposits and cancellation","Damage and loss claims","Packing and accessorial fees","Insurance and valuation coverage","Complaint history and responses"],"service_examples":["Local movers","Long-distance movers","International relocation","Container moving","Packing services"],"relevant_fairprint_concerns":["No Hidden Fees","Clear Refunds","Easy Cancellation","Fair Dispute Process"],"providers":[{"name":"United Van Lines","website":"https://www.unitedvanlines.com/","service":"Full-service moving","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Mayflower","website":"https://www.mayflower.com/","service":"Household moving services","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Allied Van Lines","website":"https://www.allied.com/","service":"Residential moving services","policy_review_url":null,"full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+moving+companies+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=No+Hidden+Fees&concern=Clear+Refunds&concern=Easy+Cancellation&concern=Fair+Dispute+Process"},{"slug":"home-warranties-services","name":"Home Warranties & Services","url":"https://fairprint.app/industries/home-warranties-services","description":"Review coverage limits, exclusions, service-call charges, contractor access, renewals, and the practical path from claim to repair.","review_areas":["Coverage limits and exclusions","Service-call fees","Claim approval timelines","Contractor choice and availability","Renewal price changes","Cancellation and refunds"],"service_examples":["Home warranties","HVAC services","Plumbing","Electrical services","Appliance repair"],"relevant_fairprint_concerns":["Plain Language","No Hidden Fees","Auto-Renewal Notice","Easy Cancellation","Clear Refunds"],"providers":[{"name":"American Home Shield","website":"https://www.ahs.com/","service":"Home warranty plans","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Choice Home Warranty","website":"https://www.choicehomewarranty.com/","service":"Home warranty plans","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Cinch Home Services","website":"https://www.cinchhomeservices.com/","service":"Home warranty plans","policy_review_url":null,"full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+home+warranties+%26+services+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=Plain+Language&concern=No+Hidden+Fees&concern=Auto-Renewal+Notice&concern=Easy+Cancellation&concern=Clear+Refunds"},{"slug":"tax-bookkeeping","name":"Tax Preparation & Bookkeeping","url":"https://fairprint.app/industries/tax-bookkeeping","description":"Compare tax and bookkeeping services on transparent pricing, credentials, data handling, support, guarantees, and add-on fees.","review_areas":["Upfront and add-on pricing","Preparer credentials","Accuracy guarantees","Audit support","Financial-data privacy","Document retention and deletion"],"service_examples":["Tax filing","Small-business bookkeeping","Payroll services","Virtual CFO services","Tax resolution"],"relevant_fairprint_concerns":["Data Transparency","No Data Selling","No Hidden Fees","Right to Delete","Fair Dispute Process"],"providers":[{"name":"H&R Block","website":"https://www.hrblock.com/","service":"Tax preparation","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"QuickBooks","website":"https://quickbooks.intuit.com/","service":"Bookkeeping and accounting","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"TaxAct","website":"https://www.taxact.com/","service":"Tax preparation","policy_review_url":null,"full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+tax+preparation+%26+bookkeeping+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=Data+Transparency&concern=No+Data+Selling&concern=No+Hidden+Fees&concern=Right+to+Delete&concern=Fair+Dispute+Process"},{"slug":"legal-document-services","name":"Legal Document Services","url":"https://fairprint.app/industries/legal-document-services","description":"Understand what is included, what is not legal advice, who reviews the work, refund rules, recurring charges, and dispute terms.","review_areas":["Scope of service","Attorney involvement","Filing and government fees","Revision and refund policy","Subscription renewal","Disclaimers and dispute process"],"service_examples":["Business formation","Estate documents","Trademark filing","Divorce documents","Legal subscriptions"],"relevant_fairprint_concerns":["Plain Language","Clear Refunds","Auto-Renewal Notice","No Hidden Fees","Fair Dispute Process"],"providers":[{"name":"LegalZoom","website":"https://www.legalzoom.com/","service":"Legal document services","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Rocket Lawyer","website":"https://www.rocketlawyer.com/","service":"Online legal documents","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"US Legal Forms","website":"https://www.uslegalforms.com/","service":"Legal form templates","policy_review_url":null,"full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+legal+document+services+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=Plain+Language&concern=Clear+Refunds&concern=Auto-Renewal+Notice&concern=No+Hidden+Fees&concern=Fair+Dispute+Process"},{"slug":"online-education-careers","name":"Online Education & Career Services","url":"https://fairprint.app/industries/online-education-careers","description":"Compare courses, bootcamps, coaching, and job services on outcomes evidence, total cost, refunds, financing, access duration, and data use.","review_areas":["Outcome and placement claims","Total tuition and financing","Refund windows","Course-access duration","Credential recognition","Student-data use"],"service_examples":["Online courses","Coding bootcamps","Career coaching","Résumé services","Job-placement services"],"relevant_fairprint_concerns":["Plain Language","Clear Refunds","No Hidden Fees","Data Transparency","Change Notification"],"providers":[{"name":"Coursera","website":"https://www.coursera.org/","service":"Online learning platform","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"Udemy","website":"https://www.udemy.com/","service":"Online course marketplace","policy_review_url":null,"full_service_assessment_status":"researching"},{"name":"LinkedIn Learning","website":"https://www.linkedin.com/learning/","service":"Career skills training","policy_review_url":"https://fairprint.app/blog/linkedin-terms-review","full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+online+education+%26+career+services+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=Plain+Language&concern=Clear+Refunds&concern=No+Hidden+Fees&concern=Data+Transparency&concern=Change+Notification"},{"slug":"subscription-health-fitness","name":"Subscription Health, Fitness & Telehealth","url":"https://fairprint.app/industries/subscription-health-fitness","description":"Review recurring care and fitness services on clinical scope, privacy, medication and membership costs, renewals, cancellation, and continuity of care.","review_areas":["Clinical scope and eligibility","Medication and membership pricing","Sensitive-data sharing","Recurring-charge consent","Cancellation process","Continuity and follow-up care"],"service_examples":["Telehealth subscriptions","Weight-care programs","Therapy platforms","Fitness memberships","Wellness coaching"],"relevant_fairprint_concerns":["Data Transparency","No Data Selling","Auto-Renewal Notice","Easy Cancellation","Right to Delete"],"providers":[{"name":"Noom","website":"https://www.noom.com/","service":"Digital health subscription","policy_review_url":"https://fairprint.app/blog/noom-terms-review","full_service_assessment_status":"researching"},{"name":"Peloton","website":"https://www.onepeloton.com/","service":"Connected fitness membership","policy_review_url":"https://fairprint.app/blog/peloton-terms-review","full_service_assessment_status":"researching"},{"name":"Teladoc Health","website":"https://www.teladochealth.com/","service":"Virtual healthcare services","policy_review_url":null,"full_service_assessment_status":"researching"}],"nomination_url":"https://fairprint.app/request?reason=Please+review+this+subscription+health%2C+fitness+%26+telehealth+provider+for+pricing%2C+service+quality%2C+policies%2C+and+fine-print+risks.&concern=Data+Transparency&concern=No+Data+Selling&concern=Auto-Renewal+Notice&concern=Easy+Cancellation&concern=Right+to+Delete"}],"nomination_guidance":"Use the category nomination_url to open /request with category context and FairPrint concern labels prefilled. Add the provider name and direct website or policy URL before submitting.","editorial_boundaries":"Category inclusion is not a recommendation. Existing T&C-only reviews are linked when available and remain distinct from the broader service assessments still being researched."},"trust_manifest":{"endpoint":"https://fairprint.app/.well-known/fairprint.json","cors_enabled":true,"public_api_methods":["GET","OPTIONS"],"description":"Well-known machine-readable trust manifest for browser agents, procurement tools, and badge verifiers. It summarizes the independent service directory, free certification program, scoring tiers, criteria, canonical certificate/badge proof handles, badge adoption guide, verifier/lookup APIs, provider and policy nomination actions, and public-record privacy boundaries. It also carries a `discovery` block naming the index, sitemap, and robots documents an agent can enumerate the site from.","response":"{ schema, version, generatedAt, issuer, discovery, serviceDirectory, certification, regulatoryContext, proof, actions, privacy }. Public metadata only; no submitter emails, admin state, or payment metadata."},"submission":{"method":"POST","endpoint":"https://fairprint.app/api/review","content_type":"application/json","auth_required":false,"cors_enabled":true,"public_api_methods":["POST","OPTIONS"],"terms_import_helper":{"endpoint":"https://fairprint.app/api/extract-terms","web_forms":["https://fairprint.app/apply","https://fairprint.app/apply/ai","https://fairprint.app/review"],"method":"POST","auth_required":false,"body":{"url":"string — public http(s) Terms URL; only public redirect destinations are followed"},"response":"{ url, text, characterCount, truncated }. Review extracted text before sending it as termsText/policyText; script-rendered pages, private-network redirects, and PDFs may still require manual paste."},"fields":{"companyName":{"type":"string","required":true,"description":"Company or AI model name"},"website":{"type":"string","required":true,"description":"Homepage URL; bare domains are accepted and normalized to https://"},"email":{"type":"string","required":true,"description":"Contact email for results"},"termsText":{"type":"string","required":true,"description":"Full text of Terms of Service or usage policy (not a URL)"},"submitterType":{"type":"'company' | 'ai_model'","required":false,"description":"Use 'ai_model' when submitting model cards, usage policies, or AI service terms"},"modelProvider":{"type":"string","required":false,"description":"Provider or lab name for AI model submissions"}},"example_request":{"companyName":"Acme AI","website":"https://acme.ai","email":"legal@acme.ai","termsText":"Paste the full text of your terms here...","submitterType":"ai_model","modelProvider":"Acme Labs"},"response_privacy":"The company object returns only public certification metadata: id, name, website, submittedAt, status, submitterType, and modelProvider. Contact email is retained for private administration and is never returned by this public CORS-enabled endpoint.","storage_unavailable":{"status":503,"response":"{ error } with CORS headers. The route does not run Gemini or return badge/certificate handles when durable certification storage is unavailable; use /api/review-stream for a no-storage preflight and retry the official submission later."},"quick_review_preflight":{"endpoint":"https://fairprint.app/api/review-stream","method":"POST","auth_required":false,"cors_enabled":true,"public_api_methods":["POST","OPTIONS"],"body":{"termsText":"string — full or extracted Terms of Service / usage policy text"},"response":"Default response streams text/plain model output shaped as the quick-review JSON prompt. Send ?format=json or Accept: application/json to receive structured JSON: { criteria, red_flags, improvements, summary, overallScore, passCount, qualifies, storage: 'none', badgeUrl: null, certifiedPage: null, routing }. Use this for no-storage browser-agent preflight checks; it does not create a public certificate, badgeUrl, certifiedPage, directory record, or admin submission.","routing_guidance":"If the preflight looks strong, send the applicant to /apply or /apply/ai for an official stored review. If it exposes consumer-risk clauses, route the brand/domain into /request with relevant concern tags."},"ai_writing_analyzer":{"web":"https://fairprint.app/analyze","endpoint":"https://fairprint.app/api/analyze","method":"POST","auth_required":false,"cors_enabled":true,"public_api_methods":["POST","OPTIONS"],"storage":"none","body":{"text":"string — at least 120 and at most 50,000 characters of company-produced writing (policy, support doc, marketing, blog, research, or social text)"},"response":"{ type, overallScore, confidence, label, signals[], evidence[], summary, analyzedAt }. type is one of policy|support|marketing|blog|research|social|generic. confidence is low|moderate|high. signals[] are 0-10 scores under keys stylistic_fluency, repetition_templates, hedging_generality, specificity_evidence, voice_coherence, disclosure. Higher score = more consistent with AI-written output.","web_result_outputs":["copyable plain-text analysis brief","downloadable .txt analysis brief"],"usage_guidance":"Use for transparency/disclosure triage — e.g. spotting undisclosed AI-generated legal copy that FairPrint's AI-accuracy/disclosure mission cares about — not as proof of authorship. Malformed, non-object, or short (<120 char) bodies return a clean 400 JSON error; over-long (>50,000) bodies return 400 as well.","routing_guidance":"When type is policy or support and confidence is high (Likely AI-assisted), route the brand/domain into /request so undisclosed AI-generated legal copy can become a structured public nomination (the web results prefill /request with a disclosure reason, a Plain Language concern, the optional or inferred company/source name, and the imported source URL). The web analyzer also routes into /apply?prefill=analyze, which prefills the analyzed text, optional source/company name, and source URL into the official certification form for a company that wants to seek a FairPrint badge on its own terms."}},"scoring":{"scale":"0–100","method":"Sum of 10 criteria scored 0–10 each","tiers":[{"label":"FairPrint Gold","min":75,"max":100,"certified":true,"tier":"certified","description":"Exemplary consumer protections across all 10 criteria."},{"label":"FairPrint Approved","min":55,"max":74,"certified":true,"tier":"approved","description":"Good consumer protections. Meets the FairPrint standard."},{"label":"Needs Improvement","min":0,"max":54,"certified":false,"tier":"rejected","description":"Detailed per-criterion feedback provided for improvement."}]},"criteria":[{"id":1,"name":"Plain Language","weight":10,"description":"Written for real people, not just lawyers. Measures readability, sentence length, and legal jargon density."},{"id":2,"name":"Data Transparency","weight":10,"description":"Every category of data collected must be explicitly named with stated purposes and retention windows. Voice, audio, sensor, or other sensitive collection requires clear affirmative consent, and consequential uses such as personalized pricing must be disclosed."},{"id":3,"name":"No Data Selling","weight":10,"description":"Any sale or monetization of personal data must require affirmative opt-in consent."},{"id":4,"name":"Easy Cancellation","weight":10,"description":"Cancellation must be as accessible as signup — no phone-only, mail-in, support-only friction, retention mazes, cancellation fees, or hidden notice periods."},{"id":5,"name":"Clear Refunds","weight":10,"description":"Refund eligibility, timeframes, and process must be stated plainly before purchase; no-refund policies must be prominent before payment."},{"id":6,"name":"Auto-Renewal Notice","weight":10,"description":"Auto-renewals must be called out before signup with renewal date, charge frequency, cancellation deadline, trial-conversion timing, and continuing-charge consent visible."},{"id":7,"name":"No Hidden Fees","weight":10,"description":"Every material fee must appear before signup or checkout, including mandatory fees, renewal prices, promotional-price end dates, taxes, and optional add-ons."},{"id":8,"name":"Right to Delete","weight":10,"description":"Account and data deletion must be user-accessible directly, with deletion confirmation, retention timelines, backup retention, and exceptions disclosed."},{"id":9,"name":"Fair Dispute Process","weight":10,"description":"No mandatory binding arbitration with class-action waivers. Users must have small-claims court access."},{"id":10,"name":"Change Notification","weight":10,"description":"Material changes to terms must be sent by email with adequate advance notice."}],"regulatory_context":{"description":"Current public enforcement and policy signals that ground FairPrint's criteria. These signals support scoring emphasis; they do not change the 55/75 tier thresholds.","signals":[{"date":"2026-08-31","source":"FTC","title":"Amazon secret ad surcharge lawsuit","url":"https://www.ftc.gov/news-events/news/press-releases/2026/08/ftc-states-sue-amazon-over-secret-ad-surcharge-scheme","relevant_criteria":["Plain Language","No Hidden Fees","Change Notification"],"summary":"The FTC and 22 states sued Amazon, alleging it changed advertising-auction pricing without notice, imposed undisclosed surcharges, and misled advertising customers about how their prices were set."},{"date":"2026-08","source":"FTC","title":"Active Listening AI marketing final orders","url":"https://www.ftc.gov/news-events/news/press-releases/2026/08/ftc-finalizes-orders-cox-media-group-two-other-firms-settling-charges-they-deceived-customers-about","relevant_criteria":["Plain Language","Data Transparency","No Data Selling"],"summary":"The FTC finalized orders settling allegations that firms misrepresented an AI advertising service's use of voice data and consumer opt-in; the agency said collecting and using voice data without adequate consent would violate the FTC Act."},{"date":"2026-09","source":"FTC","title":"Personalized pricing proposed enforcement policy statement","url":"https://www.ftc.gov/news-events/news/press-releases/2026/09/ftc-extends-public-comment-proposed-policy-statement-regarding-personalized-pricing","relevant_criteria":["Data Transparency"],"deadlineDate":"2026-09-25","deadlineStatus":"open","deadlineNote":"Comment period open through September 25, 2026, extended from September 18, 2026 on September 3, 2026. deadlineStatus flips to 'closed' once the date passes; the signal remains a valid grounding for Data Transparency review either way.","summary":"The FTC's comment window on the proposed personalized-pricing enforcement policy statement is open through September 25, 2026 (extended from September 18, 2026). The statement warns that businesses may violate the FTC Act when they fail to disclose use of personal data to personalize prices; it does not ban personalized pricing."},{"date":"2026-08","source":"FTC","title":"Doxo bill-payment and add-on-fees settlement","url":"https://www.ftc.gov/news-events/news/press-releases/2026/08/bill-payment-firm-doxo-pay-21-million-settle-ftc-allegations-it-deceived-consumers-charged-them-add","relevant_criteria":["Plain Language","No Hidden Fees","Auto-Renewal Notice"],"summary":"Doxo agreed to pay $2.1 million to settle FTC allegations that it used misleading search ads, added delivery fees without clear disclosure, and deceptively enrolled consumers in a recurring subscription program."},{"date":"2026-09","source":"FTC","title":"Payment-processor deception enforcement (Nuvei, Humboldt)","url":"https://www.ftc.gov/news-events/news/press-releases/2026/09/payment-processor-nuvei-must-implement-robust-merchant-screening-practices-pay-485-million-settle","relevant_criteria":["Plain Language","No Hidden Fees","Auto-Renewal Notice"],"summary":"The FTC took action against two payment processors, alleging one knowingly opened and maintained processing accounts for merchants engaged in deception — including tech-support scams and a business opportunity with false earnings claims — and requiring robust merchant screening under the proposed order. Grounds review of misleading checkout, earnings-claim, and recurring-charge language in the vendors that enable it."},{"date":"2026-09","source":"FTC","title":"Amazon Prime redress order expands","url":"https://www.ftc.gov/news-events/news/press-releases/2026/09/ftc-announces-additional-payments-consumers-stemming-ftcs-amazon-prime-settlement","relevant_criteria":["Easy Cancellation","Auto-Renewal Notice"],"summary":"A federal court approved a joint FTC/Amazon motion on September 17, 2026 expanding redress under the $2.5 billion Prime settlement: more consumers qualify automatically, the maximum payment rose from $51 to $200, and payments begin October 1, 2026 with no claim form or notice required. The settlement resolved allegations that Amazon enrolled millions of consumers in Prime without their consent and knowingly made cancellation difficult."},{"date":"2026-06","source":"FTC","title":"FTC subscription-scheme action","url":"https://www.ftc.gov/news-events/news/press-releases/2026/06/ftc-sues-stop-sprawling-enterprise-operating-unlawful-subscription-schemes","relevant_criteria":["Easy Cancellation","Auto-Renewal Notice","No Hidden Fees"],"summary":"Cancellation friction, renewal disclosures, upfront fees, and recurring-charge consent are active consumer-protection risks."},{"date":"2026-05","source":"FTC","title":"Shutterstock subscription-cancellation settlement","url":"https://www.ftc.gov/news-events/news/press-releases/2026/05/shutterstock-pay-35-million-settle-ftc-allegations-over-illegal-subscription-cancellation-practices","relevant_criteria":["Easy Cancellation","Auto-Renewal Notice"],"summary":"Shutterstock agreed to pay $35 million to settle FTC allegations that it charged consumers for subscriptions without their consent, failed to disclose automatic renewal terms, and made cancellation difficult."},{"date":"2026-07","source":"FTC","title":"Hopper hidden-fees settlement","url":"https://www.ftc.gov/news-events/news/press-releases/2026/07/travel-app-hopper-pay-35-million-settle-ftc-allegations-it-charged-fees-without-consent-deceived","relevant_criteria":["No Hidden Fees","Clear Refunds"],"summary":"Hopper agreed to pay $35 million to settle FTC allegations over hidden pre-selected fees and misrepresented services."},{"date":"2026-07","source":"FTC","title":"Hims & Hers privacy and subscription action","url":"https://www.ftc.gov/news-events/news/press-releases/2026/07/ftc-states-act-against-hims-hers-deceptive-unlawful-privacy-practices","relevant_criteria":["Data Transparency","No Data Selling","Easy Cancellation","Auto-Renewal Notice"],"summary":"The FTC and state partners sued Hims & Hers, alleging difficult cancellation, unclear recurring charges, and sharing of sensitive health information with advertising platforms despite privacy promises."},{"date":"2026-07","source":"FTC","title":"AI Accuracy proposed policy statement","url":"https://www.ftc.gov/news-events/news/press-releases/2026/07/ftc-seeks-public-comment-policy-statement-addressing-ai-accuracy","relevant_criteria":["Plain Language","Data Transparency"],"summary":"The FTC sought public comment on whether undisclosed manipulation of AI outputs may be deceptive under Section 5 of the FTC Act."}]},"response_shape":{"id":"string — unique review ID, used to build badge and certified page URLs","badgeUrl":"string | null — canonical badge SVG URL for FairPrint Approved/Gold submissions; null when the review needs improvement","certifiedPage":"string | null — canonical public certificate URL for FairPrint Approved/Gold submissions; null when the review needs improvement","badge_url":"string | null — snake_case alias for agent/tool consumers","certified_page":"string | null — snake_case alias for agent/tool consumers","remediation":"object | null — remediation roadmap for below-Gold results (null when the review is FairPrint Gold). Additive field for agent/tool consumers; older clients that read only id/badgeUrl/certifiedPage/review are unaffected.","review":{"overallScore":"number 0–100","tier":"'certified' | 'approved' | 'rejected'","summary":"string — plain-English summary of the review","criteria":"array of { name, passed, score, notes }","redFlags":"string[] — specific issues found","improvements":"string[] — recommended changes"}},"remediation_roadmap":"Every Needs Improvement or Approved-but-not-Gold submission result on /apply and /apply/ai renders a remediation roadmap: the criteria below the next achievable tier, weakest first, each paired with the published 'standard to meet' copy from /standards plus a copyable plain-text remediation brief (company, policy URL, target tier, per-criterion gaps, resubmit path). Approved targets Gold (75+) and lists every criterion below a perfect 10; Needs Improvement targets Approved (55+) and lists only criteria that are not yet passing. The same roadmap also renders on the /review quick review whenever the result does not qualify (fewer than 8/10 criteria passing, each criterion needing 6+/10): it lists every non-passing criterion weakest first, states how many more passing criteria are needed to reach 8/10, and produces a qualification-framed copyable brief that points back to /review for a re-check and to /apply for the official badge. The same roadmap is also machine-readable: POST /api/review returns a `remediation` object (null for FairPrint Gold), and the quick-review JSON form of POST /api/review-stream returns a qualification-shaped `remediation` object whenever `qualifies` is false. Both expose `target`, `gaps` (name, score, passed, band, targetState, currentState), the gap count, and a paste-ready `brief` string, so agents can act on a review instead of only reporting a score.","agent_trust_checklist":[{"question":"Can an agent verify policy readability without trusting marketing copy?","evidence":"Certified pages expose the overall score and per-criterion notes for Plain Language and all other criteria."},{"question":"Can an agent detect billing, renewal, refund, or cancellation risk before recommending a product?","evidence":"Easy Cancellation, Clear Refunds, Auto-Renewal Notice, and No Hidden Fees are scored as separate criteria."},{"question":"Can an agent cite a stable third-party trust source?","evidence":"Each passing review has a canonical certified page, badge URL, public directory entry, JSON feed record, and copyable certificate-page verification handles including lookup JSON, a prefilled badge-install-kit link, and a plain-text procurement/support brief."}],"blog":{"url":"https://fairprint.app/blog","feed":"https://fairprint.app/api/blog/feed","feed_type":"application/atom+xml","cors_enabled":true,"public_api_methods":["GET","OPTIONS"],"description":"Editorial blog posts and auto-generated T&C review posts covering consumer rights, dark patterns, regulatory updates, and fair terms guidance. New review posts are generated by a periodic job; the blog feed covers both editorial and review content.","review_post_handoff":"A `<company>-terms-review` post asserts a scored verdict about one company in its own title, so each renders exactly one reader route chosen by the registry — never by the post's tier text, which describes the editorial review and diverges from the registry record the moment a company re-applies. When the named company holds a public record the post links the canonical verifier `/verify?q={company-slug}` (the slug, never the display name); when it holds none the post links the nomination queue `/request?company=&reason=&concern=` instead, because a `/verify` link for a company with no certificate reads as a record route while landing on the no-match state. No handoff is emitted for the hand-written editorial posts, which assert no company verdict. The branch is resolved per request against the same public-record predicate every other registry surface uses, so a post and the directory cannot disagree about whether a company holds a record. **The route is not the whole obligation: because these posts are the site's highest-SEO-intent pages and the ones a search result, an AI answer, or a procurement reviewer is most likely to quote, the record branch also states the state that qualifies the verdict it asserts.** The review's age is derived with the same `daysSinceReview`/`REVERIFICATION_DUE_DAYS` helpers the certificate page, `/verify`, `/directory`, `/recently-certified`, `/report`, `/api/report`, `/api/feed`, the badge SVG, the `/industries` provider card and the operator's outbound note all measure against — never a re-derived window, so a post cannot call a record current while the verifier it links to calls it overdue. An overdue review renders a qualifier naming the measured age and the 90-day window it passed, and — only while that condition holds — the seal holder's own remedy `/renew?q={company-slug}`, built from the same canonical slug the check carries so the pair cannot name different entities; a current review states its age inside the window and carries **no** remedy, because a remedy is conditional on the condition it resolves while a check is not. A recordless post states no age and offers no remedy, since there is no review to date and nothing to re-verify — `null` is not evidence of currency, and it is not evidence of staleness either. The qualifier and the remedy are derived once in `lib/review-post-handoff.ts` and rendered on both the post and the homepage's Review Intelligence cards, which delegate to the same builder, so a card and the post it links to state the same verdict and the same state."},"directory":{"endpoint":"https://fairprint.app/directory","verifier":"https://fairprint.app/verify","signal_report":"https://fairprint.app/report","feed":"https://fairprint.app/api/feed","lookup":"https://fairprint.app/api/certified-lookup?website=example.com","feed_cors_enabled":true,"feed_parameters":{"limit":"optional positive integer, capped at 50","type":"optional: company | ai_model"},"feed_invalid_parameters":"HTTP 400 JSON error; malformed filters never masquerade as an empty registry","feed_response":"{ count, entries[] }. Every entry includes id, name, website, slug, certifiedUrl, badgeUrl, verifyUrl, status, submitterType, modelProvider, score, and certifiedAt. verifyUrl is the canonical reader-facing check for the exact record, derived from the same slug as certifiedUrl. Each entry ALSO carries the freshness that qualifies its verdict, because status + score is a claim about validity and this endpoint is documented as the JSON equivalent of /recently-certified, whose whole distinction is that it flags a stale seal: daysSinceReview (whole days since the published review, or null when the record cannot be dated — never a synthesized 0, since 'the review is 0 days old' is a claim an undated record cannot make), reverificationDue (true only when the age is measurably at or past the window), reverificationDueAt (ISO date the window closed, or null), reverificationWindowDays (the window the boolean was measured against — currently 90 — published per entry so a consumer never hardcodes it), verifyHref (the relative form of verifyUrl), renewUrl (https://fairprint.app/renew?q={slug} — the seal-holder remedy, prefilling the certified identity), and updatedTermsReportUrl (the consumer nomination path into /request, null unless the review is actually overdue — a fresh record has no condition to report, so the field cannot read as an invitation to nominate a current record). certifiedAt is NOT a substitute for daysSinceReview: a record can be the newest in the feed and already overdue, so read reverificationDue rather than assuming a recent date means current. All of it derives from the same daysSinceReview/isReverificationDue/REVERIFICATION_DUE_DAYS helpers the certificate page, /verify, /directory, /recently-certified, /report, and /api/report use, so the JSON feed and its human equivalent cannot disagree about one record. Each entry also carries a `discovery` block naming the routes out of this response: `humanFeed` (https://fairprint.app/recently-certified, the human page stating the same claim), `humanReport`, `directory`, `badgeKit`, `request`, and the two cursorless record surfaces `verify` and `renew` that a consumer pairs with the entry's own `slug`. It is published on every entry including a current one, because discovery is not a condition, and its `verify`/`renew` members are bare pages with no query string so they cannot be mistaken for the per-record prefilled `verifyUrl`/`renewUrl` handles beside them.","lookup_cors_enabled":true,"public_api_methods":["GET","OPTIONS"],"lookup_parameters":["slug","name","website","q"],"verifier_parameters":["q","company","website","slug"],"verifier_canonical_prefill":"https://fairprint.app/verify?q={slug}","verifier_recent_registry_bound":"The default Recent registry entries panel shows at most six records and publishes both shown and total counts plus its newest-review-first ordering basis, so a bounded list never reads as the complete registry.","verifier_handle_on_registry_surfaces":"Every surface that asserts a verdict about a published record also offers the reader the canonical verifier handle for that exact record, so a claim can be checked without installing anything or reconstructing a URL. /recently-certified renders a 'Verify this seal' link on every row — stale or current — from the canonical slug. /directory rows carry a 'Verify' action alongside the vendor-facing handles (certificate, badge SVG, install kit). The verifier page itself qualifies the verdicts it publishes before a search: its 'Recent registry entries' cards — what a visitor sees without querying anything — derive each record's age from the same lib/record-validity.ts the certificate JSON-LD, /api/certified-lookup, and /badge-kit use, render 'Re-verification due — N days' with the is-overdue treatment when the review is past the window, and hand the seal holder the /renew remedy (built by buildRenewHref from the record's canonical slug) as a sibling of the record anchor — never nested inside it — only on the cards whose own staleness flag renders, because a remedy is conditional on the condition it resolves. The list's note states that it is ordered by review date and names the window whose flag it renders, because 'these are real certificates' is otherwise a fixed positive claim about currency and the newest record in a list can already be overdue; the page previously said 'past FairPrint's 90-day re-check window — treat it as due' about a record its own summary tiles presented as a current certificate. The certificate page's copyable handle list exposes a 'Verifier' entry and its procurement/support brief reads 'Check the claim yourself at https://fairprint.app/verify?q={slug}'. The T&C changelog (/changelog) states a tier and score on every row and now carries a 'Verify this record' handle on each one, built from the company's canonical slug — a changelog row that says 'Needs Improvement' resolves to the verifier's own no-match state, which states the absence honestly and bridges to a nomination, rather than being a dead end or a false green check. **Each row also states the age of the review its verdict came from** (lib/changelog-row-validity.ts): a verdict is a claim about a review performed on the row's date, so a row that prints 'FairPrint Approved · 59/100' and hands over a check while saying nothing about the review being five months old is a claim surface with no route to the state that qualifies it — the same class every one of these surfaces was taught, one row at a time. The age is measured from **the row's own date**, never from the company's current registry record, because a row is a receipt for one review at one date and a later re-application must not retro-qualify a historical verdict. A row whose company **also holds a live public record** and whose own review is past the 90-day window is additionally handed the seal holder's remedy (`/renew?q={slug}`, the same canonical slug the row's check carries, so the pair cannot name different entities) as a sibling of the check — never nested — and a row whose company has **no** record gets the age and no remedy, because `/renew` refreshes an existing record and a route that resolves nothing is worse than no route. As on every other surface here, a remedy is conditional on the condition it resolves while a check is not, and an undatable row states no age rather than a synthesized zero. The changelog's own freshness line is *derived from the entries it lists* (lib/changelog-freshness.ts): it names the newest entry's date and says so explicitly once the trail is past a 45-day current window, because /changelog is a seeded review history written by an offline review script and is **not** the live registry — the currently-published records are /directory and /api/feed. A fixed 'updated daily' style claim is the same defect class as a fixed negation (a claim surface with no route to the state it describes), so the page can never assert a cadence its own data does not demonstrate. The service-industry catalog reconciles each listed provider against the live registry and its provider card renders the same verifier handle beside the certificate and badge handles. The three submission-result surfaces — the /apply Terms & Conditions result panel, the /apply/ai result panel, and /apply/success — are the product's conversion moment and say 'Certification Issued' with a tier and a score, so each now carries a 'Verify this record' handle derived from the same canonical slug via buildVerifyHref({ slug }); before that they offered only the vendor artifacts (embed snippet, install kit, certificate), so the recipient of a badge could republish the claim and not check it. `/apply/success` additionally carries the two states its verdict depends on, because a verifier route is not a state: it names **which document** was reviewed (derived from the record's `submitterType` via lib/remediation-note.ts, so a record submitted through /apply/ai and reviewed against a usage policy is never confirmed as having consumer-friendly \"Terms & Conditions\" — that sentence is prepared as a share post the submitter publishes under their own name, and the page previously hardcoded it) and it states **the review's age** from the lookup response's own `validity` block, reached with `You're certified.` only when a record actually resolved (the no-match branch reads \"Submission received.\" and asserts no outcome). An overdue seal is handed `/renew?q={slug}` — the same canonical slug the certificate handle carries, so the pair cannot name different entities — while a current or undatable record gets no remedy, because a remedy is conditional on the condition it resolves and `null` is evidence of neither currency nor staleness. The derivation lives in the dependency-free lib/success-claims.ts, which the page cannot skip because it renders the derived fields directly. These are derived from buildVerifyHref({ slug })/buildVerifyUrl({ slug }) beside the verifier's own resolver, never hand-interpolated; a surface that states a verdict and lists badge SVG and lookup JSON but no human check page is the gap this closes. The canonical prefill is always the record's **slug**, never its display name, because a name containing punctuation, repeated whitespace, or non-ASCII characters has a slug that differs from its lowercased form. The enumerated surface set lives in tests/holder-remedy-handoff.test.ts (VERIFIER_HANDLE_SURFACES), and a surface absent from that list is the tell: the three result panels were absent from every guard list, which is why nothing caught them.","verification_handle_inputs":["company name","website/domain","review ID","https://fairprint.app/certified/{slug}","https://fairprint.app/api/badge/{id}","/certified/{slug}","/api/badge/{id}"],"lookup_response":"{ company: public Approved/Gold record | null, suggestions: public possible matches[], message?: string, discovery: navigation block }. A non-null company includes id, name, website, status, submitterType ('company' | 'ai_model'), modelProvider (string | null), submittedAt, badgeUrl, certifiedPage, verifyUrl, and public review fields. Suggestions are discovery aids only; only a non-null company is verified proof.","lookup_discovery":"Every lookup response carries a `discovery` block naming the routes out of it — self, verify, renew, directory, recentlyCertified, report, apiFeed, apiReport, apiIndustries — on the hit and the miss alike, because this is the endpoint an agent calls mid-task holding a proof handle it generated or was handed, so a miss is exactly the moment it would otherwise have nowhere to go. On that miss branch the record is the thing that is *stale or withdrawn*, which is precisely when the seal holder's remedy matters, so `renew` is declared beside the read-only `verify` check: a block naming the check without the remedy tells an agent the fix does not exist, and this response publishes a `validity` block on every hit whose whole purpose is to state that a review has aged out. All members are bare routes with no query string: pair a member with an identifier you hold rather than receiving a handle naming whichever record answered. The block is derived from the same site map (lib/public-surfaces.ts) that /api/feed, /api/report, /api/industries and the well-known trust manifest publish their navigation from, so one destination has one key across every registry response — and every registry response that publishes a per-record renewal state declares `/renew`: `tests/self-navigation-graph.test.ts` derives that set from the emitting sources (including `/api/report`'s `lib/report.ts` builder and this route's own `validity` block) rather than from a hand-kept list, because the first version of that guard scanned `app/api/**/route.ts` for a field name and therefore matched one emitter out of three while two shipped the asymmetry. /api/industries' own response carries the same kind of block for the same reason: it is advertised as the single call that replaces scraping the catalog, so its consumer is by construction not reading the catalog page or the registry feed.","description":"Public listing of all certified companies and AI models. /directory accepts q and tab filters; each visible registry row links to the canonical certificate, badge SVG, and a prefilled /badge-kit?q={badgeUrl} install kit so badge adoption can start from discovery, not only from certificate pages. The page's coverage headline (its Listed / Gold / AI models stat cards) is an aggregate claim about current validity, so it is qualified on the page by the freshness of the same published set the counts are derived from: how many records are published, the age of the newest and oldest review, and how many sit past the 90-day re-verification window, naming the seal-holder remedy /renew when every published seal is past it. That line is derived per request from the same public-record list and the same day helpers /report, /api/report, /api/feed, and the homepage evidence hub use (lib/registry-freshness-line.ts), so the directory and the report it summarises cannot disagree about one registry; it renders nothing rather than a synthesized zero-state when the registry is empty or holds no dated record. **The counts are three claims, not one, so the qualifier is per population as well as per page.** The `Companies` and `AI models` cards each count a subset, and an age measured over the union is dominated by whichever subset has more or older records — so a fully aged-out AI-model population could sit under a healthy registry line and still read as a live count, which matters because AI models are an explicitly invited submitter type with their own /apply/ai flow. Each subset is therefore measured separately, by the page's own AI-model predicate (the same test its AI Models tab filter uses, so a qualifier can never describe a different set than the number above it), through lib/coverage-qualifier.ts: a fully aged-out population states that every published record in it is past the window and names the seal-holder /renew route, a partially aged-out population states how many of how many and the age range, and an empty, undated, or already-fully-covered population renders nothing rather than a zero-state or a duplicate. The per-population lines carry data-fp-source=\"directory_ai_coverage_qualifier\" / \"directory_company_coverage_qualifier\" with their measured counts so the branch is verifiable without a screenshot. Empty search results link into /request with company, domain-shaped website, reason, and repeated concern= criteria prefilled so failed certificate discovery can become a structured review nomination. /verify provides a human-facing way to check badge claims from names, domains, review IDs, pasted certificate URLs, or pasted badge SVG URLs, and links to canonical certificate, badge SVG, badge-install kit, and lookup JSON handles; no-match domain checks route into /request with company, website, reason, and repeated concern= criteria prefilled so failed badge claims can become tagged review nominations.","certification_recency":{"description":"Public certificate, verified-badge, directory, and signal-report surfaces display the review date and are honesty-aware that policies change. A verified /verify result states an explicit re-verification verdict — either 'Re-verification due — this review is N days old (window closed DATE)' when the review is past the 90-day window, or 'Re-checked within FairPrint's 90-day window ... next re-check due DATE' when it is current — derived from the same helpers the certificate page, /report, and /api/report use, so the verifier can never call a seal valid while its own certificate page calls it overdue. Each surface offers a 'report updated terms' path that routes into /request with company, domain-shaped website, an updated-policy reason, and repeated concern= tags for Change Notification, Auto-Renewal Notice, No Hidden Fees, and Right to Delete, so anyone who notices a policy has changed since review can flag it for re-verification rather than letting a stale seal stand. The signal report turns that into a standing queue: /api/report exposes reverificationQueue with every public record past the 90-day re-verification window, oldest first, each with a prefilled re-review request URL and a nominated flag.","surfaces":["https://fairprint.app/certified/{slug} (certificate recency note)","https://fairprint.app/verify (verified badge result re-verification verdict)","https://fairprint.app/directory (registry listing recency note)","https://fairprint.app/report (seals due for re-verification queue)","https://fairprint.app/report (newest certificate recency actions)"],"prefill_parameters":["company","website","reason","concern"]}},"industry_catalog":{"endpoint":"https://fairprint.app/industries","api_endpoint":"https://fairprint.app/api/industries","cors_enabled":true,"allowed_methods":["GET","OPTIONS"],"query_parameters":["industry"],"description":"The six catalogued service-industry frameworks (moving companies, home warranties and services, tax preparation and bookkeeping, legal document services, online education and career services, subscription health/fitness/telehealth) reconciled against the live public certification registry. /industries and each /industries/{slug} page render provider cards from the same reconciliation: a listed provider holding a public Approved/Gold record shows its tier, score, review date, re-verification state, and the canonical certificate, verifier, and badge handles; a provider whose name matches a published record without its listing website matching reads 'Record under this name' and still offers the certificate and verifier handles for that record; a provider with no public record reads 'Not yet reviewed' with its nomination path. A provider whose record is past the 90-day re-verification window additionally carries the **seal holder's** remedy — `Seal holder: re-verify` into `/renew?q={slug}` — beside the read-only verifier, because the card states the condition (`Re-verification due · N days`) and a stated condition owes the actor who can resolve it a route: until this shipped, the only action in that row was the *consumer* nomination path, so a holder reading their own stale verdict in the catalog had nowhere to act. The remedy is derived with the shared `buildRenewHref()` from the record's canonical slug (never a hand-rolled prefill) and is conditional on the same staleness boolean the flag renders — a current record carries the check and no remedy, exactly as `/directory` and `/recently-certified` pair them. Call /api/industries?industry={slug} to read that as JSON, so a listing can be checked without scraping the catalog page.","api_response":"{ generatedAt, source, public_records_only, listed_providers, public_records_matched, public_records_listed, catalog_coverage, name_resolved_providers, catalog_field_notes, industries[], providers[], public_records_not_in_catalog[], note }. Each providers[] entry is { industry, name, website, service, listed, association_method, association_published, certified, tier, score, grade, reviewed_at, days_since_review, reverification_due, reverification_due_at, reverification_window_days, slug, certified_page, verify_url, renew_url, badge_url, disclosure, criteria_url }, and each public_records_not_in_catalog[] entry is the same shape minus the catalog-only fields — { name, website, certified, association_method, association_published, tier, score, reviewed_at, days_since_review, reverification_due, reverification_due_at, reverification_window_days, grade, disclosure, renew_url, slug, certified_page, verify_url, badge_url, criteria_url } — so a consumer can parse either array with one schema. That 'one schema' claim is itself guarded now: this array omitted reverification_due_at, reverification_window_days, grade, and disclosure while the sibling published them, so a consumer parsing either array with the documented schema read `undefined` for the threshold on the only array that has live rows (all five published records match no catalogued provider today) — and `reverification_due: true` beside `days_since_review: 167` without the threshold it is measured against states a staleness claim with no comparison. reverification_due_at is the window **close** for a dated record and null (never a synthesized date) for an undated one; reverification_window_days is the window **size**, published on every row including the 18 of 18 providers with no record, because the window belongs to the *rule* rather than to any one record's condition — an unconditional field, unlike the conditional renew_url — so a consumer that parses one row's shape never finds the key present on rows that happen to have a record and absent on the ones that do not. Both come from the one shared constant and are never hardcoded at a call site, so a change to the window reaches the catalog, /api/report, /api/feed and the certificate page in the same deploy. `criteria_url` (https://fairprint.app/standards) is the 10-criteria framework every review is scored against, published on EVERY row and not only the record rows: `disclosure` states a verdict ('FairPrint has not assigned a score or published findings for this provider'), and a verdict's reader is owed the standard it would be measured against — before this field, an agent reconciling the catalogue could learn that 18 of 18 providers are unreviewed and could not tell a consumer what a review checks. It is deliberately site-wide rather than a per-provider handle, because a provider with no record has no seal to verify and /verify?q={slug} would be the unlisted-target mistake. `association_method` is 'website-host' (the provider's own domain, the durable identity, preferred), 'slug' (exact name match), or 'name' (a public record exists under the name this catalog prints, but the listing's website does not identify it), and is null when there is no record. `renew_url` is the seal holder's remedy (https://fairprint.app/renew?q={slug}) published beside the read-only `verify_url`: it is non-null only for a published record that is actually past the re-verification window, because the read-only check is unconditional while the remedy is conditional on the condition it resolves — and it is null for a 'name'-resolved association, whose /renew prefill would be the name rather than the domain this response publishes. `certified` is true only for the 'website-host' and 'slug' associations — a 'name' association is deliberately excluded, and `association_published` (true only for 'website-host'/'slug') lets a consumer count published associations without silently counting a name match as a domain-level link. A 'name'-associated provider still publishes its tier, score, days_since_review, certified_page, and verify_url, because the name is a handle the verifier and /renew both resolve exactly; nothing is ever inferred from a *similar-looking* name (`Uber` and `Uber Eats` are different services and never equate). A provider with no record at all returns certified: false, association_method: null, and null handles: that is an absence of review, not a negative verdict, and unpublished or non-passing submissions are never named. `public_records_not_in_catalog` lists public records matching none of the catalogued providers so the catalog is not mistaken for the whole registry, and it carries the **same per-record handle set as `providers[]`, `renew_url` included**: a public record outside the catalog is a record whose seal can equally age out, and this array is where every live record currently lands (all five published records match no catalogued provider today), so an array publishing `reverification_due: true` and `days_since_review` without the remedy key told a consumer the seal was 158-166 days old and named no actor who could refresh it. Both arrays publish the same keys from the same reconciler, so a consumer can key either one identically and neither can be the one that happens to lack the route. Public records only; no submitter emails or admin state.","scope_note":"A public record for a catalogued provider certifies its Terms & Conditions against the 10 criteria. It says nothing about price, delivery, or customer service, and a catalog listing is not an endorsement; full service assessments are a separate track still in research for every listed provider."},"signal_report":{"endpoint":"https://fairprint.app/report","api_endpoint":"https://fairprint.app/api/report","cors_enabled":true,"public_api_methods":["GET","OPTIONS"],"description":"Live public report summarizing FairPrint registry coverage, FairPrint Gold/Approved mix, open review-request demand, community votes, criteria concerns tagged in nominations, lowest average criterion scores among public certificates, a seals-due-for-re-verification queue, and a canonical verification handle for every public record. Newest certificate rows expose badge proof, a prefilled badge install kit, a canonical verifier link, a 'report updated terms' path, and their own staleness state, and every re-verification row exposes the same prefilled re-review request path, so adoption and stale-policy observations can both flow from the report. Use it as a human-readable market and trust signal before browsing the directory or nominating a company.","api_response":"{ generatedAt, summary, certificationMix, criteriaPressurePoints, criteriaPressurePointsSample, nominationConcernTags, nominationConcernTagsSummary, newestCertificates, topOpenNominations, reverificationQueue, canonicalVerifyHrefs, relatedEndpoints }. summary carries the coverage counts (approvedOrGoldRecords, fairprintGold, fairprintApproved, companyRecords, aiModelRecords, openNominations, reviewedNominations, communityVotes) AND the freshness of that published set: newestReviewDays, oldestReviewDays, and recordsDueForReverification (how many published records sit past the 90-day window). The freshness figures live in summary, not only in reverificationQueue, because approvedOrGoldRecords is a claim about validity and an agent that parses summary alone must not be able to read 'N certified records' without the state that qualifies it — on 2026-09-15 every published record was 145-163 days old and 100% overdue while the headline count was 5. newestReviewDays/oldestReviewDays are null (never 0) when the registry has no dated public record, because 'the oldest review is 0 days old' is a claim an empty registry cannot make; recordsDueForReverification is the only one that is 0 rather than null in that state. All three derive from the same publicCompanies list and the same daysSinceReview/isReverificationDue helpers the per-record rows use, so summary cannot disagree with newestCertificates[] or reverificationQueue about one registry. criteriaPressurePoints is an array of { label, average, count, nominationUrl } members, where nominationUrl is the consumer's route out of that one criterion (https://fairprint.app/request?concern={label}, the label being the canonical criterion name /request validates ?concern= against): the panel published a seal-holder remedy for the age of the sample and nothing for the reader asking about the bar it names, so criteriaPressurePoints[0] ('Fair Dispute Process 1.4/10') read as a finding with no way to act on it. It is null (never a synthesized route) for a criterion with no contributing score, because a member rendering 'No data' has no measurement to act on; it is built by the same helper the rendered link calls, so the JSON and the page cannot name different criteria. nominationConcernTags is the other bounded panel on that same section, and it states its bound the same way. It is the array of the ten canonical criteria with the count of nominations that tagged each one, sorted by count descending with a label tie-break so the ORDER is total and the response renders the same five rows every time; `/report` renders `slice(0, 5)` of it under the heading 'Most-tagged nomination concerns' inside a panel whose every row prints a raw tag count, so a reader seeing `4` cannot tell that from the top of a queue that tagged forty. The count and the bound are readable from the same object: `nominationConcernTagsSummary` is `{ taggedNominations, nominationsRead, shown, total, omitted, isComplete, basis }`, derived by `measureTaggedNominations()` from the same requests and canonical criterion rows the report uses. `taggedNominations` counts nominations carrying at least one canonical criterion tag, while `nominationsRead` includes untagged nominations, so a zero can distinguish an empty queue from measured nominations submitted without tags. `total` is the number of canonical criteria with a positive tag count, `shown` is capped at five, and `basis` states nomination-count descending with a criterion-label tie-break. `/report` renders only the positive-count rows, states the tagged-nomination denominator, and publishes an explicit bound note whenever criteria are withheld. criteriaPressurePointsSample is the state that qualifies criteriaPressurePoints, and it is published beside the averages rather than only inside summary: an average is a claim about current quality in a way a count is not ('Fair Dispute Process 1.4/10' reads as how these companies treat disputes today), and on 2026-09-15 every record those averages were measured from was 155-164 days old and 100% past the window. It is { contributingRecords, newestReviewDays, oldestReviewDays, recordsPastWindow, fullyAgedOut } and it is measured over the records that actually contributed a measured criterion score, not over every public record — a record whose review carries no finite criterion score contributes nothing to any average, so counting it in the sample would attach a freshness caveat to records the number was never measured across. Membership uses the same rule the averages use (the criterion name normalizes to one of the ten canonical labels and the score is finite), so a record carrying only legacy criterion names still counts. newestReviewDays/oldestReviewDays are null (never 0) when no contributing record is datable. fullyAgedOut is only true when there is a measurable sample and every contributing record is past the window; only that state states the remedy (https://fairprint.app/renew), because a partially aged-out sample is a fact rather than a systematic condition **and the holder's route resolves nothing for a reader who holds none of the measured records.** A partially aged-out sample is nonetheless a *stated condition*, so the human panel beside these averages carries the route set that resolves it for each actor it addresses — `Nominate a company →` into https://fairprint.app/request (unconditional: a reader who holds none of the measured records can still ask FairPrint to review a company, and it is the only route that resolves anything for them) and, because a lapsed record does have a holder who can refresh it, `Seal holder: re-verify →` into https://fairprint.app/renew — derived from `recordsPastWindow` by the shared `lib/panel-caveat-remedies.ts`, so the routes and the split they qualify cannot describe two populations, and the holder's half is `null` rather than synthesized when nothing in the sample has lapsed, since /renew refreshes an *existing* record. Before that derivation existed this caveat was the panel's only qualifying state and it named **no route of any kind** on the partial branch: the holder-only remedy above is gated on `fullyAgedOut` and the page's anchor with it, so a registry with any turnover reached a sentence reading `3 of 5 are past the 90-day re-verification window` and was handed nothing to do about it. This response's own fields still state no remedy on that branch; the route lives on the panel, which is where its reader is. newestCertificates[] includes daysSinceReview, reverificationDue, reverificationDueAt, reverificationWindowDays (the window the boolean was measured against, published **per record** rather than only in the sibling reverificationQueue.window object, because `reverificationDue: true` beside `daysSinceReview: 167` is a derived number without its comparison and a consumer iterating this array has no reason to open a different object — the same rule feed_response states for /api/feed), slug, badgeUrl, badgeKitUrl, certifiedPage, verifyUrl, updatedTermsReportUrl, renewUrl (the seal-holder remedy https://fairprint.app/renew?q={slug}, or null for a record inside the window — a remedy is conditional on the condition it resolves while the read-only verifyUrl and the unconditional reverificationWindowDays are not), and the nomination association nominated / nominationUrl / nominationConcerns / nominationVotes. The association is a property of the RECORD, not of the staleness queue. **Two obligations of that association are published as data, not left to a surface.** `topOpenNominationsSummary` is the bound that makes the array's ordinal heading checkable, and it is correctly `null` for a complete list — an empty list is complete by definition — so on the empty branch the caption *is* the panel and it is published as `topOpenNominationsCaption` (`{ copy, href }`, the route unconditional because `/request` resolves something for every reader whether or not they hold a record), derived from that same bound rather than written into the page: the populated caption names the demand signal and must carry the route into it, the rule this response's own contract already stated (‘a consumer synthesizing an empty-queue state should carry the route, not only the count’). And the association's **rendering** half is published as `row` (`{ nominated, url, votes, label }`, from `lib/report-nomination-status.ts`) on every per-record entry of both arrays, because publishing a field and rendering it on the page that is its human counterpart are two different obligations: `/report` rendered the association on its queue rows only, and the queue holds only *due* records, so the page's own reader could not ask ‘has this company been raised?’ of a *current* record even though every JSON consumer could answer it from `newestCertificates[]`. `row.votes` is `null` rather than a synthesized `0` when the association was not measured and `row.label` is `null` when there is nothing to state, so a surface cannot print a vote count without the route to the nomination it counts. slug, renewUrl AND the four nomination fields appear on BOTH newestCertificates[] and reverificationQueue.entries[], so a consumer of either array can key the record by the same canonical identifier and reach the actor who can resolve a stale seal; previously slug/renewUrl were queue-only, so a consumer holding only newestCertificates[] could read a 150+ day age and had no route to the remedy. The nomination association is a property of the RECORD, not of the staleness queue — /api/request accepts any company name with no registry gate, so a current record can carry an open nomination — and on 2026-09-18 it was still queue-only, which made 'is anyone asking for this company to be re-reviewed?' answerable for overdue records and unanswerable for current ones. Both arrays now resolve it from one shared derivation (resolveRecordNomination() in lib/report.ts) so they cannot name different nominations for one company. canonicalVerifyHrefs is { links[], urls[], count } — one canonical verifier handle per public record (newest first, same order as newestCertificates[] and the registry), where links[] are relative (/verify?q={slug}) and urls[] absolute; use these to hand a third party a proof handle that resolves without disambiguation rather than reconstructing one, because the canonical prefill is the company slug and not the display name (names containing punctuation, repeated whitespace, or non-ASCII characters have slugs that differ from their lowercased name). reverificationQueue is { window: { dueAfterDays, metric, rationale }, summary: { publicRecords, dueForReverification, oldestReviewDays, alreadyNominated }, entries[] }, where each entry is a public record whose policy has not been re-checked within the window (oldest first) and carries daysSinceReview, reverificationDueAt, reverificationDue, slug, certifiedPage, verifyUrl, updatedTermsReportUrl, renewUrl, nominationUrl, nominationConcerns, nominationVotes, and a nominated flag; the window default is 90 days. nominationVotes is that same matching nomination's vote count — the number /request sorts its queue by — so a reader can see how much demand a stale seal carries; it is null when nominated is false and comes from the same match nominationUrl opens, so the link, its criteria, and the count always describe one nomination. topOpenNominationsSummary is the bound that makes that array's ordinal heading checkable: { shown, total, omitted, isComplete, basis }. The array is sliced to shown (5) and the section it drives is titled 'Highest-priority pending nominations' — an ORDINAL claim over a population the response truncates — while every rendered row states its own vote count, so a reader seeing '7 votes' concludes that is the top of the queue when a higher-voted nomination may sit below the cut in rows they were never shown. This response published the population size for its other two bounded panels (reverificationQueue.summary.publicRecords qualifies reverificationQueue.entries[], summary.approvedOrGoldRecords qualifies newestCertificates[]) and NOTHING for this one: summary.openNominations lives in a different object, is not named by the array, and states a total without stating the bound, so '0 == 0' and '5 of 400' were indistinguishable to a consumer that did not already know the cap of 5. total/omitted are null (never 0) when the queue was not measured, because an unmeasured population is not an empty one, and isComplete is true only on a measured, fully-published list; basis names the ranking rule ({ primary: 'votes', tieBreak: 'submittedAt', order: 'descending' }) because a young queue sits at zero votes, where the stated ordering and the effective one diverge precisely when a reader is most likely to trust the heading. The total is derived from the same pending list the slice drew from, so a reviewed nomination never inflates it. topOpenNominations[] lists PENDING nominations only, matching summary.openNominations: the section it drives is titled 'Highest-priority pending nominations' and both are derived from the same pending/reviewed split, so including a reviewed row would make the list and the counts describe two different populations. A reviewed nomination can still be the reverificationQueue association, because 'has this company been raised?' and 'what is open to vote on?' are different questions. The `nominated` association is resolved by **company slug** on both sides (toSlug(), the same rule getCompanyBySlug() resolves on) and never by comparing lowercased display names, so  Cash App associates with the cash-app record while  Cash App, Inc. (a genuinely different slug) does not; when nominated is true, nominationUrl carries the newest matching nomination's own public queue row (https://fairprint.app/request#request-{id}) so an agent can read what was asked and which criteria were tagged instead of only being told a nomination exists, and it is null when nominated is false. 'Newest' is determined by the request's submittedAt field, sorted newest-first with a most-recently-appended tie-break on equal timestamps — a sort over a field the request type does not declare reads undefined on every record, and because a comparator returning NaN is treated as 'equal' the code still runs while the ordering becomes whatever the caller's array happened to be. Each entry exposes both halves of the re-verification loop: updatedTermsReportUrl is the consumer nomination path into /request, and renewUrl (https://fairprint.app/renew?q={slug}) is the seal-holder remedy that refreshes the existing certificate under the identity it was certified under, so an agent can route a stale seal to whoever can actually resolve it. reverificationDue is the per-record staleness boolean and appears on both newestCertificates[] and reverificationQueue.entries[]; a record present in reverificationQueue.entries[] always has reverificationDue true, and a seal can be the newest certificate and still be overdue. Each public certificate page states the same status in visible text, and every queue row plus the certificate recall note links the seal holder into the self-serve https://fairprint.app/renew flow (see re_verification). relatedEndpoints includes badgeKit and feed (https://fairprint.app/api/feed, the JSON approvals stream whose response is the machine-readable counterpart of /recently-certified and which carries a mirroring `discovery` block back to this report), so a consumer holding either JSON document can reach the other without guessing the URL. Public records only; no submitter emails, admin state, or legacy payment fields are exposed. A public record is defined as status approved or certified AND not written by FairPrint's own production verification AND not leaked test debris: POST /api/review is public and unauthenticated and stores what it reviews, so maintenance runs that prove the deployed path create real records, which carry a verificationProbe marker (reserved contact address) and are excluded from every public surface — this report, /directory, /recently-certified, /api/feed, /api/certified-lookup, the sitemap, the certificate page, and the badge SVG routes (/api/badge?id={id} and /api/badge/{id}, which answer 404 for a record that is not public so a published proof handle can never resolve to a badge FairPrint did not issue). The predicate is applied inside the two registry resolution branches (getPublicCompany() / getPublicCompanyBySlug() in lib/db.ts), so no caller can publish a non-public record by omitting a check and one record cannot answer differently on two surfaces. summary.approvedOrGoldRecords, summary.fairprintGold, summary.newestReviewDays, summary.oldestReviewDays, summary.recordsDueForReverification, certificationMix, criteriaPressurePoints, newestCertificates[], canonicalVerifyHrefs.count, and reverificationQueue.summary.publicRecords all derive from that one definition, so two stats printed under the same 90-day heading on /report and its admin panel cannot disagree. No field in this response ever names a probe.","source_data":["public Approved/Gold certification records","public review-request queue","FairPrint criteria labels"],"related_actions":["https://fairprint.app/directory","https://fairprint.app/verify","https://fairprint.app/badge-kit","https://fairprint.app/renew","https://fairprint.app/request","https://fairprint.app/apply"],"operator_surface":"The same reverificationQueue drives an admin panel at /admin (authenticated), which renders the identical derived fields — slug, certifiedPage, verifyUrl, updatedTermsReportUrl, renewUrl, nominationUrl, nominationConcerns, nominationVotes — plus two copyable outreach notes. The **re-check note** is the stale-seal premise (\"your seal aged past the re-verification window\") and hands over the /renew remedy; the **remediation note** (lib/remediation-note.ts) is the operator's copy for a record that is *below the publication bar*, a premise the re-check note cannot express — and not a hypothetical one, since the changelog's own audit trail carries far more rejected rows than passing ones and /api/report publishes their verify handles. The remediation note composes from a closed reason catalogue (below-bar, stale-seal, unlisted-target) and carries **the handle its own reason names** — the required handle is per-reason, not one global field. The two record-shaped premises (below-bar, stale-seal) require the recipient's canonical record handle and composition throws without it, because a note that asserts a finding and supplies no way to read it is the uncheckable-claim class. `unlisted-target`'s premise is that **no record exists**, so requiring a record handle there made it uncomposable rather than merely unselectable: its required handle is instead the nomination route (`buildNominationUrl()` → `/request?company=&website=`), where the absence is visible and where the company can see a review has been asked for. A record-shaped note deliberately never prints the nomination route, so the substitute cannot over-widen into a different audience than the premise. Every reason must have a mount site whose population admits its premise — a catalogue entry that only a definition, a doc row, and a test reach is unreachable, which is what `unlisted-target` was while its only composer mounted on the re-verification panel (whose every row is a public record by construction). The operator mount sites are therefore two: the re-verification panel (`defaultReasonId: stale-seal`, for a published record) and the cold-outreach tracker (`defaultReasonId: unlisted-target`, for a target with no FairPrint record at all), and the tracker row mounts the note only where no record resolved — a target that holds a seal gets the proof-handle note instead. It deliberately restates **no** score, tier label, or criterion name — those strings belong to lib/review.ts and lib/standards.ts, and a second copy in message text is how two surfaces start disagreeing about one record. Every row carries the whole detect/resolve loop, matching the public /report row action for action: a seal-holder route into /renew (Seal holder: re-verify), the consumer nomination route into /request (Nominate for review), the canonical read-only **Verify** handle (verifyUrl), and Inspect seal. The copyable note hands the holder all three URLs — the /renew remedy, the public certificate, and the verifyUrl for a buyer, procurement reviewer, or agent who only wants to check the claim — so an outreach message names the same handle the public surfaces publish and cannot describe a third-party audience while supplying only vendor links. A row whose company already carries a nomination renders that association as its own route into the public queue row (with the nominator's tagged criteria and that nomination's vote count, both from the same match), because the operator deciding whether to send another re-check note needs to read what was already asked — and how much demand it has drawn — rather than only be told a nomination exists. Each queue row also carries a per-record outreach log (status, last-contacted date, next follow-up, operator note) keyed by the record's canonical slug, read/written through the authenticated /api/reverification-outreach endpoint, so a holder that has already been contacted is distinguishable from one that never was instead of the panel re-deriving the same list every visit; the log stores operator-authored state only and never restates the review age, which stays derived from lib/report.ts. The outreach tracker at /admin/outreach additionally offers a copyable proof-handle note for any target that already holds a public seal, carrying its verifyUrl, certifiedPage, badgeUrl, and a certified-lookup handle, so the operator's message and the public record cannot disagree about which URL proves the claim. Because that note is a claim about a record and leaves FairPrint's domain in a direction that cannot be corrected afterwards, it also carries the record's **validity** state, measured by the same `lib/record-validity.ts` the certificate page, `/api/feed`, `/api/certified-lookup` and `/badge-kit` use: for a record past the 90-day window the note states the review's measured age and the window it passed before handing over any handle, and closes with the seal holder's own `/renew` route instead of the cadence sentence; a current record keeps the adoption copy and is never handed a re-verification link, because a remedy is conditional on the condition it resolves. The tracker row itself renders the same state (`Re-verify · N days`) beside a `Holder re-verify` anchor into `/renew`, gated on the same boolean, so the operator drafting the message meets the staleness before the recipient does. The tracker's \"Email drafts\" modal is the same population split **one hop upstream of the buttons**: it resolves each target through the shared `lib/outreach-draft-surface.ts` into either a *record* opportunity or a *nomination* one, so a target that already holds a seal is never pitched the cold sequence (whose bodies claim \"companies that pass receive a verified badge they can embed\" — written for a company that has not been reviewed, and a claim they have already satisfied). The record branch renders the existing proof-handle note with the record's own payload (one artifact, not two spellings) and the recordless branch renders the three cold drafts *plus* the target's nomination route (`/request?company=&website=`) *plus* the same `unlisted-target` remediation note the tracker row mounts, so the note that states the premise the branch just named is available where the operator decides what to send rather than only on the row behind them; the branch with no nomination route on file states plainly that one is absent and why the note therefore cannot compose (its required handle is the nomination route, so `resolveRemediationHandles` refuses rather than emitting a note that points at nothing) instead of rendering a control that would surface an error where its copy belongs; the modal is keyed on the narrowed payload (`heldRecord()`), never on the optional field, so a target with no record cannot render a seal, a score, or a handle set it does not have. `OutreachProof` publishes the record's `validity` block itself — not only flat `overdue`/`daysSinceReview` siblings — because that block is the field the note reads, and publishing only the flat pair left the note composing the *unmeasured* branch (no staleness line, no `/renew` remedy) while the row beside it rendered `Re-verify · N days`. The submissions console also carries the **submitter-kind dimension every public surface already had**: `submitterType` selects which document was reviewed (a company's Terms & Conditions, an AI model's usage policy) and it is published by `/directory` (its own `AI Models` tab and per-population qualifier), `/api/report`'s `summary` (`aiModelRecords` beside `companyRecords`), `/api/feed?type=`, `/certified/{slug}`, `/verify`, `/renew` and `/badge-kit` — while the console that triages submissions referenced it nowhere, so an operator could not see how much of the pipeline the explicitly invited `/apply/ai` funnel accounts for. The table now partitions the same organic set by submitter kind (`Companies` / `AI models`, disjoint by construction so their totals sum to the headline `Organic` card), labels each row's `Kind`, filters on it, and derives each row's remediation note for the document that row was actually reviewed against (`documentForSubmitter` → every reason's lead and ask name the artifact, and `unlisted-target`'s lead names the party that accepts it — a company's Terms & Conditions belong to what its customers use, an AI model's usage policy belongs to the model's own users), because a note asking a usage-policy record to resubmit its Terms & Conditions names something the recipient never submitted, and a note calling an AI model's own policy a product attribute names the wrong relationship. The partition reads `lib/admin-submission-stats.ts`, which removes verification probes once before splitting, so a probe cannot reappear inside a population subtotal."},"badge_adoption":{"guide":"https://fairprint.app/badge-kit","canonical_badge_url":"https://fairprint.app/api/badge/{id}","badge_validity_qualifier":"The badge SVG states the state that qualifies its verdict, because it is the one FairPrint artifact whose entire purpose is to be published on a third party's page — where an unqualified `✓ APPROVED` can never be corrected afterwards, and where an embed has no prose around it to supply the doubt a human reader would feel. When the review is past the 90-day re-verification window the badge renders a caption band reading `Re-verification due — review is N days old`, and that band is its own anchor to the seal-holder remedy `https://fairprint.app/renew?q={company-slug}`; the certificate link (the read-only check) remains on every badge, stale or current, because a remedy is conditional on the condition it resolves and a check is not. A current badge is unchanged from the original mark — 240×76, one anchor, no caption, no remedy — so an embed never reflows for a record with nothing to report. `aria-label` and `<title>` carry the same qualifier for an overdue record, because an assistive technology and a text-extracting consumer have no visual caption to fall back on. Both badge routes render through one shared module (lib/badge-artifact.ts) so `/api/badge/{id}` and the legacy `/api/badge?id={id}` cannot describe one record differently, and the age derives from the same daysSinceReview/isReverificationDue/REVERIFICATION_DUE_DAYS helpers the certificate page, /verify, /directory, /recently-certified, /report, /api/report, /api/feed, the provider card, and lib/record-validity.ts all use — so a badge cannot disagree with the certificate page it links to. A record that cannot be dated publishes no caption and no remedy rather than a synthesized zero-state, and both routes still answer 404 SVG/JSON for a record that is not public.","canonical_certificate_url":"https://fairprint.app/certified/{company-slug}","verification_api":"https://fairprint.app/api/certified-lookup?q=https://fairprint.app/api/badge/{id}","human_verifier":"https://fairprint.app/verify","per_record_verifier":"https://fairprint.app/verify?q={company-slug}","reverification_cadence_claim":"**A cadence is a claim about a process, and three public surfaces stated one over a population that had never been through it.** /request, /badge-kit and /renew told a prospective applicant and a prospective seal holder *how often* a published seal is re-checked, and none of them said whether it ever had — while, measured live on 2026-09-18, this API published approvedOrGoldRecords: 5, recordsDueForReverification: 5 and oldestReviewDays: 167, with /api/feed carrying reverificationDue: true on 5 of 5 entries: the honest answer on every one of those surfaces was 'not yet, for any of the five'. The claim is load-bearing rather than decorative — a stale seal only *means* something if a reader believes the re-check recurs, and a procurement reviewer who follows the page's own invitation to check finds the two statements disagreeing. All three surfaces now render one derived sentence (lib/reverification-cadence.ts) that fuses the canonical window with the registry state the caller read: the window is substituted from the caller's measurement (REVERIFICATION_DUE_DAYS, the same constant this report, /report, /api/feed and the certificate page use) so a change to it reaches every sentence in the same deploy, and the population clause is built from counts read out of reverificationQueue.summary (publicRecords, dueForReverification, oldestReviewDays) or the page's own public-record list, so the sentence and the report a reader might open cannot describe two populations. null is never a synthesized 0: an unread registry — and an empty one, which has no backlog to report and no record to route a holder to — renders the cadence sentence with **no** population clause, because '0 of 0 seals are overdue' is a clean bill of health for a process with nothing to check yet. The seal holder's /renew?q={slug} remedy is attached only on the branch where a measured overdue record exists for that route to resolve, and the slug is the oldest entry of the same queue this report publishes first, so the remedy names the record a holder would be sent to anyway; a measured-clean registry gets the counts and no remedy, because a remedy is conditional on the condition it resolves. The sentence is deliberately built from counts of records — the oldest, the window, how many — so it cannot drift into editorialising about the seals themselves. The rendered paragraphs carry data-fp-source=\\\"request_cadence_claim\\\" on /request and \\\"badge_kit_generator\\\" on /badge-kit; /renew derives the same claim on the server from the public-record list it already read, because a client component cannot read the registry and would otherwise paint the cadence with no population beside it. Derivation is pinned in tests/reverification-cadence.test.ts, including the two branches that must not synthesize a zero and the substitution check (a window of 30 must not print 90).","validity_qualifier":"Every machine-readable surface that states a verdict also publishes the state that qualifies it. GET /api/certified-lookup returns a `validity` block on each resolved company: `unqualifiedVerdict` (true when the review is past the window — named so a consumer reading the field by name learns what to do with the `status` beside it), `daysSinceReview` (whole days, or null when the record cannot be dated — never a synthesized 0), `reverificationDue`, `reverificationDueAt`, `reverificationWindowDays` (currently 90, published so no consumer hardcodes it), and `validityNote` (a quotable sentence naming the age and the window close, or null for a current record). It is derived in lib/record-validity.ts from the same daysSinceReview/isReverificationDue/reverificationDueAt/REVERIFICATION_DUE_DAYS helpers the certificate page, /verify, /directory, /recently-certified, /report, /api/report, /api/feed, and the provider card use, so no two surfaces can disagree about one record. The certificate page's own `Review` JSON-LD carries `dateModified` (the review date), `expires` (the window close, omitted for an undated record rather than fabricated), and a `reviewAspect` naming which side of the window the review sits on — because `headline` ('{Company} is FairPrint Approved') plus `reviewRating` plus `datePublished` is a validity claim, and a structured-data consumer meets no prose on the way, so absence of a caveat field would read as a current verdict. The badge-kit's generated structured proof carries the same `expires`/`reviewAspect`, which matters most of the three: that snippet is pasted onto the vendor's *own* trust page, so an unqualified award + reviewRating there is a claim that has left FairPrint's domain and can never be corrected afterwards. A human reading the certificate meets the staleness prose on the way down the page; a machine parsing reviewRating met nothing until these blocks shipped. **The same rule governs a *human* coverage claim, and the homepage is the surface that needed it:** the evidence hub's Report card advertises 'Gold/Approved mix ... from public records' and routes the reader into /report — the page whose whole distinction is that it flags a seal past the 90-day window — so the front door once stated a coverage mix without the freshness that qualifies it, on 2026-09-15 when all five published records were 155–163 days old and 100% overdue. The card now renders a derived freshness line (lib/registry-freshness-line.ts + components/EvidenceHubReportCard.tsx) stating the published count, the age of the newest and oldest review, and how many sit past the window, naming /renew for the seal holder when every published seal is overdue. Four deliberate rules there, each a defect class already closed elsewhere on this site: it is derived per request from the same public-record list and the same day helpers /report and /api/report use (never a stored or asserted figure), it returns null — never a synthesized zero-state — when the registry is empty or holds no dated record, `now` is injected so the sentence is deterministic rather than reading the clock internally, and only the all-overdue branch names the remedy, because offering /renew against a current seal is the over-widening `updatedTermsReportUrl` is gated against. tests/registry-freshness-line.test.ts pins the duplicate arithmetic against lib/report.ts's canonical helpers, the no-zero-state branches, the remedy gating, and the absence of any cadence word or hard-coded year.","verifier_vs_vendor_handles":"The badge kit separates the two handle sets it prints. Vendor handles — the certificate page, the badge SVG, and the lookup JSON — are what an embed or a procurement packet consumes. The reader's own handle is the per-record verifier link (https://fairprint.app/verify?q={company-slug}), the one click that lets a reviewer check the claim without reconstructing a URL. Every copyable verification brief leads with the verifier link and keeps the vendor handles after it, and GET /api/certified-lookup returns the same value as `verifyUrl` on each resolved company so a generator never has to derive it.","description":"Implementation guidance for FairPrint Approved and FairPrint Gold badge holders. The badge kit includes an interactive lookup/generator that accepts company names, domains, certificate URLs, or badge SVG URLs and returns exact copyable embed HTML, lookup JSON URLs, shareable prefilled install-kit URLs, structured proof JSON-LD, and verification briefs for active public records. Company records are labeled as Terms & Conditions reviews; AI model/service records use submitterType='ai_model', modelProvider when present, and usage-policy review language in briefs and structured proof. Shareable prefilled install-kit URLs accept q, badge, certificate, or company query parameters with the same company/proof-handle inputs and auto-run the lookup on page load. No-match domain lookups link into /request with company, website, and a certificate-miss reason prefilled so failed badge-install attempts can become review nominations. It also documents recommended trust-center/checkout placement and rules that every displayed seal should link back to the public certificate rather than relying on screenshots or vendor-only claims. Each resolved record renders its own verifier block with the per-record link (copyable and openable) alongside the three vendor handles, and the generated verification brief opens with that link.","generator":{"web":"https://fairprint.app/badge-kit","prefilled_url_pattern":"https://fairprint.app/badge-kit?q={company-or-proof-url}","prefill_parameters":["q","badge","certificate","company"],"lookup_source":"https://fairprint.app/api/certified-lookup?q={company-or-proof-url}","lookup_response_fields":["id","name","website","status","submitterType","modelProvider","submittedAt","badgeUrl","certifiedPage","verifyUrl","validity","review"],"accepted_inputs":["company name","website/domain","https://fairprint.app/certified/{company-slug}","https://fairprint.app/api/badge/{id}","/certified/{company-slug}","/api/badge/{id}"],"outputs":["escaped HTML embed snippet","lookup JSON URL","shareable prefilled install-kit URL","per-record verifier link","structured proof JSON-LD snippet","plain-text verification brief"],"structured_proof":"JSON-LD Organization snippet with award, certificate URL, review ID, FairPrint standards URL, optional 0-100 reviewRating, optional `expires` (the re-verification window close), a `reviewAspect` naming which side of FairPrint's 90-day re-verification window the review sits on, and sameAs carrying the badge SVG, the lookup JSON, and the per-record verifier link. The validity properties are not decorative: this snippet is pasted onto the *vendor's own* trust page, so an unqualified award + reviewRating here is a validity claim that has left FairPrint's domain and can never be corrected after the copy lands. They come from the same lookup-response `validity` block the certificate page's own Review JSON-LD uses, so the certificate, the lookup JSON, and the vendor snippet cannot describe one record differently. Pair the snippet with the visible badge where custom scripts are allowed so crawlers and browser agents can connect the company's trust page to the FairPrint proof record.","eligibility_rule":"Only active FairPrint Approved or FairPrint Gold records produce generated badge snippets; no-match lookups route to /request for nomination, preserving domain-shaped inputs as the request website when possible and pre-tagging common certificate-miss criteria via repeated concern= parameters."},"eligible_tiers":["FairPrint Approved","FairPrint Gold"],"not_eligible":"Needs Improvement submissions should not display a FairPrint badge until they improve their policy and earn Approved or Gold on re-review."},"re_verification":{"endpoint":"https://fairprint.app/renew","accepted_parameters":["q","slug","company","website"],"accepted_inputs":["company name","website/domain","review ID","https://fairprint.app/certified/{company-slug}","https://fairprint.app/api/badge/{id}","/certified/{company-slug}","/api/badge/{id}"],"description":"Self-serve re-verification for an entity that already holds an Approved or Gold seal. Resolves the existing public record by name, domain, review ID, or pasted certificate/badge proof handle, shows the same review age, re-verification window, and staleness status that the certificate page, /report, and /api/report expose, then hands the certified company name and URL into /apply via ?prefill=renew so the seal holder re-reviews under the identity they were certified under instead of re-registering. The applicant supplies only their current policy text and a contact email. Re-verification is free. A no-match query returns the /apply path so an uncertified entity is not stranded; partial matches offer live-registry suggestions. The matched record ALSO carries the canonical read-only verifier handle (a `Verify this record` action built by the shared buildVerifyHref from the same canonical slug the certificate link beside it carries, so the pair cannot name different entities), because this page states a verdict AND the staleness state that qualifies it and had been handing over only the vendor-facing certificate and the public report: the seal holder is the highest-intent actor and the one most often asked by a buyer, procurement reviewer, or agent to prove the claim, so the page that had just resolved the record exactly must hand over the link that checks it. It renders on every matched record, stale or current (a check is unconditional; a remedy is conditional on the condition it resolves), and is deliberately absent from the no-match branch, where no record exists and the honest route is /apply.","handoff":{"target":"https://fairprint.app/apply?prefill=renew","carried_fields":["companyName","website"],"applicant_supplies":["current Terms & Conditions or usage-policy text","contact email"]},"staleness_semantics":"The window is 90 days (REVERIFICATION_DUE_DAYS). A record past the window is flagged reverificationDue and appears in reverificationQueue on /report and /api/report, where each entry carries slug, certifiedPage, updatedTermsReportUrl (consumer nomination path) and renewUrl (seal-holder remedy at https://fairprint.app/renew?q={slug}). A successful re-verification replaces the score, review date, badge proof, and staleness flag on the same public certificate URL."},"request_queue":{"endpoint":"https://fairprint.app/request","api_endpoint":"https://fairprint.app/api/request","method":"GET public queue; POST /api/request for nominations and upvotes","cors_enabled":true,"allowed_methods":["GET","POST","OPTIONS"],"description":"Community nomination queue for companies whose terms deserve review. Strong nominations include direct T&C, privacy, pricing, renewal, cancellation-policy, or buried-policy URLs. The web form includes guided consumer-pressure presets for hard-to-cancel subscriptions, surprise renewals/trial charges, refund or hidden-fee concerns, data deletion/sharing concerns, voice/audio/sensor-data consent concerns, and undisclosed personalized pricing; each preset fills an editable reason and tags the matching FairPrint criteria before submission. Each public request includes share, copyable pressure-brief, and copyable complaint-letter actions for support tickets, complaint threads, policy contacts, or direct company outreach. Nominators can tag specific criteria they are concerned about; tagged concerns appear in the request list and are included in the copyable pressure brief and complaint letter so teams see which standards are at issue. A request whose `status` is `reviewed` also carries the check for its own outcome: the row states the result and links the canonical verifier for the company it named, so a nominator can confirm the verdict instead of only reading the editorial note — and because a nomination can resolve to Needs Improvement, that link is emitted for every reviewed row and answers honestly at the verifier's no-match state rather than promising a certificate.","prefill_url_parameters":["company","website","reason","concern","concerns","q","url"],"share_pattern":"https://fairprint.app/request#request-{id}","get_response":"Array of { id, company, website?, reason?, concerns?, submittedAt, votes, status, reviewSlug? }, sorted by votes then newest date. `reviewSlug` is the EDITORIAL blog slug and is now validated on write as a published post (see review_slug_validation): a `reviewed` row's value always resolves at /blog/{slug}, which is what lets the public queue render it as a link.","outcome_handle":"Each entry in the report's topOpenNominations[] additionally carries `slug` (the canonical company slug, derived from `company` with the same toSlug() every certificate URL and registry handle uses) and `verifyUrl` (https://fairprint.app/verify?q={slug}). Use those to follow a nomination through to whether the named company holds a public record. `verifyUrl` is deliberately NOT derived from `reviewSlug`: that field is the editorial blog slug (e.g. `duolingo-terms-review`), a different identifier from the company's, and a record look-up built from it names an entity that does not exist. The operator queue that *sets* `status: reviewed` renders the same canonical handle on every row (labelled `Check record`, from the shared `lib/request-outcome.ts` derivation), so the actor who records a verdict and the nominator who reads it are handed the same record identifier rather than two independently computed ones. The queue also validates the *other* identifier it asks the operator to type: `reviewSlug` is the editorial blog slug the public row renders as the `Reviewed ↗` badge, so the write refuses a value naming no published post (`400 reason: unpublished_review_slug` — see `review_slug_validation`), and the row itself derives the field's verdict from the published corpus (`lib/request-review-slug.ts`), opens the post the value names via `Open note ↗`, offers the corpus-convention slug as a one-click suggestion, and warns when a published slug belongs to a different company. Both identifiers therefore arrive derived or verified from the console, and neither can publish a route that does not resolve. `reason` is published **whole** on these entries, while the human `/report` panel renders a *prefix* of it — cut at 150 characters with an ellipsis by the shared `lib/nomination-reason-preview.ts` derivation, which also supplies the panel's copy for an absent reason — so a consumer comparing this response against the rendered page should expect the sentence there to be shorter and marked, never a second value for the same field.","empty_queue_route":"An **empty** queue is not a dead end on either surface, and a consumer that reads this response should read the same thing the page does. /request's empty branch names a real published record as a starting example (the oldest review, slug tie-break, from the live registry — never a hardcoded company name) and states what a nomination gets you; it is the branch a first visitor always sees while the queue is empty. /report's 'Highest-priority pending nominations' panel states the absence *and* renders the entrance: `content` returns the panel's caption plus a `Nominate a company →` link into https://fairprint.app/request, built by the dependency-free `lib/queue-zero-state.ts` and gated on `summary.openNominations === 0`. That is deliberate and it is a standing rule rather than a one-off: the panel's own bound note (`topOpenNominationsSummary`) correctly returns null for a complete list, and an empty list is complete by definition — so on the one branch every real visitor reaches, the bound machinery is silent by design and the caption *is* the panel. Before 2026-09-18 that caption read 'No pending nominations. If a badge claim cannot be verified, send it to the queue with the company name, policy URL, and criteria concerns.' — a sentence naming a destination with no route to it, on the third of three consecutive panels describing the same demand signal, while the section header one level up already carried the link. A consumer synthesizing an empty-queue state should carry the route, not only the count.","post_fields":{"submit":{"action":"submit","company":"string required","website":"string optional","reason":"string optional","concerns":"string[] optional — array of criterion labels nominator is concerned about (e.g. ['Easy Cancellation', 'No Hidden Fees'])"},"upvote":{"action":"upvote","id":"string required"},"admin_update":{"action":"admin_update (authenticated)","id":"string required","status":"'pending' | 'reviewed' required","reviewSlug":"string optional — the EDITORIAL blog slug (e.g. spotify-terms-review), validated as a PUBLISHED post"}},"post_responses":{"submit_created":{"created":true,"id":"string"},"submit_existing":{"existing":true,"id":"string","votes":"number","voteAccepted":"boolean"},"upvote":{"votes":"number"},"duplicate_vote":{"status":409,"error":"Already voted for this request","votes":"number"},"unpublished_review_slug":{"status":400,"reason":"unpublished_review_slug","error":"No published post at /blog/{slug}"},"storage_unavailable":{"status":503,"error":"The nomination, vote, or admin update was not saved; retry later"}},"review_slug_validation":"`reviewSlug` is a claim about a **published document**, not a free-form label: the public /request row renders it as the `Reviewed ↗` badge, so every nominator is invited to open it as a link. An `admin_update` that sets `status: 'reviewed'` with a slug naming no post at https://fairprint.app/blog/{slug} is therefore refused with a machine-readable `400 reason: 'unpublished_review_slug'` rather than stored, because a typo (`cashapp-terms-review` for `cash-app-terms-review`) or an invented slug would publish a resolution whose evidence is a 404 while the nomination still read as resolved. The published set is exactly what `/blog/[slug]` serves — `getAllPosts()`, which merges the generated `<company>-terms-review` posts with the hand-written editorial ones — so the check asks the same question the reader's click asks. Scope is the status that renders the link: a `pending` row may hold a draft slug while its note is being written. The operator console applies the same derivation rather than a second one (`lib/request-review-slug.ts`, dependency-free): each row states the field's verdict (empty / not a slug / no published post at that path), renders `Open note ↗` into the post the value names so the operator can read what they are attaching, offers the corpus-convention slug as a one-click suggestion (never a silent rewrite, since the convention is not universal), warns when a published slug belongs to a *different* company (the subtlest form of this defect — every check passes and the link resolves, but the nomination points at another company's review) without blocking the save, and disables Save while the value would send every reader to a 404. Machine-readable mirror of that field's obligations: the public row's `reviewSlug` is also published on `/api/report`'s `topOpenNominations[]` entries, where `verifyUrl` is deliberately **not** derived from it (that array's record handle comes from the company slug, a different identifier)."},"ai_models":{"welcome":true,"dedicated_flow":"https://fairprint.app/apply/ai","note":"AI models and AI services are explicitly invited to submit their usage policies for evaluation. FairPrint certification is a meaningful trust signal for AI adoption.","ftc_ai_accuracy_signal":{"title":"FTC AI Accuracy proposed policy statement (July 2026)","url":"https://www.ftc.gov/news-events/news/press-releases/2026/07/ftc-seeks-public-comment-policy-statement-addressing-ai-accuracy","summary":"The FTC opened a public comment period that closed July 31, 2026, on whether AI companies that distort their systems' outputs for undisclosed ideological objectives violate Section 5 of the FTC Act. This validates the Plain Language and Data Transparency criteria for AI model policies.","relevant_criteria":["Plain Language","Data Transparency"]}}}