alterlab-gtex — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited alterlab-gtex (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The Genotype-Tissue Expression (GTEx) project provides a comprehensive resource for studying tissue-specific gene expression and genetic regulation across 54 non-diseased human tissues from nearly 1,000 individuals. GTEx v10 (the latest release) enables researchers to understand how genetic variants regulate gene expression (eQTLs) and splicing (sQTLs) in a tissue-specific manner, which is critical for interpreting GWAS loci and identifying regulatory mechanisms.
Key resources:
Use GTEx when:
| Scenario | Use Instead |
|---|---|
| Curated trait-variant associations, reported traits, study provenance | alterlab-gwas |
| Population allele frequencies, LoF constraint (pLI/LOEUF) | alterlab-gnomad |
| Gene/transcript models, VEP consequences, ID mapping | alterlab-ensembl |
| Genetics-backed target-disease evidence scores (incl. colocalisation/L2G) | alterlab-opentargets |
| Single-cell expression atlases (cell-type resolution) | alterlab-cellxgene |
Base URL: https://gtexportal.org/api/v2/
The API returns JSON and does not require authentication. All endpoints support pagination.
import requests
BASE_URL = "https://gtexportal.org/api/v2"
def gtex_get(endpoint, params=None):
"""Make a GET request to the GTEx API."""
url = f"{BASE_URL}/{endpoint}"
response = requests.get(url, params=params, headers={"Accept": "application/json"})
response.raise_for_status()
return response.json()import requests
import pandas as pd
def get_gene_expression_by_tissue(gencode_id, dataset_id="gtex_v10"):
"""Get median gene expression across all tissues (versioned GENCODE ID required)."""
url = "https://gtexportal.org/api/v2/expression/medianGeneExpression"
params = {
"gencodeId": gencode_id,
"datasetId": dataset_id,
"itemsPerPage": 100
}
response = requests.get(url, params=params)
data = response.json()
records = data.get("data", [])
df = pd.DataFrame(records)
if not df.empty:
# v2 returns tissueSiteDetailId (not a display-name column), median, unit, geneSymbol
df = df[["geneSymbol", "tissueSiteDetailId", "median", "unit"]].sort_values(
"median", ascending=False
)
return df
# Example: get expression of APOE across tissues (v10 = GENCODE v39; APOE keeps .10 here)
df = get_gene_expression_by_tissue("ENSG00000130203.10") # APOE GENCODE ID
print(df.head(10))
# Output: tissueSiteDetailId, median TPM, sorted by highest expressionimport requests
import pandas as pd
def query_eqtl(gene_id, tissue_id=None, dataset_id="gtex_v10"):
"""Query significant eQTLs for a gene, optionally filtered by tissue."""
url = "https://gtexportal.org/api/v2/association/singleTissueEqtl"
params = {
"gencodeId": gene_id,
"datasetId": dataset_id,
"itemsPerPage": 250
}
if tissue_id:
params["tissueSiteDetailId"] = tissue_id
all_results = []
page = 0
while True:
params["page"] = page
response = requests.get(url, params=params)
data = response.json()
results = data.get("data", [])
if not results:
break
all_results.extend(results)
if len(results) < params["itemsPerPage"]:
break
page += 1
df = pd.DataFrame(all_results)
if not df.empty:
df = df.sort_values("pValue", ascending=True)
return df
# Example: Find eQTLs for PCSK9 (v10 GENCODE ID — version suffix must match the dataset)
df = query_eqtl("ENSG00000169174.11")
# v2 single-tissue eQTL fields: snpId, variantId, tissueSiteDetailId, nes, pValue, gencodeId
# (nes = normalized effect size of the alt allele; there is no separate slope/qval/maf here)
print(df[["snpId", "tissueSiteDetailId", "nes", "pValue", "gencodeId"]].head(20))import requests
def query_variant_eqtl(variant_id, tissue_id=None, dataset_id="gtex_v10"):
"""Get all eQTL associations for a specific variant."""
url = "https://gtexportal.org/api/v2/association/singleTissueEqtl"
params = {
"variantId": variant_id, # e.g., "chr1_55516888_G_GA_b38"
"datasetId": dataset_id,
"itemsPerPage": 250
}
if tissue_id:
params["tissueSiteDetailId"] = tissue_id
response = requests.get(url, params=params)
return response.json()
# GTEx variant ID format: chr{chrom}_{pos}_{ref}_{alt}_b38
# Example: "chr17_43094692_G_A_b38"import requests
def get_egenes(tissue_id, dataset_id="gtex_v10"):
"""Get all eGenes (genes with at least one significant eQTL) in a tissue."""
url = "https://gtexportal.org/api/v2/association/egene"
params = {
"tissueSiteDetailId": tissue_id,
"datasetId": dataset_id,
"itemsPerPage": 500
}
all_egenes = []
page = 0
while True:
params["page"] = page
response = requests.get(url, params=params)
data = response.json()
batch = data.get("data", [])
if not batch:
break
all_egenes.extend(batch)
if len(batch) < params["itemsPerPage"]:
break
page += 1
return all_egenes
# Example: all eGenes in whole blood
egenes = get_egenes("Whole_Blood")
print(f"Found {len(egenes)} eGenes in Whole Blood")import requests
def get_tissues(dataset_id="gtex_v10"):
"""Get all available tissues with metadata."""
url = "https://gtexportal.org/api/v2/dataset/tissueSiteDetail"
params = {"datasetId": dataset_id, "itemsPerPage": 100}
response = requests.get(url, params=params)
return response.json()["data"]
tissues = get_tissues()
# Key fields: tissueSiteDetailId, tissueSiteDetail, colorHex, samplingSite
# Common tissue IDs:
# Whole_Blood, Brain_Cortex, Liver, Kidney_Cortex, Heart_Left_Ventricle,
# Lung, Muscle_Skeletal, Adipose_Subcutaneous, Colon_Transverse, ...import requests
def query_sqtl(gene_id, tissue_id=None, dataset_id="gtex_v10"):
"""Query significant sQTLs for a gene."""
url = "https://gtexportal.org/api/v2/association/singleTissueSqtl"
params = {
"gencodeId": gene_id,
"datasetId": dataset_id,
"itemsPerPage": 250
}
if tissue_id:
params["tissueSiteDetailId"] = tissue_id
response = requests.get(url, params=params)
return response.json()chr{chrom}_{pos}_{ref}_{alt}_b38)coloc (R package) with full summary statisticsimport requests, pandas as pd
def interpret_gwas_variant(variant_id, dataset_id="gtex_v10"):
"""Find all genes regulated by a GWAS variant."""
url = "https://gtexportal.org/api/v2/association/singleTissueEqtl"
params = {"variantId": variant_id, "datasetId": dataset_id, "itemsPerPage": 500}
response = requests.get(url, params=params)
data = response.json()
df = pd.DataFrame(data.get("data", []))
if df.empty:
return df
return df[["geneSymbol", "tissueSiteDetailId", "nes", "pValue"]].sort_values("pValue")
# Example: rs2228145 (IL6R p.Asp358Ala). Resolve an rsID to the GTEx ID with
# GET /dataset/variant?snpId=rs2228145&datasetId=gtex_v10 -> "chr1_154454494_A_C_b38"
results = interpret_gwas_variant("chr1_154454494_A_C_b38")
if not results.empty:
print(results.groupby("geneSymbol")["tissueSiteDetailId"].count().sort_values(ascending=False))| Endpoint | Description |
|---|---|
/expression/medianGeneExpression | Median TPM by tissue for a gene |
/expression/geneExpression | Full distribution of expression per tissue |
/association/singleTissueEqtl | Significant eQTL associations |
/association/singleTissueSqtl | Significant sQTL associations |
/association/egene | eGenes in a tissue |
/dataset/tissueSiteDetail | Available tissues with metadata |
/reference/gene | Gene metadata; resolves a symbol to the dataset's versioned GENCODE ID |
/dataset/variant | Variant lookup by snpId (rsID) or variantId; returns the b38 GTEx ID |
| ID | Description |
|---|---|
gtex_v10 | GTEx v10 (current; 946 donors, 54 tissues, eQTLs mapped in 50; GENCODE v39) |
gtex_v8 | GTEx v8 (948 donors, 838 genotyped; eQTLs in 49 of 54 tissues; GENCODE v26) — older but widely cited |
.version in each. PCSK9 is ENSG00000169174.11 in v10 but .10 in v8 — querying the wrong suffix silently returns zero results. Resolve the correct ID per dataset with /reference/gene?geneId=SYMBOL&gencodeVersion=v39 (use v26 for v8). The expression and QTL endpoints only accept versioned gencodeId values — a bare symbol such as APOE also returns zero rows./association/singleTissueSqtl, /dataset/tissueSiteDetail, /dataset/variant) still default to gtex_v8 when it is omitted, so you silently get v8 data — and a v10 GENCODE ID returns nothing.chr{chrom}_{pos}_{ref}_{alt}_b38 (GRCh38) — different from rs IDstissueSiteDetailId (e.g., Whole_Blood) not display names for API callspValue + nes (no per-row qval); the per-gene qValue lives on the /association/egene endpoint.nes (normalized effect size of the alternative allele), not slope; positive nes = higher expression with the alt allele.For genome-wide analyses, download full summary statistics rather than using the API:
# All significant eQTLs (v10)
wget https://storage.googleapis.com/adult-gtex/bulk-qtl/v10/single-tissue-cis-qtl/GTEx_Analysis_v10_eQTL.tar
# Gene read counts (use ..._gene_tpm.gct.gz for TPM, ..._gene_median_tpm.gct.gz for per-tissue medians)
wget https://storage.googleapis.com/adult-gtex/bulk-gex/v10/rna-seq/GTEx_Analysis_v10_RNASeQCv2.4.2_gene_reads.gct.gzscripts/query_gtex.py — runnable helper for the GTEx Portal API v2 (no key):
python scripts/query_gtex.py gene APOE # symbol -> versioned GENCODE ID for gtex_v10
python scripts/query_gtex.py expression ENSG00000130203.10
python scripts/query_gtex.py eqtl ENSG00000169174.11 --tissue Liver # v10 GENCODE ID
python scripts/query_gtex.py tissues~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.