// Bewary API v1 response types, generated from the contract (https://bewary.app/docs/response).
// Copy this file into your project. Every documented fact key is always present, null when unknown.
// Treat an unknown enum value as "unknown": new values are added in minor versions.

export interface AccountResponse {
  credits: CreditState
  recent: Array<UsageRow>
}

/** A mobile app on the Apple App Store or Google Play (task E12, added 2026-10-29). */
export interface AppFacts {
  store: "apple" | "google_play" | null
  /** Apple: the numeric App Store id. Google Play: the package name (application id). */
  app_id: string | null
  /** live: the store lists it now. removed: we saw it live before and the store no longer lists it. not_found: the store has no such app and we never saw it live. unknown: the store could not be read (degraded). */
  status: "live" | "removed" | "not_found" | "unknown" | null
  /** The store's title for the app (from the last live sighting). */
  name: string | null
  /** The developer or seller name the store publishes (from the last live sighting). */
  developer: string | null
  /** How many ratings the store shows (Apple: in that storefront). */
  rating_count: number | null
  /** Apple: the App Store country read, ISO 3166-1 alpha-2. */
  storefront: string | null
  /** The first time we saw it live, RFC 3339 UTC. */
  first_seen: string | null
  /** The last time we saw it live, RFC 3339 UTC. */
  last_seen: string | null
  /** The first check that found it gone after a live sighting, RFC 3339 UTC; null while live. */
  missing_since: string | null
}

/** The site's history in a public web archive (E9): when its homepage was first and last captured, and whether it was captured before the current registration began (a name that lapsed and was registered again). */
export interface ArchiveHistory {
  /** When we read the archive's index, RFC 3339 UTC. Kept for up to 30 days. */
  checked_at: string
  /** The archive holds at least one capture of the host's homepage. */
  captured: boolean
  /** The first capture, RFC 3339 UTC; null when none. */
  first_capture_at: string | null
  /** The latest capture, RFC 3339 UTC; null when none. */
  last_capture_at: string | null
  /** The homepage was captured more than 30 days before registered_at: the name had an earlier life before this registration (it lapsed and someone registered it again). Null when either date is unknown, or for a tenant of a hosting platform. */
  captured_before_registration: boolean | null
  /** How many calendar months hold a capture before registered_at; null unless captured_before_registration. */
  months_before_registration: number | null
  /** The last capture before registered_at, RFC 3339 UTC: the end of the earlier life. */
  last_capture_before_registration_at: string | null
}

export interface BatchError {
  error: ItemError
}

export interface BatchResponse {
  schema_version: string
  request_id: string
  checked_at: string
  /** Credits charged: one per valid item. */
  credits: number
  /** In the order of items. */
  results: Array<Envelope | BatchError>
}

export interface Brand {
  id: string | null
  name: string
}

export interface Classification {
  /** What the message is: delivery_notice, account_alert, otp_or_2fa_code and so on. */
  artifact_type: string
  stance: "inbound_solicitation" | "user_own_record" | "ambiguous"
  ask: "none" | "pay" | "click_link" | "call_back" | "give_credentials" | "give_code" | "install_app" | "reply" | "other"
  /** Null until the calibrated classifier ships. */
  scam_type: string | null
  impersonated_brand: Brand | null
  probabilities: Record<string, Record<string, number>>
}

/** One source's complaints about a number over the retained 13 months (DIRECTORY-CONTRACT.md). Counts only: never a complaint's text, a name or a place smaller than a state. */
export interface ComplaintStats {
  count: number
  /** Distinct days with a complaint. */
  days: number
  /** First complaint day, 00:00Z. */
  first_at: string
  /** Last complaint day, 00:00Z. */
  last_at: string
  /** FTC: subject code (imposters, debt_reduction, ...); FCC: call type (prerecorded_voice, live_voice, text_message, ...). */
  subjects: Record<string, number>
  /** FTC: recorded-message flag; FCC: prerecorded-voice share. */
  robocall_share: number
  /** The last 90 days, only days with complaints, oldest first. */
  daily: Array<DailyCount>
  /** Top 5 US states (USPS codes) of the people who complained. */
  reporter_states: Record<string, number>
  /** Complaints by the hour of the call, "0" to "23", every hour present: the reporter's local time as they gave it (FCC: the call time; FTC: the call time when it was not given as midnight). 13 months. */
  hours: Record<string, number>
  /** Complaints by weekday of the complaint day, "1" (Monday) to "7", every day present. 13 months. */
  weekdays: Record<string, number>
}

