Start
Quickstart
From an API key to your first task model and its predictions in about five minutes.
Build with an AI assistant
Help me use the Dydema EEG API in this project: developers.dydema.com/llms.txtPaste it into Claude, Cursor or ChatGPT to have it write the integration; for assistants that cannot read the web, copy the full reference.
1. Create an API key
Create a Dydema account (or sign in to the dashboard if you already have one; it is the same account as dydema.com), confirm the three points of the Terms of Service the dashboard asks for, open API keys and create a key. It is shown once, so copy it now. Then make it available to your code as an environment variable:
export DYDEMA_API_KEY="dk_live_…"$env:DYDEMA_API_KEY="dk_live_…"This lasts only for that terminal window. Run the examples from the same terminal: a new terminal, or the Run button of an editor such as VS Code or PyCharm, does not see the key, and Python stops with KeyError: 'DYDEMA_API_KEY'. Set the variable again there, or in your editor’s run configuration.
X-Usage-Usd header on each response says what it cost.2. Set up Python or JavaScript
Every example comes in Python, JavaScript and cURL; pick the tab once and every page follows. The JavaScript examples need Node.js 22 or newer and no packages: save one as example.mjs and run node example.mjs. cURL needs nothing.
The Python examples need NumPy and Requests. Install them in a virtual environment for this project: installing into the system Python fails on recent macOS (Homebrew) and Linux with externally-managed-environment. In the same terminal:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install numpy requestspy -m venv .venv
.venv\Scripts\activate
python -m pip install numpy requestsIn a new terminal, run the activate line again before running your scripts. If PowerShell refuses to run the activate script, skip it and type .venv\Scripts\python wherever these docs say python.
3. Create a task model
A task model is a classifier for one task, fitted on labeled trials you send. The request carries the trials, shaped (trials, channels, samples), with one 10-10 or 10-20 channel name per row, the sampling rate, the units (uV, mV or V), one label per trial, and a sentence describing the task and what each label means.
The Python and JavaScript examples build 40 synthetic trials: a 10 Hz rhythm over C4 or C3, plus noise. None of it is real EEG. Save the Python one as quickstart.py and run python quickstart.py, or the JavaScript one as quickstart.mjs and run node quickstart.mjs, in the terminal where you set the key. The cURL example writes 20 of the trials as a JSON body with awk (macOS or Linux; on Windows, use Python or JavaScript).
import os
import json
import base64
import numpy as np
import requests
API = 'https://dydema--eegapi-gateway.modal.run/v1'
CHANNELS = ["Fp1","Fp2","F7","F3","Fz","F4","F8","T7","C3","Cz","C4","T8","P7","P3","Pz","P4","P8","O1","O2"]
# Synthetic stand-in, NOT real EEG: 40 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials carry
# a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. Replace with your own trials and labels.
rng = np.random.default_rng(40)
t = np.arange(4 * 128) / 128
labels = ["left", "right"] * 20
x = 10 * rng.standard_normal((len(labels), len(CHANNELS), t.size))
for i, label in enumerate(labels):
x[i, CHANNELS.index("C4" if label == "left" else "C3")] += 20 * np.sin(2 * np.pi * 10 * t)
x = np.ascontiguousarray(x, dtype="<f4") # shape (trials, channels, samples), sent as base64 float32
response = requests.post(
f"{API}/task-models",
headers={"Authorization": f"Bearer {os.environ['DYDEMA_API_KEY']}"},
json={
"x": base64.b64encode(x.tobytes()).decode("ascii"),
"shape": list(x.shape),
"sampling_rate_hz": 128,
"units": "uV",
"channels": CHANNELS,
"labels": labels, # one per trial, in the order of x
"description": "Motor imagery: after a cue, imagine squeezing the left or the right hand. Labels: left, right.",
"name": "hand imagery (example)",
"expires_in_days": 1, # hosting stops after a day; leave it out to keep the model until you delete it
},
timeout=300,
)
if not response.ok:
error = response.json()
raise SystemExit(f"{response.status_code} {error['code']}: {error['message']} {error['alternatives']}")
task = response.json()
print(task["id"], task["status"]) # active: fitted and hosted
print(json.dumps(task["training_report"], indent=2)) # validation accuracy and chance
with open("task_model_id.txt", "w") as f: # the next examples read it
f.write(task["id"])// Node 22 or newer. Save as example.mjs and run: node example.mjs
import fs from "node:fs";
const key = process.env.DYDEMA_API_KEY;
if (!key) throw new Error("set DYDEMA_API_KEY first");
const API = 'https://dydema--eegapi-gateway.modal.run/v1';
const CHANNELS = ["Fp1","Fp2","F7","F3","Fz","F4","F8","T7","C3","Cz","C4","T8","P7","P3","Pz","P4","P8","O1","O2"];
// Synthetic stand-in, NOT real EEG: 40 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials
// carry a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. Replace with your own trials and labels.
const noise = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
const SAMPLES = 4 * 128;
const labels = Array.from({ length: 40 }, (_, i) => (i % 2 ? "right" : "left"));
const x = new Float32Array(labels.length * CHANNELS.length * SAMPLES); // shape (trials, channels, samples)
labels.forEach((label, t) => CHANNELS.forEach((name, c) => {
const rhythm = name === (label === "left" ? "C4" : "C3") ? 20 : 0;
for (let s = 0; s < SAMPLES; s++) {
x[(t * CHANNELS.length + c) * SAMPLES + s] = 10 * noise() + rhythm * Math.sin((2 * Math.PI * 10 * s) / 128);
}
}));
const base64 = Buffer.from(x.buffer).toString("base64"); // little-endian float32
const response = await fetch(`${API}/task-models`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({
x: base64,
shape: [labels.length, CHANNELS.length, SAMPLES],
sampling_rate_hz: 128,
units: "uV",
channels: CHANNELS,
labels, // one per trial, in the order of x
description: "Motor imagery: after a cue, imagine squeezing the left or the right hand. Labels: left, right.",
name: "hand imagery (example)",
expires_in_days: 1, // hosting stops after a day; leave it out to keep the model until you delete it
}),
signal: AbortSignal.timeout(300_000),
});
if (!response.ok) {
const error = await response.json();
console.error(`${response.status} ${error.code}: ${error.message}`, error.alternatives);
process.exit(1);
}
const task = await response.json();
console.log(task.id, task.status); // active: fitted and hosted
console.log(JSON.stringify(task.training_report, null, 2)); // validation accuracy and chance
fs.writeFileSync("task_model_id.txt", task.id); // the next examples read it# Synthetic stand-in, NOT real EEG: 20 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials carry
# a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. awk writes the JSON body; curl sends it.
awk 'BEGIN {
srand(20); pi = atan2(0, -1)
split("Fp1 Fp2 F7 F3 Fz F4 F8 T7 C3 Cz C4 T8 P7 P3 Pz P4 P8 O1 O2", ch, " ")
printf "{\"name\": \"hand imagery (example)\", \"expires_in_days\": 1, "
printf "\"description\": \"Motor imagery: after a cue, imagine squeezing the left or the right hand. Labels: left, right.\", "
printf "\"sampling_rate_hz\": 128, \"units\": \"uV\", \"channels\": ["
for (c = 1; c <= 19; c++) printf "%s\"%s\"", (c > 1 ? ", " : ""), ch[c]
printf "], \"labels\": ["
for (i = 0; i < 20; i++) printf "%s\"%s\"", (i ? ", " : ""), (i % 2 ? "right" : "left")
printf "], \"x\": ["
for (i = 0; i < 20; i++) {
printf "%s[", (i ? "," : "")
for (c = 1; c <= 19; c++) {
printf "%s[", (c > 1 ? "," : "")
for (s = 0; s < 512; s++) {
v = 10 * (rand() + rand() + rand() + rand() - 2) * 1.73 # noise, about 10 uV
if (ch[c] == (i % 2 ? "C3" : "C4")) v += 20 * sin(2 * pi * 10 * s / 128)
printf "%s%.1f", (s ? "," : ""), v
}
printf "]"
}
printf "]"
}
printf "]}\n"
}' > task_model_body.json
curl -sS https://dydema--eegapi-gateway.modal.run/v1/task-models \
-H "Authorization: Bearer $DYDEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @task_model_body.json -o task_model.json
cat task_model.json; echo
# The first "id" in the answer is the task model's; the next examples read it from this file.
grep -o '"id": *"[^"]*"' task_model.json | head -n 1 | cut -d '"' -f 4 > task_model_id.txtWizard reads the description and the dataset’s shape, never the signal, and selects compatible methods. Each one is fitted on part of your trials and scored on the rest, and the best scorer is refitted on all of them and hosted. The answer is the new task model: its id (saved in task_model_id.txt for the next steps), its status, and a training_report block with validation accuracy and chance. The example makes a task model that expires after a day. Its create fee is $1.00, paid once. Predictions are billed per window ($0.003). Wizard selects the method automatically.
4. Predict on new trials
Send new trials with the same channel names, in any order, and no labels. Each trial comes back with a label and the probabilities of every class.
import os
import base64
import numpy as np
import requests
API = 'https://dydema--eegapi-gateway.modal.run/v1'
CHANNELS = ["Fp1","Fp2","F7","F3","Fz","F4","F8","T7","C3","Cz","C4","T8","P7","P3","Pz","P4","P8","O1","O2"] # the channel names the model was fitted on (any order)
TASK_MODEL_ID = open("task_model_id.txt").read().strip() # written by the create example
# Synthetic stand-in, NOT real EEG: 6 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials carry
# a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. Replace with your own trials and labels.
rng = np.random.default_rng(6)
t = np.arange(4 * 128) / 128
labels = ["left", "right"] * 3
x = 10 * rng.standard_normal((len(labels), len(CHANNELS), t.size))
for i, label in enumerate(labels):
x[i, CHANNELS.index("C4" if label == "left" else "C3")] += 20 * np.sin(2 * np.pi * 10 * t)
x = np.ascontiguousarray(x, dtype="<f4") # shape (trials, channels, samples), sent as base64 float32
response = requests.post(
f"{API}/task-models/{TASK_MODEL_ID}/predictions",
headers={"Authorization": f"Bearer {os.environ['DYDEMA_API_KEY']}"},
json={"x": base64.b64encode(x.tobytes()).decode("ascii"), "shape": list(x.shape),
"sampling_rate_hz": 128, "units": "uV", "channels": CHANNELS},
timeout=120,
)
if not response.ok:
error = response.json()
raise SystemExit(f"{response.status_code} {error['code']}: {error['message']} {error['alternatives']}")
for truth, prediction in zip(labels, response.json()["data"]): # one per trial, in the order sent
print(f"sent {truth}, predicted {prediction['label']}", prediction["probabilities"])// Node 22 or newer. Save as example.mjs and run: node example.mjs
import fs from "node:fs";
const key = process.env.DYDEMA_API_KEY;
if (!key) throw new Error("set DYDEMA_API_KEY first");
const API = 'https://dydema--eegapi-gateway.modal.run/v1';
const CHANNELS = ["Fp1","Fp2","F7","F3","Fz","F4","F8","T7","C3","Cz","C4","T8","P7","P3","Pz","P4","P8","O1","O2"]; // the channel names the model was fitted on (any order)
const TASK_MODEL_ID = fs.readFileSync("task_model_id.txt", "utf8").trim(); // written by the create example
// Synthetic stand-in, NOT real EEG: 6 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials
// carry a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. Replace with your own trials and labels.
const noise = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
const SAMPLES = 4 * 128;
const labels = Array.from({ length: 6 }, (_, i) => (i % 2 ? "right" : "left"));
const x = new Float32Array(labels.length * CHANNELS.length * SAMPLES); // shape (trials, channels, samples)
labels.forEach((label, t) => CHANNELS.forEach((name, c) => {
const rhythm = name === (label === "left" ? "C4" : "C3") ? 20 : 0;
for (let s = 0; s < SAMPLES; s++) {
x[(t * CHANNELS.length + c) * SAMPLES + s] = 10 * noise() + rhythm * Math.sin((2 * Math.PI * 10 * s) / 128);
}
}));
const base64 = Buffer.from(x.buffer).toString("base64"); // little-endian float32
const response = await fetch(`${API}/task-models/${TASK_MODEL_ID}/predictions`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({
x: base64, shape: [labels.length, CHANNELS.length, SAMPLES], sampling_rate_hz: 128, units: "uV", channels: CHANNELS,
}),
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
const error = await response.json();
console.error(`${response.status} ${error.code}: ${error.message}`, error.alternatives);
process.exit(1);
}
(await response.json()).data.forEach((prediction, i) => { // one per trial, in the order sent
console.log(`sent ${labels[i]}, predicted ${prediction.label}`, prediction.probabilities);
});# Synthetic stand-in, NOT real EEG: 6 trials of 4 s, 19 channels at 128 Hz, in microvolts. "left" trials carry
# a 10 Hz rhythm over C4 and "right" trials over C3, on top of noise. awk writes the JSON body; curl sends it.
awk 'BEGIN {
srand(6); pi = atan2(0, -1)
split("Fp1 Fp2 F7 F3 Fz F4 F8 T7 C3 Cz C4 T8 P7 P3 Pz P4 P8 O1 O2", ch, " ")
printf "{"
printf "\"sampling_rate_hz\": 128, \"units\": \"uV\", \"channels\": ["
for (c = 1; c <= 19; c++) printf "%s\"%s\"", (c > 1 ? ", " : ""), ch[c]
printf "], \"x\": ["
for (i = 0; i < 6; i++) {
printf "%s[", (i ? "," : "")
for (c = 1; c <= 19; c++) {
printf "%s[", (c > 1 ? "," : "")
for (s = 0; s < 512; s++) {
v = 10 * (rand() + rand() + rand() + rand() - 2) * 1.73 # noise, about 10 uV
if (ch[c] == (i % 2 ? "C3" : "C4")) v += 20 * sin(2 * pi * 10 * s / 128)
printf "%s%.1f", (s ? "," : ""), v
}
printf "]"
}
printf "]"
}
printf "]}\n"
}' > predict_body.json
curl -sS https://dydema--eegapi-gateway.modal.run/v1/task-models/$(cat task_model_id.txt)/predictions \
-H "Authorization: Bearer $DYDEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @predict_body.jsonX-Usage-Usd is what the call cost. A request normally answers in seconds; rarely up to about 30 seconds longer right after we deploy.
5. Use your own recording
Cut one epoch per trial around each event marker (the API does not see your triggers) and send those epochs with their labels. Dataset format has conversion scripts for EDF, BIDS and NumPy data, and a helper that sends a dataset larger than one request in batches. At least 5 labeled trials per class are required, and 20 or more are recommended.
training_report is measured on your own labels, but the winner was chosen as the best of several on the same split, so it is optimistic. For a figure to report, predict on trials the model has never seen.6. Stop hosting
A task model is charged for hosting ($1.00 a month per model), a month at a time in advance, until it expires or you delete it. Deleting stops later months; the current month is not refunded. Delete the one you made here:
import os
import requests
TASK_MODEL_ID = open("task_model_id.txt").read().strip() # written by the create example
response = requests.delete(
f'https://dydema--eegapi-gateway.modal.run/v1/task-models/{TASK_MODEL_ID}',
headers={"Authorization": f"Bearer {os.environ['DYDEMA_API_KEY']}"},
timeout=30,
)
if not response.ok:
error = response.json()
raise SystemExit(f"{response.status_code} {error['code']}: {error['message']} {error['alternatives']}")
print(response.json()) # deleted: no further hosting charge; the current month is not refunded// Node 22 or newer. Save as example.mjs and run: node example.mjs
import fs from "node:fs";
const key = process.env.DYDEMA_API_KEY;
if (!key) throw new Error("set DYDEMA_API_KEY first");
const TASK_MODEL_ID = fs.readFileSync("task_model_id.txt", "utf8").trim(); // written by the create example
const response = await fetch(`https://dydema--eegapi-gateway.modal.run/v1/task-models/${TASK_MODEL_ID}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
const error = await response.json();
console.error(`${response.status} ${error.code}: ${error.message}`, error.alternatives);
process.exit(1);
}
console.log(await response.json()); // deleted: no further hosting charge; the current month is not refundedcurl -sS -X DELETE https://dydema--eegapi-gateway.modal.run/v1/task-models/$(cat task_model_id.txt) \
-H "Authorization: Bearer $DYDEMA_API_KEY"7. When something goes wrong
Every error has the same shape: a machine-readable code, a message, the field at fault and alternatives that say how to fix it. The examples print the status, code, message and alternatives. Rate-limit and capacity errors carry Retry-After; retry those with an Idempotency-Key so a retry is never billed twice (how). Every code is listed in Errors & limits.