Every result on this page came from the public Terpedia Knowledge API at build time. Nothing is mocked. The same service powers kb.terpedia.com and chat.terpedia.com.
The endpoint used throughout is the open encyclopedia resolver:
GET https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app/v1/cyc/{keyword}It accepts a compound name, synonym, cultivar name, or protein name and returns:
| Key | What it holds |
|---|---|
entry | The resolved entity: label, type, identifiers, structure, provenance |
entities | Related entities in the graph (up to 30) |
statements | Typed relationships (found_in_taxon, contains, associated_with ...), each with sources |
citations | The literature and database records behind those statements |
context | A plain-text evidence summary that begins with the instruction do not extend claims beyond the cited evidence |
Partner endpoints (/v1/tabular/search, /v1/tabular/count, /v1/semantic/search, /v1/entities/{id}/neighbors) require an API key and are described in the white paper.
import json, sys
API = "https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app"
def kb(keyword: str) -> dict:
"""Resolve a keyword with the Terpedia Knowledge API.
Works in CPython (urllib) and in the browser under Pyodide (fetch).
Returns {} when the API has no entry for the keyword (HTTP 404).
"""
from urllib.parse import quote
url = f"{API}/v1/cyc/{quote(keyword)}"
if sys.platform == "emscripten": # JupyterLite / Pyodide
from pyodide.http import open_url
text = open_url(url).read()
data = json.loads(text)
return {} if "error" in data else data
from urllib.request import urlopen
from urllib.error import HTTPError
try:
with urlopen(url, timeout=60) as r:
return json.load(r)
except HTTPError as e:
if e.code == 404:
return {}
raise
print("helper ready ->", API)helper ready -> https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app
1. Resolve a name to a chemical identity¶
The first thing any downstream system needs is an unambiguous identity. A trade name, a lab-report label and a literature synonym must all land on the same record. Terpedia returns the structure (InChI, InChIKey, SMILES) and the cross-references that let you join to PubChem, ChEBI, ChEMBL, CAS, KEGG, FooDB and others.
d = kb("limonene")
e = d["entry"]
print("label :", e["label"])
print("type :", e["type"])
print("formula :", e.get("molecularFormula"))
print("SMILES :", e.get("smiles"))
print("InChIKey :", e["identifiers"].get("inchiKey"))
print("class :", e.get("taxonomy", {}).get("direct_parent"))
print()
print("cross-references:")
for k, v in sorted(e["identifiers"].items()):
print(f" {k:12s} {v}")label : (-)-Limonene
type : chemical
formula : C10H16
SMILES : CC(=C)[C@@H]1CCC(C)=CC1
InChIKey : XMGQYMWWDOXHJM-JTQLQIEISA-N
class : Menthane monoterpenoids
cross-references:
biocyc CPD-4886
cannabisdb CDB005271
cas 5989-27-5
chebi 15382
chembl CHEMBL15799
chemspider 389747
drugbank DB08921
foodb FDB006329
inchiKey XMGQYMWWDOXHJM-JTQLQIEISA-N
kegg C06099
knapsack C00010868
pubchem 440917
pubchemCid 22311
wikidata Q278809
wikipedia Limonene
Every record carries its own provenance: which dataset it came from, the source record id, when it was retrieved, and under which license it may be reused. That last field matters for commercial users; it is how Terpedia keeps license-scoped sources visible instead of laundering them into one undifferentiated table.
p = e["provenance"]
for k in ("datasetId", "datasetVersion", "sourceRecordId", "curationStatus",
"licenseTerms", "license", "retrievedAt"):
print(f"{k:16s} {p.get(k)}")datasetId cannabisdb
datasetVersion 1.0-a7d05067088ee5b8
sourceRecordId CDB005271
curationStatus source_curated
licenseTerms CC BY-NC 4.0
license https://creativecommons.org/licenses/by-nc/4.0/
retrievedAt 2026-09-06T11:18:33.538Z
2. Why identity resolution is hard: one molecule, dozens of names¶
Limonene alone arrives under many names in COAs, ingredient lists, and papers.
The alias list is what lets a search for d-limonene, (+)-limonene,
dipentene, or a CAS number resolve to the same node.
aliases = e["aliases"]
print(len(aliases), "aliases on record. A sample:")
for a in aliases[:18]:
print(" -", a)58 aliases on record. A sample:
- (+)-(4R)-Limonene
- (+)-(R)-Limonene
- (+)-4-Isopropenyl-1-methylcyclohexene
- (+)-Limonene
- (4R)-1-Methyl-4-isopropenylcyclohex-1-ene
- (4R)-4-Isopropenyl-1-methylcyclohexene
- (R)-(+)-Limonene
- (R)-(+)-p-Mentha-1,8-diene
- (R)-1-Methyl-4-(1-methylethenyl)cyclohexene
- (R)-4-Isopropenyl-1-methyl-1-cyclohexene
- (R)-p-Mentha-1,8-diene
- 4BetaH-p-mentha-1,8-diene
- D-(+)-Limonene
- D-Limonen
- AISA 5203-L (+)limonene
- Dipentene
- (-)-Limonene
- 1-Methyl-4-(1-methylethenyl)cyclohexene
3. Statements with sources: what is this molecule associated with?¶
Beyond identity, the graph holds typed statements. For a compound these
include taxa it has been reported in (found_in_taxon), protein associations
from the source database (associated_with), and class membership. Each
statement is a claim with a source, not a fact about biology. Note the
evidenceType field: source_curated_protein_association tells you exactly
what kind of evidence you are looking at.
from collections import Counter
stmts = d["statements"]
print("statement predicates for limonene:")
for pred, n in Counter(s["predicate"] for s in stmts).most_common():
print(f" {n:3d} {pred}")
print()
print("protein associations (source-curated, not efficacy):")
for s in stmts:
if s["predicate"] == "associated_with":
src = (s.get("sources") or [{}])[0]
print(f" {s['subjectLabel']} -> {s['objectLabel']:<32s} "
f"[{src.get('evidenceType')}] {src.get('url')}")statement predicates for limonene:
20 found_in_taxon
3 part_of
2 associated_with
2 has_part
1 classified_as
1 described_as
1 has_molecular_formula
1 image
1 mass
1 canonical_smiles
1 inchi
1 inchi_key
1 chemical_formula
1 subclass_of
1 instance_of
1 chembl_id
1 pubchem_cid
protein associations (source-curated, not efficacy):
(-)-Limonene -> Cytochrome P450 2C9 [source_curated_protein_association] https://cannabisdatabase.ca/proteins/CDBP00973/compound_protein_links
(-)-Limonene -> Cytochrome P450 2C19 [source_curated_protein_association] https://cannabisdatabase.ca/proteins/CDBP00975/compound_protein_links
4. Resolve a protein target¶
The same resolver handles proteins. CB2 lands on the reviewed UniProt entry
P34972 from Terpedia’s curated core graph; CYP2C9 and TRPV1 come from the
CannabisDatabase.ca protein set and bring their compound associations with
them.
for name in ("CB2", "CYP2C9", "TRPV1"):
r = kb(name)
if not r:
print(f"{name}: no entry")
continue
ent = r["entry"]
assoc = [s for s in r.get("statements", []) if s["predicate"] == "associated_with"]
print(f"{name:7s} -> {ent['label']:<48s} id={ent['id']:<16s} "
f"source={ent['provenance']['datasetId']:<24s} associations={len(assoc)}")CB2 -> Cannabinoid receptor 2 id=uniprot:P34972 source=terpedia-curated-core associations=1
CYP2C9 -> Cytochrome P450 2C9 id=CDBP00973 source=cannabisdb associations=16
TRPV1 -> Transient receptor potential cation channel subfamily V member 1 id=CDBP01848 source=cannabisdb associations=6
r = kb("CYP2C9")
print("compounds associated with", r["entry"]["label"], "in the source graph:")
seen = set()
for s in r["statements"]:
if s["predicate"] == "associated_with" and s["subjectLabel"] not in seen:
seen.add(s["subjectLabel"])
print(" -", s["subjectLabel"])compounds associated with Cytochrome P450 2C9 in the source graph:
- Delta-9-tetrahydrocannabinol
- Limonene
- Formic acid
- NADP
- NADPH
- Oxygen
- Salicylic acid
- Water
- Rutin
- Hydrogen sulfide
- Heme
- (-)-Limonene
- Dopamine
- Paraxanthine
- Nicotine
- Ethanol
5. Compare a panel of terpenes in one pass¶
A formulator or QA lead rarely wants one molecule. This loop pulls a small panel and shows how much the graph currently knows about each: formula, mass, number of reported taxa, number of statements, and which dataset supplied the primary record. Where a record is thin, the table says so; a missing value is reported as missing, not filled in.
panel = ["myrcene", "limonene", "linalool", "alpha-pinene", "caryophyllene",
"humulene", "valencene", "nootkatone"]
rows = []
for name in panel:
r = kb(name)
if not r:
rows.append((name, "not found", "", "", "", "", ""))
continue
ent = r["entry"]
st = r.get("statements", [])
taxa = sum(1 for s in st if s["predicate"] == "found_in_taxon")
mass = ""
for s in st:
if s["predicate"] == "mass":
raw = str(s.get("objectLabel") or s.get("objectValue") or s.get("value") or "")
mass = raw.split()[0] if raw.split() else ""
break
rows.append((name, ent["type"], ent.get("molecularFormula") or "", mass,
str(taxa), str(len(st)), ent["provenance"]["datasetId"]))
hdr = ("keyword", "type", "formula", "mass", "taxa", "stmts", "primary source")
w = [max(len(str(x[i])) for x in rows + [hdr]) for i in range(len(hdr))]
print(" ".join(h.ljust(w[i]) for i, h in enumerate(hdr)))
print(" ".join("-" * w[i] for i in range(len(hdr))))
for row in rows:
print(" ".join(str(c).ljust(w[i]) for i, c in enumerate(row)))keyword type formula mass taxa stmts primary source
------------- ---------------------- ------- ---------- ---- ----- -------------------------
myrcene chemical C10H16 136.125201 25 40 cannabisdb
limonene chemical C10H16 136.125201 20 40 cannabisdb
linalool terpene C₁₀H₁₈O 154.135765 27 40 terpedia-terpene-registry
alpha-pinene terpene C₁₀H₁₆ 136.125 27 40 terpedia-terpene-registry
caryophyllene essential_oil_compound C₁₅H₂₄ 204.187801 28 40 terpedia-terpene-registry
humulene chemical C15H24 204.187801 24 40 cannabisdb
valencene essential_oil_compound C₁₅H₂₄ 204.188 29 40 terpedia-terpene-registry
nootkatone essential_oil_compound C₁₅H₂₂O 218.167065 25 35 terpedia-terpene-registry
6. From a cultivar name to quantified chemistry, with the paper behind it¶
Cultivar names are notoriously unreliable, so Terpedia never treats a name as
chemistry. What it can do is return the measured records that exist for a
name, each with its quantification, status, and the publication it came from.
Here the contains statements for Blue Dream carry concentrations in mg/g
dry weight and resolve to a PubMed identifier. A compound appears once per
measured sample, so repeated rows are separate measurements, not duplicates.
r = kb("Blue Dream")
ent = r["entry"]
print(ent["label"], "-", ent["type"], "-", ent["id"])
print()
print(f"{'compound':<36s} {'value':<26s} status")
for s in r["statements"][:14]:
if s["predicate"] != "contains":
continue
q = s.get("qualifiers", {})
print(f"{s['objectLabel']:<36s} {q.get('value',''):<26s} {q.get('status','')}")
src = (r["statements"][0].get("sources") or [{}])[0]
print()
print("source:", src.get("title", "")[:140], "...")
print("pmid :", src.get("pmid"), "->", src.get("url"))
print("evidence type:", src.get("evidenceType"))Blue Dream - cannabis_cultivar - STRAIN0007
compound value status
Cannabichromene 1.3 +/- 0.1 mg/g dry wt Detected and Quantified
Cannabidiol 0.5 +/- 0.1 mg/g dry wt Detected and Quantified
Delta-9-tetrahydrocannabinol 109.9 +/- 24.1 mg/g dry wt Detected and Quantified
Delta-9-tetrhydrocannabivarin 0.3 +/- 0.1 mg/g dry wt Detected and Quantified
Delta-9-cis-tetrahydrocannabinol 109.9 +/- 24.1 mg/g dry wt Detected and Quantified
(+)-nerolidol 0.353 mg/g dry wt Detected and Quantified
(+)-nerolidol 0.414 mg/g dry wt Detected and Quantified
(+)-nerolidol 0.794 mg/g dry wt Detected and Quantified
(+)-nerolidol 0.8 mg/g dry wt Detected and Quantified
beta-Myrcene 2.146 mg/g dry wt Detected and Quantified
beta-Myrcene 2.405 mg/g dry wt Detected and Quantified
beta-Myrcene 5.113 mg/g dry wt Detected and Quantified
beta-Myrcene 6.512 mg/g dry wt Detected and Quantified
beta-Myrcene 7.5 +/- 2.8 mg/g dry wt Detected and Quantified
source: Richins RD, Rodriguez-Uribe L, Lowe K, Ferral R, O'Connell MA: Accumulation of bioactive metabolites in cultivated medical Cannabis. PLoS On ...
pmid : 30036388 -> https://pubmed.ncbi.nlm.nih.gov/30036388/
evidence type: curated_cultivar_concentration
7. Where the compound has been reported: occurrence statements¶
found_in_taxon statements link a compound to organisms in which it has been
reported. An occurrence is a report that a compound was detected in an
organism in a cited work. It is not a concentration and not evidence that the
organism is a meaningful commercial source. The statement tells you where the
record came from and under what license. Where a taxon label has not yet been
resolved, the bare Wikidata identifier is shown; that coverage gap is visible
rather than papered over.
r = kb("linalool")
occ = [s for s in r["statements"] if s["predicate"] == "found_in_taxon"]
print(len(occ), "occurrence statements returned for linalool (page 1). First ten:")
for s in occ[:10]:
src = (s.get("sources") or [{}])[0]
print(f" {s['objectLabel']:<32s} {src.get('datasetId'):<10s} {src.get('license')}")27 occurrence statements returned for linalool (page 1). First ten:
Baccharis dracunculifolia wikidata https://creativecommons.org/publicdomain/zero/1.0/
Pilocarpus grandiflorus wikidata https://creativecommons.org/publicdomain/zero/1.0/
Myrcianthes cisplatensis wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q15583465 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q15586596 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q13939086 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q536839 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q15583174 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q3595850 wikidata https://creativecommons.org/publicdomain/zero/1.0/
Q28801933 wikidata https://creativecommons.org/publicdomain/zero/1.0/
8. The evidence boundary is part of the payload¶
Every response includes a context block written for downstream language
models. It opens with an explicit instruction not to extend claims beyond the
cited evidence, then lists the statements with their sources. This is how the
chat agents at chat.terpedia.com are kept honest: the knowledge layer, not the
prompt, sets the boundary.
ctx = kb("limonene")["context"]
print(ctx[:1100])
print("...")Verified Terpedia semantic graph (do not extend claims beyond the cited evidence):
- (-)-Limonene --associated_with--> Cytochrome P450 2C9 [https://cannabisdatabase.ca/proteins/CDBP00973/compound_protein_links]
- (-)-Limonene --associated_with--> Cytochrome P450 2C19 [https://cannabisdatabase.ca/proteins/CDBP00975/compound_protein_links]
- (-)-Limonene --classified_as--> Menthane monoterpenoids [https://cannabisdatabase.ca/compounds/CDB005271]
- (-)-Limonene --described_as--> d-Limonene, also known as dipentene, belongs to the class of organic compounds known as menthane monoterpenoids. These are monoterpenoids with a structure based on the o-, m-, or p-menthane backbone. P-menthane consists of the cyclohexane ring with a methyl group and a (2-methyl)-propyl group at the 1 and 4 ring position, respectively. The o- and m- menthanes are much rarer, and presumably arise by alkyl migration of p-menthanes. Thus, D-limonene is considered to be an isoprenoid lipid molecule. d-Limonene is a very hydrophobic molecule, practically insoluble (in water), and relatively neutral. (-)-Limonene is e
...
9. Honest misses¶
A keyword the graph cannot resolve returns HTTP 404 and {"error": "entry not found"}
rather than a fuzzy guess. That is deliberate. Coverage is versioned and
expanding; the counting reports in the white paper describe exactly what is
and is not in scope today.
for k in ("thujone", "artemisinin", "not-a-real-compound"):
print(f"{k:22s} ->", "found" if kb(k) else "no entry (404)")thujone -> no entry (404)
artemisinin -> no entry (404)
not-a-real-compound -> no entry (404)
10. The same data, rendered¶
The entity pages on kb.terpedia.com are built from the same responses shown above.
Figure 1:The live limonene entity page at kb.terpedia.com.
Ask a question in natural language at chat.terpedia.com, or browse the graph by category (terpenes, molecules, proteins, enzymes, reactions, pathways, products, ingredients, diseases, organisms) at kb.terpedia.com.