export interface CreditState {
  /** The current UTC month, YYYY-MM. */
  period: string
  /** Free credits every key gets each UTC month; they reset. */
  monthly_free: number
  /** Free credits used this month. */
  monthly_used: number
  /** Prepaid credits from packs. They never expire and are spent after the free ones. */
  prepaid: number
  /** What can be spent now: free credits left this month plus prepaid. */
  remaining: number
  /** The account has bought a pack: its receipts and invoices are in the billing portal (POST /v1/billing/portal). */
  billing_account: boolean
}

export interface DailyCount {
  /** YYYY-MM-DD. */
  day: string
  count: number
}

export interface DatasetRef {
  dataset: "registration" | "dns" | "certificates" | "web_archive" | "threat_feeds" | "threat_aggregates" | "brand_registry" | "web_pages" | "ip_network" | "phone_numbering" | "us_numbering" | "phone_complaints" | "honeypot_lines" | "phone_reports_de" | "shortcodes" | "email_providers" | "breaches" | "wallet_reports" | "sanctions" | "community_reports" | "campaigns" | "text_model" | "platform_pages" | "iban_registry" | "bank_codes" | "app_stores" | "short_links" | "payment_profiles"
  /** WW: worldwide. A country code (US, DE): the data describes only that country's numbers, codes or complaints. */
  coverage: string
  in_coverage: boolean
}

export interface DnsFacts {
  resolves: boolean | null
  a: Array<string> | null
  mx: boolean | null
  ns: Array<string> | null
}

export interface DomainFacts {
  /** The owner boundary per the Public Suffix List with its private section: x.pages.dev stays x.pages.dev. Block by this. */
  registrable_domain: string | null
  /** The name a registry sold (Public Suffix List, ICANN section): x.pages.dev -> pages.dev. registered_at, expires_at and registrar describe this name. */
  registered_domain: string | null
  tld: string | null
  tld_abuse: "low" | "medium" | "high" | null
  /** RFC 3339 UTC. */
  registered_at: string | null
  /** When the registration expires, RFC 3339 UTC, from RDAP. */
  expires_at: string | null
  age_days: number | null
  age_bucket: "lt_7d" | "d7_30" | "d30_90" | "d90_365" | "gt_1y" | "unknown" | null
  /** The registrar's name, from RDAP. */
  registrar: string | null
  dns: DnsFacts | null
  /** What the domain is doing now: live (resolves), gone (NXDOMAIN), broken (SERVFAIL: a delegation that no longer answers), parked (a parking service's name servers), held (does not resolve because its registry or registrar holds it, see hold), unknown (no answer in time). */
  status: "live" | "gone" | "broken" | "parked" | "held" | "unknown" | null
  cert_first_seen: string | null
  lookalike: Lookalike | null
  platform: "none" | "multi_tenant" | null
  allowlisted: boolean | null
  /** On Bewary's scam list. */
  listed: boolean | null
  seen_in_checks_7d: number | null
  /** ISO 3166-1 alpha-2 country its country-code TLD names; null for generic TLDs and generic-use ccTLDs (.co, .io, .tv...). */
  country: string | null
  /** ISO 3166-1 alpha-2 countries whose people it is aimed at, from the national lists that name it (CERT Polska: PL, Taiwan's 165 line: TW, Germany's regulator: DE, US complaint feeds: US). Empty when no list with a national scope names it. */
  target_countries: Array<string> | null
  /** How many lists name it (our own and others'); 96% of listed domains are on one list (2026-10-27). */
  list_count: number | null
  /** Our verdict on the site, when our worker read its page in the last 90 days; null otherwise. */
  site: SiteVerdict | null
  /** The hosting platform's own answer about the site (Vercel's takedown page, Cloudflare's phishing warning, a host's not-found page), when our worker read it; null otherwise. */
  platform_status: PlatformStatus | null
  /** The status codes the registry publishes for registered_domain (the EPP statuses of RFC 5731 and 3915 as RDAP carries them, RFC 8056), in snake case: server_hold, client_hold, redemption_period, pending_delete, client_transfer_prohibited and the rest. Null when the registry publishes no status. */
  registry_status: Array<"active" | "inactive" | "associated" | "locked" | "validated" | "proxy" | "private" | "removed" | "obscured" | "add_period" | "auto_renew_period" | "renew_period" | "transfer_period" | "redemption_period" | "pending_create" | "pending_delete" | "pending_renew" | "pending_restore" | "pending_transfer" | "pending_update" | "client_hold" | "server_hold" | "client_delete_prohibited" | "client_renew_prohibited" | "client_transfer_prohibited" | "client_update_prohibited" | "server_delete_prohibited" | "server_renew_prohibited" | "server_transfer_prohibited" | "server_update_prohibited" | "delete_prohibited" | "renew_prohibited" | "transfer_prohibited" | "update_prohibited" | "other"> | null
  /** Who holds the name out of the DNS: registry (serverHold), registrar (clientHold), both, or none. A hold is how registries and registrars take a domain down. Null when the registry's status is unknown. */
  hold: "none" | "registrar" | "registry" | "both" | null
  /** true: the registry has registered_domain; false: the registry answered that no such name is registered (it lapsed or was deleted, and anyone may register it). Null when no registration service covers the TLD. */
  registered: boolean | null
  /** When the registration record (registered_at, expires_at, registrar, registry_status, hold) was read, RFC 3339 UTC. An answer is kept up to 7 days; a daily pass reads names on our lists again, those that stopped resolving first. */
  registration_checked_at: string | null
  /** The site's history in a public web archive; null until we have read it (a first lookup queues the read). */
  archive: ArchiveHistory | null
  /** When Bewary's own watch first saw this host, in a new certificate or a new name in a registry's zone file, RFC 3339 UTC; null when it never did. */
  first_seen_by_bewary: string | null
  /** When a public list we read first named this host, RFC 3339 UTC; null when none names it, or when a list naming it already did the first time we read that list (the date it was added there is not known). */
  first_listed_publicly: string | null
}

