Ingot
API v1Get the source
FeaturesFive ways in, one store

Ask it
any way

Every ingot is a set of real tables. Filter them with SQL, rank them by meaning, match names that were misspelled, and do all three in the same query.

Structured search

Exact answers, written in SQL

Each ingot stores tool results as typed tables. The model writes the query it needs and gets back only the rows that answer it, so a count comes back as one number rather than forty records.

POST/api/v1/acme/ing_01H8Z…/query
{ "sql": "SELECT company, arr
          FROM contacts
          WHERE stage = 'won'
            AND closed_at >= '2026-07-01'
          ORDER BY arr DESC
          LIMIT 3" }
200 · 3 rows · 34 ms
{ "columns": ["company", "arr"],
  "rows": [
    { "company": "Northwind", "arr": 184000 },
    { "company": "Contoso", "arr": 96500 },
    { "company": "Fabrikam", "arr": 71200 } ],
  "truncated": false,
  "next": null }
Reach for it when
  • The question has a number in the answer: how many, how much, the largest
  • Order matters, such as first, latest or top five
  • The answer is something missing, like records with no owner
Not the right tool when

The wording in the question differs from the wording in the record. SQL matches values, not meaning. Use similarity search for that.

Similarity search

Find it by what it means

Declare a column with embed: true on /add and Ingot embeds it after the write. Send text with a query and it is embedded too, bound as $q, so a question about running out of database connections finds the record that says the pool was exhausted.

POST/api/v1/acme/ing_01H8Z…/query
{ "text": "ran out of spare database connections",
  "sql": "SELECT id, summary,
            array_cosine_similarity(summary_vec, $q)
              AS score
          FROM incidents
          ORDER BY score DESC
          LIMIT 3" }
200 · 3 rows · 41 ms
{ "columns": ["id", "summary", "score"],
  "rows": [
    { "id": "INC-01",
      "score": 0.83,
      "summary": "Connection pool exhausted on catalog-db" },
    { "id": "INC-09",
      "score": 0.71,
      "summary": "Replica lag after pool resize" },
    { "id": "INC-04",
      "score": 0.64,
      "summary": "Timeouts from search under load" } ] }
Reach for it when
  • The question paraphrases the record rather than quoting it
  • You need the closest few matches, not every match
  • The text is written by people, like summaries, notes and descriptions
Not the right tool when

You need every matching row, or a count of them. Similarity returns a ranking, not a complete set. Filter with SQL first.

Fuzzy search

Close enough, on purpose

Names get misspelled, in the data and in the question. The query runs in DuckDB, so its string-distance functions come with it: jaro_winkler_similarity finds John Kowalski from "Jon Kowalsky", and "north wind" finds Northwind. Nothing is embedded, so it works on any text column.

POST/api/v1/acme/ing_01H8Z…/query
{ "sql": "SELECT id, name, company
          FROM contacts
          WHERE jaro_winkler_similarity(
            lower(name), 'jon kowalsky') > 0.85" }
200 · 1 row · 22 ms
{ "columns": ["id", "name", "company"],
  "rows": [
    { "id": "c_2291",
      "name": "John Kowalski",
      "company": "Contoso" } ] }
Reach for it when
  • Looking up a person, company or product by a name someone typed
  • Identifiers that vary in case, spacing or punctuation
  • Joining two payloads whose keys almost match
Not the right tool when

The strings share no characters, like "outage" and "incident". Edit distance can’t link synonyms. Similarity search can.

Document chunking

Long text, in pieces that fit

A contract or a runbook is too long to embed as one value and too long to read back whole. Upload it to /file and Ingot parses, chunks and embeds it in the background. Each chunk keeps its page and section heading, so an answer can say where it came from.

POST/api/v1/acme/ing_01H8Z…/file
-F "file=@q3-contracts.pdf"
-F 'body={ "chunkTokens": 800,
           "overlapTokens": 120 }'
201 · stored and queued
{ "fileId": "file_3f9c1a…",
  "mediaType": "application/pdf",
  "status": "pending",
  "chunksQuery": "SELECT … FROM ingot_file_chunks
                  WHERE file_id = 'file_3f9c1a…'
                  ORDER BY ordinal" }
Reach for it when
  • You have documents, threads or transcripts rather than tool results
  • You want the paragraph that answers the question, not the whole document
  • You need to cite where an answer came from, by page or section
Not the right tool when

The text is already short values in your own tables, like names or one-line summaries. Chunking is for uploaded documents; embed those columns instead.

Receipts

Keep the payload, send the receipt

Some tools return more than the context window can hold. /add stores all of it either way; the receipt is what goes back to the model in its place. schema hands back the table and the queries that find these rows, and full adds a summary and a search term, written in the background.

POST/api/v1/acme/ing_01H8Z…/add
{ "table": "contacts",
  "rows": "$.contacts[*]",
  "key": ["id"],
  "receipt": "full",
  "result": toolResult }
201 · 21,507 tok stored
{ "table": "contacts",
  "rowsAdded": 412,
  "receipt": {
    "status": "pending",
    "receiptQuery": "SELECT … FROM ingot_receipts
                     WHERE source_batch = 'batch_1508c8…'" } }

# seconds later, receiptQuery answers
{ "summary": "412 EMEA accounts, 17 at risk",
  "search_term": "EMEA renewal risk" }
Reach for it when
  • A single tool call returns thousands of records
  • The agent needs to know what arrived before deciding what to ask
  • You are paying for tokens the model never uses
Not the right tool when

The result is small enough to read whole. A receipt then costs an extra query to get back what the model could have read directly.

Combining them

One query, three kinds of search

They are all one SELECT, so they compose. SQL narrows the rows exactly, fuzzy matching finds the name as someone typed it, and similarity puts what is left in order of meaning.

POST/api/v1/acme/ing_01H8Z…/query
{ "text": "cannot finish checkout",
  "sql": "SELECT t.id, t.subject
          FROM tickets t
          JOIN contacts c ON c.id = t.contact_id
          WHERE t.status = 'open'
            AND jaro_winkler_similarity(
              lower(c.company), 'north wind') > 0.85
          ORDER BY array_cosine_similarity(
            t.body_vec, $q) DESC
          LIMIT 3" }
  1. 01SQL filtersOnly open tickets, joined to the contact who raised them.
  2. 02Fuzzy matches"north wind" matches Northwind, however the company was typed.
  3. 03Similarity ranksWhat remains is ordered by how close the thread is to "cannot finish checkout".
200 · 3 rows · 58 ms
{ "columns": ["id", "subject"],
  "rows": [
    { "id": "T-4410", "subject": "Payment step spins forever" },
    { "id": "T-4398",
      "subject": "Card declined after address change" },
    { "id": "T-4371", "subject": "Order total shows zero" } ] }
[ Start ]

Cast your first ingot