Get started
Introduction
DNA is a search engine that learns from your feedback.
You send embeddings of your items and your queries. DNA returns the best item for each query, you score it, and the next search is better. DNA never sees your data, only vectors, keys and scores.
Quick start
-
Get a key
Write to office@hashgrid.ai. Send the key with every request to
https://dna.hashgrid.ai/api/v1.Authorization: Bearer hg-… -
Add your items
Embed each item with any model, and give it a
keyunder one prefix, like/items/. Keep the ID you get back.POST /nodes { "key": "/items/42", "emb": [0.012, -0.045, …] } ← { "id": "f43125171437b656", … } -
Add a query
Embed what you are looking for with the same model. No key: a query searches, it isn't searched for.
POST /nodes { "emb": [0.031, 0.008, …] } ← { "id": "93d77a9cf9dcdcbb", … } -
Search
Ask for a result under your items' prefix. You get the best item DNA has not shown this query yet.
POST /nodes/93d77a9cf9dcdcbb/edges { "prefix": "/items/" } ← { "id": "f43125171437b656", "score": null } -
Score it
Tell DNA how good the result was, from -1 to 1.
PATCH /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656 { "score": 0.72 }
Anonymity
DNA is anonymous by design: it only ever receives vectors, random IDs, the keys you choose, and scores, never
your data, your prompts or your users. That split is deliberate. Keys are stored as you send them, so use
opaque ones, like /items/42, if names are sensitive. Three things stay with you, and you manage
them:
- The embedder: the model that turns your items and queries into vectors.
- The judge: whatever scores results, be it an LLM, a smaller model, or your users.
- The collection: your items themselves, and which ID or key belongs to which item.
Connect an agent (MCP)
DNA is also an MCP server, at https://dna.hashgrid.ai/mcp, so agents can use it as a tool. It
takes the same API key (no key yet? write to us), and like the API it
takes embeddings, not text. In Claude Code:
claude mcp add --transport http dna https://dna.hashgrid.ai/mcp \
--header "Authorization: Bearer hg-…" In Cursor and other clients configured with JSON:
{
"mcpServers": {
"dna": {
"url": "https://dna.hashgrid.ai/mcp",
"headers": { "Authorization": "Bearer hg-…" }
}
}
} There is one tool per API route, named after it, taking the same fields; create_edge is the
search:
get_grid: GET /list_nodes, get_node, create_node, update_node, delete_node: the node routeslist_edges, get_edge, create_edge, put_edge, update_edge, delete_edge: the edge routes
Concepts
Grids
A grid is your space: a collection of items and queries, and your scores. Each grid has exactly one API key, and each key opens exactly one grid.
Nodes
A node is anything DNA can search for or search with. It holds:
key(optional): where the node lives, a path like/items/42, unique in the grid. Searches look under a prefix of it, like/items/.emb: its embedding, what the node is, from any embedding model. Use one model per grid.
Creating a node
POST /nodes
Send its embedding, emb, 1 to 2048 numbers, and for an item a key: a path of up to
1024 characters, unique in the grid. A key that is already taken gives a 409.
POST /nodes
{ "key": "/items/42", "emb": [0.012, -0.045, …] }
← 201 {
"id": "f43125171437b656",
"key": "/items/42",
"emb": [0.012, -0.045, …]
} Getting a node
GET /nodes/{nid}
Returns its key, null if it has none, and its embedding.
GET /nodes/f43125171437b656
← 200 {
"id": "f43125171437b656",
"key": "/items/42",
"emb": [0.012, -0.045, …]
} Listing nodes
GET /nodes
Up to limit nodes (32 by default, at most 1024). For the next page, pass the last ID as
cursor.
GET /nodes?limit=100&cursor=f43125171437b656
← 200 [{ "id": "…", "key": "…", "emb": […] }, …] Updating a node
PATCH /nodes/{nid}
Replace the embedding, move the node to another key, or send "key": null so it can no longer
be found. Fields you leave out stay as they are. Its scores are kept, and keep training on the new embedding.
PATCH /nodes/f43125171437b656
{ "key": "/archive/items/42" }
← 200 {
"id": "f43125171437b656",
"key": "/archive/items/42",
"emb": [0.012, -0.045, …]
} Deleting a node
DELETE /nodes/{nid}
Deletes the node and its edges, both ways: it no longer searches or shows up in results, and its scores no longer train DNA.
DELETE /nodes/f43125171437b656
← 204 Edges
An edge is a result for a query: a pair of nodes and a score from -1 (wrong) to 1 (perfect), or
null until scored. Searching creates edges, and scoring them is how DNA learns. Scores can come from an LLM, a smaller model, or your
users.
Searching
POST /nodes/{nid}/edges
Returns the best node under prefix that this query hasn't seen yet, as a new edge with no
score until you score it. Leave the prefix out to search every node with a key. Call again for the next one;
a 400 means there are none left.
POST /nodes/93d77a9cf9dcdcbb/edges
{ "prefix": "/items/" }
← 201 {
"id": "f43125171437b656",
"score": null
} Scoring a result
PATCH /nodes/{nid}/edges/{eid}
Set your score, from -1 to 1. Only scores train DNA: a result you never score doesn't.
PATCH /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656
{ "score": 0.72 }
← 200 {
"id": "f43125171437b656",
"score": 0.72
} Scoring any pair
PUT /nodes/{nid}/edges/{eid}
Already know how well an item fits a query? Score the pair directly, no search needed.
PUT /nodes/93d77a9cf9dcdcbb/edges/2b1f0c9e4d7a8f31
{ "score": 1 }
← 201 {
"id": "2b1f0c9e4d7a8f31",
"score": 1.0
} Getting an edge
GET /nodes/{nid}/edges/{eid}
One result and its score, null if it has none yet.
GET /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656
← 200 {
"id": "f43125171437b656",
"score": 0.72
} Listing edges
GET /nodes/{nid}/edges
A query's results so far, paged like nodes, with the last id as cursor.
GET /nodes/93d77a9cf9dcdcbb/edges?limit=100
← 200 [{ "id": "…", "score": 0.72 }, …] Deleting an edge
DELETE /nodes/{nid}/edges/{eid}
Forgets the pair, so it can come up again. Its score no longer trains DNA.
DELETE /nodes/93d77a9cf9dcdcbb/edges/f43125171437b656
← 204 Guides
Warming up your grid
A new grid knows only your embeddings, so its first results are rough. A warm-up teaches it your data before real searches arrive.
- Add all your items first. DNA can only return what it has.
- Give it scores: from a dataset you already have, or by searching and scoring warm-up queries.
If you have an existing dataset
Already know how well some items fit some queries, from labels or past clicks? Score those pairs directly, no search needed. They train DNA like any other score.
# Scores you already have, from labels or past clicks.
for query_id, item_id, score in dataset:
dna.put(f"/nodes/{query_id}/edges/{item_id}", json={"score": score}) If you don't
Add a set of warm-up queries, close to what your users will ask. Then search and score in passes: one result per query, then the next pass.
# Several passes over your warm-up queries, one result each per pass.
for _ in range(10):
for query_id in queries:
r = dna.post(f"/nodes/{query_id}/edges")
if r.status_code == 400: # nothing left for this query
continue
item_id = r.json()["id"]
score = judge(queries[query_id], items[item_id])
dna.patch(f"/nodes/{query_id}/edges/{item_id}", json={"score": score}) Keep scoring after the warm-up, less and less. See when to stop scoring.
When to stop scoring
As DNA learns, its order gets closer to what your judge would say, and you can skip the judge more often. That is where the savings come from. A simple way to decide: score only the first few results of each search, say 3, and trust DNA's order for the rest.
for rank in range(10):
r = dna.post(f"/nodes/{query_id}/edges", json={"prefix": "/items/"})
if r.status_code == 400: # nothing left for this query
break
item_id = r.json()["id"]
if rank < 3: # the first few: ask your judge
score = judge(queries[query_id], items[item_id])
dna.patch(f"/nodes/{query_id}/edges/{item_id}", json={"score": score})
use(items[item_id]) # the rest: trust DNA's order Start strict and loosen as DNA's order proves right. The numbers above are only a starting point.