export interface EmailFacts {
  /** Entity id of the address's domain. */
  domain: string | null
  mx: boolean | null
  disposable: boolean | null
  free_provider: boolean | null
  role_account: boolean | null
  /** For a throwaway provider: receiving (it still takes mail: a mail host that resolves, or the domain's own address when it has no MX, RFC 5321), dead (the name is gone, declares it takes no mail with a null MX, RFC 7505, or has no mail host that resolves), unknown (not re-checked yet). Null when not disposable. */
  disposable_status: "receiving" | "dead" | "unknown" | null
  /** When the throwaway domain was last re-checked, RFC 3339 UTC; null when not disposable or not checked yet. */
  disposable_checked_at: string | null
}

export interface EntityBase {
  /** e1, e2 and so on, unique in the response. */
  id: string
  value: string
  normalized: string
  role: "sender" | "link_target" | "redirect_hop" | "hosting" | "callback_number" | "reply_to" | "payee" | "mentioned" | "unknown"
  verdict: "scam" | "suspicious" | "safe" | "unknown"
  p_scam: number
  risk_score: number
  reason_codes: Array<string>
  /** Ids of linked entities: a link's domain, a redirect target. */
  related: Array<string>
  /** Every dataset checked for this entity, with its geography. in_coverage false means the dataset does not cover this entity (a US-only dataset and a non-US number), so its silence is not a clean result. */
  datasets: Array<DatasetRef>
}

export interface UrlEntity extends EntityBase { type: "url"; facts: UrlFacts }
export interface DomainEntity extends EntityBase { type: "domain"; facts: DomainFacts }
export interface IpEntity extends EntityBase { type: "ip"; facts: IpFacts }
export interface PhoneEntity extends EntityBase { type: "phone"; facts: PhoneFacts }
export interface ShortcodeEntity extends EntityBase { type: "shortcode"; facts: ShortcodeFacts }
export interface EmailEntity extends EntityBase { type: "email"; facts: EmailFacts }
export interface WalletEntity extends EntityBase { type: "wallet"; facts: WalletFacts }
export interface PaymentHandleEntity extends EntityBase { type: "payment_handle"; facts: PaymentHandleFacts }

