Skip to content

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.txt

Paste 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:

Set your key
macOS / Linux
export 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.

Treat the key like a password: keep it in an environment variable or a secret manager, never in source control or a browser app. Every call is billed to your account; the 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:

Create a virtual environment
macOS / Linux
python3 -m venv .venv
source .venv/bin/activate
python -m pip install numpy requests

In 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).

POST /v1/task-models
Python
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"])

Wizard 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.

POST /v1/task-models/{id}/predictions
Python
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"])

X-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.

The validation score in 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:

DELETE /v1/task-models/{id}
Python
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

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.