{
  "openapi": "3.1.0",
  "info": {
    "title": "Namedesk API",
    "summary": "Programmatic access to Namedesk research, pricing, and signals.",
    "description": "REST + JSON API for domain research and appraisal. Same wallet, same data, same shape as the Namedesk dashboard. Authenticate with an `x-api-key` header generated in Settings -> API keys. Read endpoints are pure (they never start new research; you only pay when you submit a research job).",
    "version": "1.0.0",
    "contact": {
      "name": "Namedesk support",
      "email": "hello@namedesk.app",
      "url": "https://namedesk.app/faqs"
    },
    "termsOfService": "https://namedesk.app/privacy",
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.namedesk.app",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Full API documentation",
    "url": "https://namedesk.app/docs/api"
  },
  "security": [
    {
      "apiKey": []
    },
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Wallet",
      "description": "Credit balance for the authenticated organization."
    },
    {
      "name": "Research jobs",
      "description": "Submit, list, inspect, and cancel research jobs. 10 credits per uncached domain for a full read; quick reads cost 1 credit per name when the routing pass returns one."
    },
    {
      "name": "Reports",
      "description": "Fetch the most recent domain report or the pricing-only slice of it. Pure reads - never trigger compute."
    }
  ],
  "paths": {
    "/v1/wallet": {
      "get": {
        "tags": [
          "Wallet"
        ],
        "summary": "Get current credit balance",
        "description": "Returns the credit balance for the authenticated organization. Use this before submitting a job to verify funding.",
        "operationId": "getWallet",
        "responses": {
          "200": {
            "description": "Current balance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": {
                      "type": "integer",
                      "minimum": 0,
                      "example": 47
                    }
                  },
                  "required": [
                    "credits"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/research-jobs": {
      "post": {
        "tags": [
          "Research jobs"
        ],
        "summary": "Submit a research job",
        "description": "Submit between 1 and 100 domains for a full read on every name. 10 credits per uncached domain. Cached domains return their existing report immediately at no cost. The `Idempotency-Key` header is required - reusing a key within 24 hours returns the original job.",
        "operationId": "createResearchJob",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Client-generated UUID. Identical keys within 24 hours return the original job; different keys are independent submissions."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateResearchJobRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted and started. The response carries the job's per-domain breakdown; uncached names are still 'pending' or 'running'. Poll GET /v1/research-jobs/{id} every few seconds until counts.pending and counts.running reach 0.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient credits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key was reused with a different request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Research jobs"
        ],
        "summary": "List research jobs",
        "description": "List the authenticated organization's research jobs, newest first. Paginated.",
        "operationId": "listResearchJobs",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size. Defaults to 20. Minimum 1, maximum 100 - values above 100 are clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor returned in the previous response."
          }
        ],
        "responses": {
          "200": {
            "description": "Page of research jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ResearchJob"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true
                    }
                  },
                  "required": [
                    "jobs"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/research-jobs/quick": {
      "post": {
        "tags": [
          "Research jobs"
        ],
        "summary": "Submit a quick-read research job",
        "description": "Submit 1-100 domains for quick reads. Each name goes through a fast routing pass that decides whether the full appraisal is worth it:\n- **Quick read** (`read_type=\"quick\"`) - the routing pass judged the name not worth the full appraisal. You get a first-pass wholesale / aftermarket / brokered estimate plus a one-sentence rationale. 1 credit per name.\n- **Full read** (`read_type=\"full\"`) - the routing pass judged the name worth the deep appraisal, which runs end to end (every signal, every probe, every recommendation). 10 credits per name.\n\nThe job returns 202 immediately; the per-name decisions land asynchronously. Poll GET /v1/research-jobs/{id} until counts.pending and counts.running reach 0.\n\nCredits are reserved at the worst case (10 per uncached name) and the running total settles down as quick reads come back. `credits_charged` on the job payload is the authoritative running total - always reflects what your wallet actually paid.\n\n`Idempotency-Key` works the same as the full endpoint.\n\nThe `POST /v1/research-jobs` endpoint (without `/quick`) is unchanged - every domain runs the full appraisal at 10 credits each, no routing involved.",
        "operationId": "createQuickResearchJob",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Client-generated UUID. Identical keys within 24 hours return the original job; different keys are independent submissions."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateResearchJobRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted and started. `mode=\"quick\"` on the response. Per-domain credits_charged settles as each name's routing decision lands (1 credit for a quick read, 10 credits for a full read).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient credits. Quick-mode jobs reserve 10 credits per uncached name up front (the worst case, in case every name escalates to a full read); your wallet needs to cover that even though the actual charge is typically lower.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key was reused with a different request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/rerun": {
      "post": {
        "tags": [
          "Research jobs"
        ],
        "summary": "Rerun a single domain",
        "description": "Force a fresh appraisal on one domain. Default body is empty (full read, 10 credits). Pass `{\"quick\": true}` to use the same routing pass the quick endpoint uses (1 credit if the routing pass returns a quick read, 10 credits if it judges the name worth the full appraisal).\n\nUnlike the cached read of `GET /v1/domains/{domain}/pricing`, this endpoint always runs a fresh appraisal - useful when you want to refresh a stale result or upgrade a quick read to the full appraisal. The new result overwrites the prior one for future reads of `/v1/domains/{domain}/pricing` and the report endpoints (a full read always overrides a quick read for the same name; the earlier entry stays in your history).\n\nThis is the API equivalent of the dashboard's \"Run full read\" upgrade button. Customers who got a quick read in a bulk job can upgrade individual names without resubmitting the whole job.",
        "operationId": "rerunDomain",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The domain name (e.g. `acme.ai`)."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quick": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, use the routing pass (may return a quick read for 1 credit, or run the full appraisal for 10 credits). Default false always runs the full appraisal at 10 credits."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rerun started or completed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run_id": { "type": "string" },
                    "domain": { "type": "string" },
                    "status": {
                      "type": "string",
                      "enum": ["pending", "running", "finalizing", "succeeded", "failed", "cancelled"]
                    },
                    "mode": {
                      "type": "string",
                      "enum": ["full", "quick"]
                    },
                    "read_type": {
                      "type": "string",
                      "enum": ["full", "quick"],
                      "nullable": true,
                      "description": "Populated immediately for a quick read (the rerun returns terminal in-line). Null for a full read while the appraisal is still running; poll `GET /v1/domains/{domain}/pricing` for the eventual result."
                    },
                    "read_reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "The routing model's one-sentence rationale for the read tier this name received. Present on quick reads and on full reads that came from the quick endpoint; null for full reads submitted directly to /v1/research-jobs."
                    },
                    "credits_charged": {
                      "type": "integer",
                      "description": "1 for a quick read; 10 for a full read (reserved up front; finalized when the appraisal finishes)."
                    },
                    "credits_balance_after": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "run_id",
                    "domain",
                    "status",
                    "mode",
                    "credits_charged",
                    "credits_balance_after"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient credits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/research-jobs/{id}": {
      "get": {
        "tags": [
          "Research jobs"
        ],
        "summary": "Get research job status",
        "operationId": "getResearchJob",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Job state and per-domain status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Research jobs"
        ],
        "summary": "Cancel a research job",
        "description": "Stops a running job. Returns whatever credits were still reserved for names that hadn't finished yet - names that already came back keep their charge (10 credits for a full read, 1 for a quick read). The response carries `credits_refunded` so you can see exactly how much landed back in your wallet. Idempotent: calling it on an already-terminal job returns 409.",
        "operationId": "cancelResearchJob",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Job cancelled. Returns the final state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/research-jobs/{id}/report/{domain}": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Get a domain report scoped to a job",
        "description": "Returns the report for a specific domain inside a specific job. Useful when you've submitted the same domain in multiple jobs and want the version that came from a given submission.",
        "operationId": "getJobReport",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          },
          {
            "$ref": "#/components/parameters/Domain"
          }
        ],
        "responses": {
          "200": {
            "description": "Domain report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainReport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/report/{domain}": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Get the most recent report for a domain",
        "description": "Returns the most recent report for the named domain in this organization, regardless of which job produced it. Pure read - never triggers a probe. Returns 404 if the org has never researched this domain.",
        "operationId": "getLatestReport",
        "parameters": [
          {
            "$ref": "#/components/parameters/Domain"
          }
        ],
        "responses": {
          "200": {
            "description": "Domain report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainReport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Domain has not been researched by this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/pricing": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Get the pricing slice of a domain report",
        "description": "Cheapest possible call. Returns ONLY the Pricing slice for a domain this org has researched. No LLM, no probe - pure cache read. The shape matches `pricing` inside DomainReport.",
        "operationId": "getDomainPricing",
        "parameters": [
          {
            "$ref": "#/components/parameters/Domain"
          }
        ],
        "responses": {
          "200": {
            "description": "Pricing slice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pricing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Domain has not been researched, or the pricing step did not produce a row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/research-jobs/{id}/report": {
      "get": {
        "tags": [
          "Research jobs",
          "Reports"
        ],
        "summary": "Get all domain reports for a job",
        "description": "Returns every domain in the job with its current job_status and (when computed) its full DomainReport. Customers poll this during a running job to watch reports fill in; once terminal, it's a one-shot answer for 'give me everything from this submission.'\n\nDomains in `pending` or `running` status carry only `domain` + `job_status` \u2014 no `report` key (the omit-when-null contract). `paid` and `cached` domains carry the full DomainReport. `failed` domains carry an `error` code (see \"Error codes\" in the docs).",
        "operationId": "getJobReports",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Job header + array of per-domain reports.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobReportsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/research-jobs/{id}/report/pricing": {
      "get": {
        "tags": [
          "Research jobs",
          "Reports"
        ],
        "summary": "Get pricing-only slice for every domain in a job",
        "description": "Pricing-only variant of /v1/research-jobs/{id}/report. Each entry carries the price band (wholesale / aftermarket / brokered), reasoning, key factors, and risks - capped at 4 KB per text field. Faster and lighter than the full report wrapper; built for the 'CSV of just the dollars and reasoning' workflow without pulling the probes / signals / brand signals you don't need.\n\nPass `?format=csv` to receive a browser-downloadable CSV instead of JSON. The CSV contains one row per domain in the job, with the same data as the JSON response projected into a flat row format. Failed entries occupy a row with the `error` column populated and the dollar columns blank.",
        "operationId": "getJobPricing",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            },
            "description": "Response format. Defaults to JSON. Pass `?format=csv` to receive a browser-downloadable CSV (one row per domain) with columns: `domain, job_status, confidence, wholesale_low/mid/high, aftermarket_low/mid/high, brokered_low/mid/high, model, engine_version, computed_at, error, reasoning, key_factors, risks`. The CSV is BOM-prefixed for Excel UTF-8 compatibility, uses CRLF line terminators per RFC 4180, and serves with `Content-Disposition: attachment`."
          }
        ],
        "responses": {
          "200": {
            "description": "Job header + array of per-domain pricing slices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobPricingResponse"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "Browser-downloadable CSV. See `format=csv` parameter description for the column list."
                },
                "example": "\ufeffdomain,job_status,confidence,wholesale_low,wholesale_mid,wholesale_high,aftermarket_low,aftermarket_mid,aftermarket_high,brokered_low,brokered_mid,brokered_high,model,engine_version,computed_at,error,reasoning,key_factors,risks\r\nacme.ai,paid,high,200,500,1500,5000,25000,65000,50000,165000,350000,opus-class,v5,2026-05-07T18:33:42Z,,\"Premium 4-char .ai cohort...\",a | b,risk1\r\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Generate a key in Settings -> API keys. The full key is shown once when created."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Same key as `x-api-key`, sent as `Authorization: Bearer <key>`. Either auth scheme works; pick whichever your client prefers."
      }
    },
    "parameters": {
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "job_a3f2c91d"
        }
      },
      "Domain": {
        "name": "domain",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "acme.ai"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Request validation failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid `x-api-key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found or not owned by this organization.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable error code (e.g. `invalid_domain`, `insufficient_credits`, `not_yet_researched`).",
            "example": "insufficient_credits"
          },
          "message": {
            "type": "string",
            "description": "Human-readable description of what went wrong."
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "CreateResearchJobRequest": {
        "type": "object",
        "properties": {
          "domains": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "type": "string",
              "example": "acme.ai"
            },
            "description": "Between 1 and 100 unique domains."
          },
          "label": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional human-readable label shown in the dashboard, completion email, and activity feed."
          }
        },
        "required": [
          "domains"
        ]
      },
      "ResearchJob": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "job_a3f2c91d"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "cancelled",
              "failed"
            ]
          },
          "mode": {
            "type": "string",
            "enum": ["full", "quick"],
            "description": "'full' (POST /v1/research-jobs) runs the full multi-model appraisal on every domain at 10 credits each. 'quick' (POST /v1/research-jobs/quick) routes each name through a fast first-pass that decides whether the full appraisal is worth it: names judged worthy run the full appraisal (10 credits each); the rest get a quick-read pricing estimate (1 credit each). Credits are reserved at the worst case (10 per name) and the running total settles down as quick reads come back."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "domains_total": {
            "type": "integer"
          },
          "domains_completed": {
            "type": "integer"
          },
          "domains_failed": {
            "type": "integer"
          },
          "credits_charged": {
            "type": "integer",
            "description": "Sum of per-domain charges so far. For quick-mode jobs this number settles down as each name's decision lands - quick reads keep 1 credit, full reads keep 10."
          },
          "credits_refunded": {
            "type": "integer"
          },
          "domains": {
            "type": "array",
            "description": "Per-domain status for this job.",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "queued",
                    "running",
                    "succeeded",
                    "failed",
                    "refunded",
                    "cached"
                  ]
                },
                "read_type": {
                  "type": "string",
                  "enum": ["full", "quick"],
                  "nullable": true,
                  "description": "What kind of pricing this domain received. 'full' - the full multi-model appraisal (10 credits). 'quick' - a first-pass estimate from the routing pass (1 credit). Null until the appraisal finishes."
                },
                "read_reason": {
                  "type": "string",
                  "nullable": true,
                  "description": "The routing model's one-sentence rationale for the read tier this name received. Present on quick reads and on full reads that came from the quick endpoint; null for full reads submitted directly to /v1/research-jobs."
                },
                "credits_charged": {
                  "type": "integer",
                  "description": "Per-domain charge: 0 for cached/failed/pending, 1 for quick reads, 10 for full reads."
                },
                "report_url": {
                  "type": "string",
                  "format": "uri",
                  "nullable": true,
                  "description": "Permalink into the dashboard, populated when status = succeeded or cached."
                }
              },
              "required": [
                "domain",
                "status"
              ]
            }
          }
        },
        "required": [
          "id",
          "status",
          "mode",
          "submitted_at",
          "domains_total"
        ]
      },
      "DomainReport": {
        "type": "object",
        "description": "Comprehensive research report for a single domain. Returned by /v1/report/{domain} and /v1/research-jobs/{id}/report/{domain}. Null-valued keys are OMITTED from the response (a missing key means the underlying signal has not been computed for this domain). Free text fields are capped at 4 KB. `verdict` is the most recent verdict regardless of which intent it was computed for; customers who need a different intent should re-run research with that intent.",
        "properties": {
          "schema_version": {
            "type": "string",
            "const": "1.0",
            "description": "Bumps on breaking changes; additive changes keep 1.0."
          },
          "exported_at": {
            "type": "string",
            "format": "date-time"
          },
          "domain": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "example": "acme.ai"
              },
              "sld": {
                "type": "string",
                "example": "acme"
              },
              "tld": {
                "type": "string",
                "example": ".ai"
              },
              "report_url": {
                "type": "string",
                "format": "uri",
                "description": "Permalink to the human-readable dashboard view."
              },
              "share_url": {
                "type": "string",
                "format": "uri",
                "description": "Public share URL when the report has been published; absent otherwise."
              }
            },
            "required": [
              "name",
              "sld",
              "tld",
              "report_url"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "complete",
              "partial"
            ],
            "description": "complete = premium probes ran; partial = free-tier signals only."
          },
          "history": {
            "type": "object",
            "properties": {
              "first_researched_at": {
                "type": "string",
                "format": "date-time"
              },
              "last_refreshed_at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "first_researched_at",
              "last_refreshed_at"
            ]
          },
          "name_shape": {
            "type": "object",
            "description": "Deterministic features computed from the domain string. Always present (no compute required).",
            "properties": {
              "char_length": {
                "type": "integer",
                "minimum": 1
              },
              "char_length_class": {
                "type": "string",
                "example": "premium"
              },
              "tld_tier": {
                "type": "string",
                "example": "premium"
              },
              "pattern_class": {
                "type": "string",
                "example": "dictionary"
              },
              "syllable_count": {
                "type": "integer",
                "minimum": 0
              },
              "is_single_syllable": {
                "type": "boolean"
              },
              "is_alpha_only": {
                "type": "boolean"
              },
              "has_numbers": {
                "type": "boolean"
              },
              "has_hyphens": {
                "type": "boolean"
              },
              "is_dictionary_likely": {
                "type": "boolean"
              },
              "is_short_premium": {
                "type": "boolean"
              },
              "brandability_score": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100
              },
              "commercial_intent_score": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100
              }
            }
          },
          "verdict": {
            "type": "object",
            "description": "Most recent verdict (any intent). Markdown allowed in `paragraph` and `alternative_suggestion`.",
            "properties": {
              "call": {
                "type": "string",
                "enum": [
                  "yes",
                  "yes_caveats",
                  "no_consider",
                  "pass",
                  "neutral"
                ]
              },
              "intent": {
                "type": "string",
                "enum": [
                  "build",
                  "monetize",
                  "invest",
                  "explore"
                ],
                "description": "Intent the verdict was computed against. Absent when the intent is not retrievable."
              },
              "paragraph": {
                "type": "string",
                "description": "Markdown. Multi-sentence rationale, capped at 4 KB."
              },
              "alternative_suggestion": {
                "type": "string",
                "description": "Suggested alternative when call is no/pass. Capped at 4 KB."
              },
              "computed_at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "call",
              "paragraph",
              "computed_at"
            ]
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "ai_signals": {
            "type": "object",
            "description": "LLM-derived signals. Each sub-block carries its own `engine_version` and `computed_at`; sub-blocks are omitted when absent.",
            "properties": {
              "frontier_recall": {
                "type": "object",
                "properties": {
                  "models_total": {
                    "type": "integer",
                    "description": "How many frontier models were probed."
                  },
                  "models_recognizing": {
                    "type": "integer",
                    "description": "How many returned knowledge_high >= 50."
                  },
                  "per_model": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "model": {
                          "type": "string"
                        },
                        "knowledge_low": {
                          "type": "integer"
                        },
                        "knowledge_high": {
                          "type": "integer"
                        },
                        "summary": {
                          "type": "string",
                          "description": "Free text, capped at 4 KB."
                        },
                        "inferred_category": {
                          "type": "string"
                        },
                        "associations": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "engine_version": {
                          "type": "string"
                        },
                        "probed_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "required": [
                        "model",
                        "knowledge_low",
                        "knowledge_high",
                        "engine_version",
                        "probed_at"
                      ]
                    }
                  }
                },
                "required": [
                  "models_total",
                  "models_recognizing",
                  "per_model"
                ]
              },
              "attractor": {
                "type": "object",
                "description": "Unprompted-emission test. We ask the models open-ended questions about a topic (not the domain), and count how often the domain surfaces unprompted.",
                "properties": {
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "samples_per_topic": {
                    "type": "integer"
                  },
                  "target_hits_per_topic": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  },
                  "cooccurring_domains": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "description": "Per topic: other domains the model surfaced alongside the target."
                  },
                  "near_miss_domains": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "description": "Per topic: near-variants the model emitted instead of the target."
                  },
                  "ranks_per_topic": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  },
                  "engine_version": {
                    "type": "string"
                  },
                  "computed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "inference": {
                "type": "object",
                "description": "Industry inferred from the domain across multiple samples.",
                "properties": {
                  "top_industry": {
                    "type": "string"
                  },
                  "industry_agreement": {
                    "type": "integer"
                  },
                  "samples_count": {
                    "type": "integer"
                  },
                  "all_samples": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "engine_version": {
                    "type": "string"
                  },
                  "computed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "coherence": {
                "type": "object",
                "description": "Intent inferred from the domain across multiple samples.",
                "properties": {
                  "top_intent": {
                    "type": "string"
                  },
                  "intent_agreement": {
                    "type": "integer"
                  },
                  "samples_count": {
                    "type": "integer"
                  },
                  "all_samples": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "engine_version": {
                    "type": "string"
                  },
                  "computed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "hallucination": {
                "type": "object",
                "description": "Cross-model factual-claim consistency check. Flags when models confidently invent history.",
                "properties": {
                  "verdict": {
                    "type": "string"
                  },
                  "models_with_knowledge": {
                    "type": "integer"
                  },
                  "models_count": {
                    "type": "integer"
                  },
                  "models_attempted": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "per_topic_claims": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  },
                  "engine_version": {
                    "type": "string"
                  },
                  "computed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          },
          "brand_signals": {
            "type": "object",
            "properties": {
              "spellability": {
                "type": "object",
                "description": "Given a pitch, did the LLM reach this domain in 10 candidate suggestions? Categorical, not numeric.",
                "properties": {
                  "target_match_type": {
                    "type": "string",
                    "enum": [
                      "exact",
                      "sld_match",
                      "variant",
                      "miss"
                    ]
                  },
                  "target_rank": {
                    "type": "integer",
                    "description": "1-based position of the best match (1 = first)."
                  },
                  "suggestions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "domain": {
                          "type": "string"
                        },
                        "reasoning": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "domain"
                      ]
                    }
                  },
                  "engine_version": {
                    "type": "string"
                  },
                  "computed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                },
                "required": [
                  "target_match_type",
                  "engine_version",
                  "computed_at"
                ]
              },
              "lookalike_neighbors": {
                "type": "array",
                "description": "Domains the frontier models surfaced alongside the target \u2014 typo neighbours and brand confusables.",
                "items": {
                  "type": "object",
                  "properties": {
                    "variant": {
                      "type": "string"
                    },
                    "match_type": {
                      "type": "string",
                      "enum": [
                        "tld_swap",
                        "variant"
                      ]
                    },
                    "models_surfaced": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "snippets": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "variant",
                    "match_type",
                    "models_surfaced"
                  ]
                }
              },
              "commercial_intent": {
                "type": "object",
                "properties": {
                  "score": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "bucket": {
                    "type": "string"
                  },
                  "markers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "tokens": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Tokenization of the SLD."
                  }
                },
                "required": [
                  "score",
                  "bucket"
                ]
              }
            }
          },
          "free_tier_probes": {
            "type": "array",
            "description": "Probes from non-frontier (free-tier) models. Cheaper, less weight-bearing \u2014 kept separate from frontier_recall.",
            "items": {
              "type": "object",
              "properties": {
                "model": {
                  "type": "string"
                },
                "knowledge_low": {
                  "type": "integer"
                },
                "knowledge_high": {
                  "type": "integer"
                },
                "summary": {
                  "type": "string"
                },
                "engine_version": {
                  "type": "string"
                },
                "probed_at": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "required": [
                "model",
                "knowledge_low",
                "knowledge_high",
                "engine_version",
                "probed_at"
              ]
            }
          },
          "alternative_names": {
            "type": "array",
            "description": "Alternative names suggested by the recommendations engine. Present only when a recommendations run was triggered.",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "recognition_level": {
                  "type": "string",
                  "enum": [
                    "none",
                    "vague",
                    "strong"
                  ]
                },
                "pitfalls_triggered": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "rationale": {
                  "type": "string"
                }
              },
              "required": [
                "domain",
                "recognition_level"
              ]
            }
          },
          "marketplace": {
            "type": "object",
            "description": "DNS-signature detection of for-sale or parked status. Cached domain-level for ~1h.",
            "properties": {
              "listed": {
                "type": "boolean",
                "description": "True when a named marketplace was detected."
              },
              "marketplace_name": {
                "type": "string",
                "description": "Marketplace name when listed; absent when only parked."
              },
              "parked": {
                "type": "boolean",
                "description": "True when the domain has parking-platform NS but no named marketplace match."
              }
            },
            "required": [
              "listed",
              "parked"
            ]
          }
        },
        "required": [
          "schema_version",
          "exported_at",
          "domain",
          "status",
          "history",
          "name_shape"
        ]
      },
      "Pricing": {
        "type": "object",
        "description": "Tiered pricing band for a domain. Returned by /v1/domains/{domain}/pricing as a slice; embedded in DomainReport for full report calls. Free text in `reasoning`, `key_factors`, and `risks` is capped at 4 KB per field.",
        "properties": {
          "wholesale": {
            "$ref": "#/components/schemas/PriceBand"
          },
          "aftermarket": {
            "$ref": "#/components/schemas/PriceBand"
          },
          "brokered": {
            "$ref": "#/components/schemas/PriceBand"
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "reasoning": {
            "type": "string",
            "description": "Markdown. Market-expert prose explaining the band."
          },
          "key_factors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 5 reasons the band lands where it does."
          },
          "risks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 5 risks that bound the upside."
          },
          "input_summary": {
            "type": "object",
            "additionalProperties": true,
            "description": "Snapshot of the signal vector that fed the pricing model. Useful for auditing."
          },
          "model": {
            "type": "string",
            "description": "Identifier of the pricing model that produced this row.",
            "example": "opus-class"
          },
          "engine_version": {
            "type": "string",
            "description": "Pricing prompt version. Bumps invalidate cached rows."
          },
          "computed_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "wholesale",
          "aftermarket",
          "brokered",
          "confidence",
          "model",
          "engine_version",
          "computed_at"
        ]
      },
      "PriceBand": {
        "type": "object",
        "properties": {
          "low": {
            "type": "integer",
            "minimum": 0
          },
          "mid": {
            "type": "integer",
            "minimum": 0
          },
          "high": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "low",
          "mid",
          "high"
        ]
      },
      "JobDomainStatus": {
        "type": "string",
        "enum": [
          "pending",
          "running",
          "paid",
          "cached",
          "failed"
        ],
        "description": "Per-domain lifecycle inside a research job. `pending` = waiting to start; `running` = appraisal in progress; `paid` = finished successfully and the credits for this name were committed (1 credit for a quick read, 10 credits for a full read); `cached` = your organization already had a finished report for this name at submission time, so no new appraisal ran and no credits were charged for it; `failed` = the appraisal couldn't finish and the credits reserved for this name were returned to your wallet."
      },
      "JobReportEntry": {
        "type": "object",
        "description": "One row in /v1/research-jobs/{id}/report. `report` is present only for `paid` and `cached` outcomes; `error` is present only for `failed`. Pending and running entries carry just `domain` + `job_status`.",
        "properties": {
          "domain": {
            "type": "string",
            "example": "acme.ai"
          },
          "job_status": {
            "$ref": "#/components/schemas/JobDomainStatus"
          },
          "report": {
            "$ref": "#/components/schemas/DomainReport"
          },
          "error": {
            "type": "string",
            "description": "Stable, machine-readable code for why this name's appraisal couldn't finish. Examples: `concurrent_run_winner`, `fast_fail_pricing`, `model_timeout`. The same codes appear elsewhere in the API; safe to branch on."
          }
        },
        "required": [
          "domain",
          "job_status"
        ]
      },
      "JobPricingEntry": {
        "type": "object",
        "description": "One row in /v1/research-jobs/{id}/report/pricing. `pricing` is the full Pricing slice (numbers + reasoning + key_factors + risks, all 4 KB-capped) for `paid` and `cached` entries.",
        "properties": {
          "domain": {
            "type": "string",
            "example": "acme.ai"
          },
          "job_status": {
            "$ref": "#/components/schemas/JobDomainStatus"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "domain",
          "job_status"
        ]
      },
      "JobReportsResponse": {
        "type": "object",
        "description": "Wrapper for /v1/research-jobs/{id}/report. The `job` block embeds the same shape returned by /v1/research-jobs/{id} so customers can render progress + per-domain reports from a single call.",
        "properties": {
          "job": {
            "$ref": "#/components/schemas/ResearchJob"
          },
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobReportEntry"
            }
          }
        },
        "required": [
          "job",
          "reports"
        ]
      },
      "JobPricingResponse": {
        "type": "object",
        "properties": {
          "job": {
            "$ref": "#/components/schemas/ResearchJob"
          },
          "pricing": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobPricingEntry"
            }
          }
        },
        "required": [
          "job",
          "pricing"
        ]
      }
    }
  }
}