/** One entity found in the input. Narrow on type: if (e.type === "domain") e.facts.age_days */
export type Entity = UrlEntity | DomainEntity | IpEntity | PhoneEntity | ShortcodeEntity | EmailEntity | WalletEntity | PaymentHandleEntity

/** The answer of every lookup and check. */
export interface Envelope {
  schema_version: string
  request_id: string
  /** RFC 3339 UTC. */
  checked_at: string
  engine: string
  models: ModelsUsed
  input: InputInfo
  verdict: "scam" | "suspicious" | "safe" | "unknown"
  /** Probability that the input is part of a scam. */
  p_scam: number
  risk_score: number
  confidence: number
  classification: Classification | null
  /** The codes that decided the verdict, strongest first, at most 5. */
  reason_codes: Array<string>
  entities: Array<Entity>
  signals: Array<Signal>
  degraded: boolean
  degraded_reasons: Array<"source_timeout" | "source_unavailable" | "budget_limit" | "redirect_unresolved" | "input_truncated" | "model_unavailable" | "page_unavailable">
  cached: boolean
  explanation: Explanation | null
}

export interface ErrorDetail {
  code: "invalid_input" | "unsupported_type" | "unauthorized" | "forbidden" | "quota_exceeded" | "rate_limited" | "not_found" | "input_too_large" | "internal" | "degraded_unavailable" | "gone"
  message: string
  request_id: string | null
}

export interface ErrorResponse {
  error: ErrorDetail
}

export interface Explanation {
  text: string
  language: string
}

/** The bank a national bank-code list names for the IBAN's bank code. Never the account holder. */
export interface IbanBank {
  /** The bank's name as its country's list gives it. */
  name: string
  /** Its BIC (ISO 9362) from the same list, or null when the list has none. */
  bic: string | null
  /** The publisher's attribution for that list, e.g. 'Source: National Bank of Belgium' (1.6.0). Show it where the bank's name is shown. */
  source: string | null
  /** Where the publisher gives the list free of charge (1.6.0). Several publishers (NBB, Banka Slovenije, Banco de Espana, the ECB) require that a buyer is told the data is free at the source each time it is provided. */
  free_at: string | null
}

/** An IBAN (task E11, added 2026-10-29): the account number's validity and its bank, never its holder. */
export interface IbanFacts {
  /** ISO 3166-1 alpha-2 from the IBAN's first two letters. */
  country: string | null
  /** The SWIFT IBAN registry marks the country as a SEPA country; null for a country the registry does not list. */
  sepa: boolean | null
  /** ISO 13616: a registry country, its length, its BBAN structure and the check digits (MOD 97-10). */
  valid: boolean | null
  /** The first check that failed; null when valid. */
  invalid_reason: "country" | "length" | "structure" | "checksum" | null
  /** The print format: groups of four separated by a space. */
  print_form: string | null
  /** The bank identifier at its country's position in the BBAN (SWIFT IBAN registry); null unless valid. */
  bank_code: string | null
  /** The branch identifier where the country has one; null unless valid. */
  branch_code: string | null
  /** The bank its country's national list names for bank_code; null when we hold no list for the country (the entity's bank_codes dataset then has in_coverage false) or the code is not in it. */
  bank: IbanBank | null
  /** On Bewary's scam list: a community report named this account. */
  listed: boolean | null
  /** How many lists name it (our own and others'); 96% of listed domains are on one list (2026-10-27). */
  list_count: number | null
}

export interface InputInfo {
  kind: "lookup" | "message" | "screenshot"
  channel: "sms" | "email" | "web" | "voice" | "social_dm" | "messaging_app" | "unknown"
  language: string | null
  region: string | null
}

export interface IpFacts {
  version: 4 | 6 | null
  asn: number | null
  as_org: string | null
  /** Registry country of the address block. */
  country: string | null
  hosting_type: "datacenter" | "cdn" | "residential" | "mobile" | "business" | "unknown" | null
  reverse_dns: string | null
  listed: boolean | null
}

export interface ItemError {
  code: "invalid_input" | "unsupported_type"
}

export interface KeyCreated {
  id: string
  /** Shown once. Bewary keeps only a hash. */
  key: string
  note: string
}

export interface KeyInfo {
  id: string
  name: string | null
  created_at: string | null
  last_used_at: string | null
  revoked: boolean
  /** The key may download the bulk files and read the TAXII collection. */
  bulk: boolean
}

export interface KeyList {
  keys: Array<KeyInfo>
}

export interface Lookalike {
  brand_id: string | null
  technique: "homoglyph" | "typo" | "brand_word" | "brand_scam_word" | "subdomain" | "tld_swap" | "userinfo" | null
}

export interface ModelsUsed {
  judge?: string | null
  vision?: string | null
  verify?: string | null
  deep_tools?: string | null
  calibration?: string | null
  [key: string]: string | null | undefined
}

export interface NumberLink {
  e164: string
  reports: number
  /** The linked number has a page: the index rule, inside the current wave. */
  indexable: boolean
}

export interface Operator {
  name: string
  brand_id: string | null
}

export interface PageFacts {
  /** The page was read (deep=true only). */
  analyzed: boolean | null
  title_present: boolean | null
  has_login_form: boolean | null
  has_payment_form: boolean | null
  /** Brand id the page shows, if any. */
  brand_shown: string | null
}

export interface PaymentHandleFacts {
  provider: "cashapp" | "venmo" | "zelle" | "paypal" | "other" | null
  /** The handle, lowercase, without $ or @. */
  handle: string | null
  reports: number | null
  /** From the service's public profile page (E8, added 2026-10-29): active (a profile is shown), inactive (the service shows the profile as not active: PayPal only), not_found (no profile under this name: never made, or closed), refused (the service refused our read), unknown (no answer we can read). Existence and status only: never the name, photo or anything else on the page. Null for a provider we do not read. */
  status: "active" | "inactive" | "not_found" | "refused" | "unknown" | null
  /** When the profile page was read, RFC 3339 UTC. */
  status_checked_at: string | null
}

export interface PhoneComplaints {
  ftc: ComplaintStats | null
  fcc: ComplaintStats | null
  /** FCC: the callback numbers calls from this number told people to call, most reports first, at most 5; null when none. */
  callbacks: Array<NumberLink> | null
  /** FCC: this number as the callback number in other numbers' calls; null when it never was one. */
  pushed_by: PushedBy | null
}

export interface PhoneFacts {
  e164: string | null
  country: string | null
  number_type: "mobile" | "landline" | "voip" | "toll_free" | "premium" | "shared_cost" | "shortcode" | "unknown" | null
  original_carrier: string | null
  line_type: "mobile" | "landline" | "voip" | "toll_free" | "premium" | "shared_cost" | "shortcode" | "unknown" | null
  /** FTC and FCC complaints; null when the directory could not be read. */
  complaints: PhoneComplaints | null
  honeypot_hits_30d: number | null
  listed: boolean | null
  /** Never a person's name. */
  public_listing: "business" | "personal" | "none" | "unknown" | null
  /** The number has a directory page: it meets the index rule (two independent signals) and is inside the current wave. */
  indexable: boolean | null
  /** ISO 3166-1 alpha-2 countries whose people it is aimed at, from the national lists that name it (CERT Polska: PL, Taiwan's 165 line: TW, Germany's regulator: DE, US complaint feeds: US). Empty when no list with a national scope names it. */
  target_countries: Array<string> | null
  /** How many lists name it (our own and others'); 96% of listed domains are on one list (2026-10-27). */
  list_count: number | null
}

/** What the hosting or form platform itself serves here, from a page our own worker read in the last 90 days (E5/E7). Only the platform's own pages count; a live page read after them clears the fact. */
export interface PlatformStatus {
  platform: "vercel" | "netlify" | "cloudflare" | "github_pages" | "firebase" | "render" | "glitch" | "blogger" | "gitbook" | "godaddy_sites" | "lovable" | "heroku" | "surge" | "replit" | "framer" | "weebly" | "square_online" | "wix" | "webflow" | "canva_sites" | "google_docs" | "linktree" | "jotform" | "microsoft_forms" | "typeform"
  /** taken_down: the platform's page says it removed or blocked it for abuse or a terms violation. warning: the platform shows its own phishing or malware warning in front of it. not_found: the platform serves nothing here (never made, deleted by its owner, or removed without saying why: most hosts show one page for all three). suspended: paused for its owner's plan or usage, or by its owner; not a verdict on the site. */
  status: "taken_down" | "warning" | "not_found" | "suspended"
  /** The newest read that showed this page, RFC 3339 UTC. */
  read_at: string
  /** The first read that showed the platform's takedown page (else its warning page) since the site was last seen live, RFC 3339 UTC. Kept when the platform later deletes the site; null if it never flagged it. */
  flagged_at: string | null
}

/** A number as the callback number other numbers' calls told people to call (FCC). */
export interface PushedBy {
  /** Complaints that named this number as the callback number. */
  reports: number
  /** Distinct days with such a complaint. */
  days: number
  /** Distinct caller IDs that pushed it (complaints with no caller ID not counted). */
  callers: number
  /** First such day, 00:00Z. */
  first_at: string
  /** Last such day, 00:00Z. */
  last_at: string
  /** The FCC call types of those complaints (prerecorded_voice, live_voice, text_message, ...), most first. */
  subjects: Record<string, number>
  /** The last 90 days, only days with reports, oldest first. */
  daily: Array<DailyCount>
  /** Reports by the hour of the call, "0" to "23", every hour present. */
  hours: Record<string, number>
  /** Reports by weekday, "1" (Monday) to "7", every day present. */
  weekdays: Record<string, number>
  /** Top 5 US states (USPS codes) of the people who complained. */
  reporter_states: Record<string, number>
  /** The caller IDs that pushed it, most reports first, at most 20. */
  top: Array<NumberLink>
}

export interface RedirectHop {
  /** A short link we read: the link itself, then any other short link it led to. */
  url: string
  /** The HTTP status the shortener answered. The destination is never requested, so it is final_url, not a hop. */
  status: number | null
}

/** What the shortener itself says about the link, read from its own answer (E3, added 2026-10-29). */
export interface ShortLink {
  /** The shortening service, named by its main short domain (j.mp is bit.ly, rb.gy is rebrand.ly, q-r.to and l.ead.me are qrco.de). other: a shortener we follow but whose block and warning pages are not measured. */
  service: "bit.ly" | "tinyurl.com" | "is.gd" | "v.gd" | "cutt.ly" | "rebrand.ly" | "t.ly" | "ow.ly" | "shorturl.at" | "tiny.cc" | "t.co" | "qrco.de" | "surl.li" | "u.to" | "zpr.io" | "vk.cc" | "clck.ru" | "other"
  /** active: it redirects to final_url. warning: it shows its own warning page before the destination. disabled: the shortener disabled the link and says why (reason). removed: the link existed and no longer works, no reason given. not_found: the service has no such link. hosted: the service answers with a page of its own instead of a redirect. refused: the service refused our read. unknown: no answer we can read. */
  state: "active" | "warning" | "disabled" | "removed" | "not_found" | "hosted" | "refused" | "unknown"
  /** When disabled: it broke the service's rules (terms), the account that made it was suspended for abuse (account_suspended), or the service flagged it as harmful (threat). Null otherwise. */
  reason: "terms" | "account_suspended" | "threat" | null
  /** When the shortener's answer was read, RFC 3339 UTC. */
  read_at: string | null
}

export interface ShortcodeFacts {
  code: string | null
  country: string | null
  status: "legitimate" | "mixed" | "scam" | "unknown" | null
  operator: Operator | null
  legit_uses: Array<string> | null
  reports_30d: number | null
}

export interface Signal {
  /** A stable reason code, https://bewary.app/docs/reason-codes */
  code: string
  /** The entity id, or "message". */
  entity: string
  severity: "info" | "low" | "medium" | "high" | "critical"
  /** Signed contribution: positive toward scam, negative reassuring. */
  weight: number
  source_class: "confirmed" | "first_party" | "heuristic" | "model" | "attacker_content"
  /** Model probability when source_class is model. */
  p: number | null
  /** The dataset the signal came from, https://bewary.app/data */
  dataset: "registration" | "dns" | "certificates" | "web_archive" | "threat_feeds" | "threat_aggregates" | "brand_registry" | "web_pages" | "ip_network" | "phone_numbering" | "us_numbering" | "phone_complaints" | "honeypot_lines" | "phone_reports_de" | "shortcodes" | "email_providers" | "breaches" | "wallet_reports" | "sanctions" | "community_reports" | "campaigns" | "text_model" | "platform_pages" | "iban_registry" | "bank_codes" | "app_stores" | "short_links" | "payment_profiles"
  /** That dataset's geography: WW (worldwide) or a country code (US, DE). */
  coverage: string
  evidence: Record<string, unknown>
}

/** The score band a site's score fell in, and what the labelled hosts in it turned out to be (1.4.0). */
export interface SiteBand {
  /** The band's lowest score (inclusive). */
  score_from: number
  /** The band's highest score (exclusive, but 1.0 is in the top band). */
  score_to: number
  /** Labelled hosts whose score fell in this band. */
  hosts: number
  /** How many of them were scam or compromised sites. */
  scam_sites: number
  /** 95% interval of the band's scam share, low end (Clopper-Pearson). */
  p_low: number
  /** 95% interval of the band's scam share, high end. */
  p_high: number
}

/** Our reading of the site at this host, from a page our own worker read (added 2026-10-28; calibrated 1.4.0). Jev reads the page together with the domain's facts. Its score ranks scam sites well (AUC 0.915 on a blind round) but is not a probability, so `p_scam_site` is the measured scam share of labelled hosts in the same score band (docs/b2b/PRECISION.md, GET /v1/calibration/site). It never changes the entity's verdict. */
export interface SiteVerdict {
  /** When our worker read the page, RFC 3339 UTC. */
  read_at: string
  /** Null: no call is made from the score (a call would need the precision PRECISION.md proves first). Only an operator's `live` mode, not used, sets it. */
  scam_site: boolean | null
  /** The calibrated probability that the site is a scam site: the share of labelled hosts in `band` that were, estimated (k + 1/2) / (n + 1) and monotone in the score. */
  p_scam_site: number
  /** The model's raw answer, a ranking score, not a probability. */
  score: number
  /** The score band and its labelled counts and interval. */
  band: SiteBand
  /** The calibration fit used, e.g. site-cal-20261030-702. */
  calibration: string
  /** When the score is at or above the threshold frozen before the blind test: what the page does, as the model read it (pretends to be an organisation it is not, a throwaway name, claims an established business on a young domain, asks for sensitive details or a payment, too good to be true). Empty otherwise. */
  reasons: Array<"impersonates" | "throwaway_name" | "claim_vs_age" | "asks_sensitive" | "too_good">
  /** How many other hosts we read whose page has this page's template (a campaign's sites share one). */
  template_hosts: number | null
  /** The verdict's question set, e.g. d2. */
  version: string
  /** The pinned model that answered, e.g. jev-1.14.0. */
  model: string
}

export interface UrlFacts {
  scheme: "http" | "https" | "other" | null
  /** Entity id of the link's domain. */
  host: string | null
  /** Where the link lands after shorteners and redirects. */
  final_url: string | null
  /** The short links read on the way, in order. */
  redirect_chain: Array<RedirectHop> | null
  shortener: boolean | null
  redirect_resolved: boolean | null
  /** On a shortener: the link's state as the shortener answers it. Null for a link that is not on a shortener. */
  short_link: ShortLink | null
  page: PageFacts | null
  /** ISO 3166-1 alpha-2 countries whose people it is aimed at, from the national lists that name it (CERT Polska: PL, Taiwan's 165 line: TW, Germany's regulator: DE, US complaint feeds: US). Empty when no list with a national scope names it. */
  target_countries: Array<string> | null
  /** How many lists name it (our own and others'); 96% of listed domains are on one list (2026-10-27). */
  list_count: number | null
  /** A form or link-in-bio platform's own answer about this one URL (a Google form, a Linktree profile, a Jotform form). A hosting platform's answer about the whole site is on the host's domain entity. */
  platform_status: PlatformStatus | null
}

export interface UsageRow {
  request_id: string | null
  endpoint: string
  credits: number
  status: number | string
  at: string | null
}

export interface WalletFacts {
  /** From the address format; eth is any EVM address (the same address exists on every EVM chain). */
  chain: "btc" | "eth" | "trx" | "ltc" | "sol" | "xmr" | "dash" | null
  /** CAIP-2 chain id where the chain is fixed (bip122:..., solana:...). */
  caip2: string | null
  listed: boolean | null
  /** On a US sanctions list (added 1.1.0). */
  sanctioned: boolean | null
  reports: number | null
  /** How many lists name it (our own and others'); 96% of listed domains are on one list (2026-10-27). */
  list_count: number | null
}

