Start
Use with AI assistants
Paste one page into Claude, ChatGPT or your coding agent and ask for the code you need.
Assistant that can read the web (Claude Code, Cursor, ChatGPT)
Help me use the Dydema EEG API in this project: developers.dydema.com/llms.txtPaste that line and the assistant reads the reference from /llms.txt itself.
Or paste the whole reference
- Press Copy prompt on the reference below.
- Paste it into Claude (or any assistant) at the start of your conversation.
- Ask for what you want, for example: “Write a Python script that reads my EDF file, cuts one epoch per annotated trial and creates a task model from them.”
The reference is complete but short: base URL, authentication, Wizard and its task models, every request and response field, pricing, a runnable example, every error code, rate limits and safe retries. Coding agents can also fetch it directly from /llms.txt.
Keep your API key out of the chat: the reference tells the assistant to read it from the
DYDEMA_API_KEY environment variable. See the Quickstart to set it.Dydema EEG API reference (for AI assistants)
# Dydema EEG API (Wizard) — reference for AI assistants
The Dydema EEG API is Wizard, a mixture model for EEG classification. The caller describes a classification task in a
sentence and sends labeled trials in any 10-10/10-20 channel layout, of any length. Wizard chooses compatible methods
automatically, tests them on the caller's trials, and hosts the one that measures best as a task model with its own
prediction route; new trials come back with a label and class probabilities. Use this reference
to write working client code. Everything below is exact; do not invent endpoints, fields or parameters that are not
listed. The only public routes are /v1/task-models (and its sub-routes), /v1/usage, /v1/status and /healthz.
## Basics
- Base URL: https://dydema--eegapi-gateway.modal.run/v1
- Auth: header `Authorization: Bearer $DYDEMA_API_KEY` on every request. Keys start with `dk_live_` and are created
at the Dydema dashboard (API keys page). Read the key from the DYDEMA_API_KEY environment variable; never hard-code it.
- JSON request and response bodies. HTTPS only. Research use only, not a medical device.
- Pricing (from the price book): a ONE-TIME create fee per task model ($1.00),
which includes every window of the fit; failed fits are free. Estimates (`"dry_run": true`) are free, but are refused with
402 INSUFFICIENT_CREDIT when the balance cannot cover the create fee plus the first month of hosting, and count toward
a daily estimate allowance (429 USAGE_CAP_EXCEEDED, with Retry-After). Hosting: $1.00 a month per model,
charged in whole 30-day months in advance until `expires_at` or deletion. Predictions: billed per window ($0.003). Test keys pay nothing (their models are
deleted after 30 days).
Prepaid credit: new accounts start with $5 of free credit; charges come off the account balance. Requests with insufficient credit are refused
(402 INSUFFICIENT_CREDIT). Add credit on the dashboard's Billing page.
## Task models (/v1/task-models)
Ids are `clf_` + 32 hex digits. Timestamps (`created`, `expires_at`, `paid_through`, `next_charge_at`) are Unix
epoch seconds.
- `POST /v1/task-models` (201): `x` (trials, channels, samples) as nested lists, or base64 little-endian float32 `x` +
`shape`; `channels` (one 10-10/10-20 name per row); `sampling_rate_hz`; `units` ("uV", "mV" or "V"; "unitless",
"normalized" and "unknown" are refused with 422 UNITS_REQUIRED); `labels` (one per trial; at least 2 classes of at
least 5 trials, 20+ per class recommended); `description` (at most 100 characters: what the task is
and what each label means); optional `sessions` (one per
trial; a whole session is held out for validation), `name`, `expires_at` (ISO 8601 date or date-time) or
`expires_in_days`, `stride_seconds`, `dry_run`, `defer_fit`. Never send a `model` field.
- Method selection is automatic and uses only methods whose licence permits the use; held-out trials decide. There is
no model, size or method parameter.
- Send `"dry_run": true` first. The answer reports category, create_usd, hosting_usd_per_month and warnings.
Trial duration can affect accuracy; follow the returned warnings. Nothing is fitted or charged. A created task model adds an opaque id and a training_report with validation accuracy
and chance. Evaluate predictions on independent recordings; fitting scores may be optimistic.
- More trials than one request (4,000,000 numbers as sent and after resampling, 4,096 trials): create with
`"defer_fit": true` and the first batch (status collecting; each batch is billed per window, the create fee is charged at /fit), `POST
/v1/task-models/{id}/trials` for each further batch (signal fields + labels + optional sessions), then `POST
/v1/task-models/{id}/fit` within 7 days (an unfitted collection is deleted with its features; only features are kept,
never the signal). Shuffle trials first so every batch holds every class.
- `POST /v1/task-models/{id}/predictions`: the signal fields without labels, with the SAME channel names the model was
fitted on (any order). Answer: `{"object": "list", "data": [{"object": "classification", "index", "label",
"probabilities": {"<class>": p}}], ...}`, one per trial in order.
- `PATCH /v1/task-models/{id}` `{"expires_at": "2026-12-31" | null, "name": "..."}`; `GET /v1/task-models` lists
(collecting + active, newest first, `limit` 1-100, `before` = a previous page's `next_before`),
`GET /v1/task-models/{id}` returns one, `DELETE /v1/task-models/{id}` deletes (idempotent: 200 again). Status:
collecting | active | expired | deleted. Unknown id: 404 CLASSIFIER_NOT_FOUND; an expired or deleted model answers the
same generic unavailable response.
- Zero-shot (no labeled trials): `POST /v1/task-models` with `{"mode": "zero_shot", "description", "classes"? (2-64 names),
"channels", "sampling_rate_hz", "trial_seconds", "dry_run"?, "reference_classes"?}` - no x, no labels. The head is
fitted on reference trials; the answer adds `mode: "zero_shot"`, supported class names, aggregate `evidence`,
`explanation` (what the participant does for each class) and `reference_classes`. Estimate first, review the
explanation, then create with `reference_classes` returned unchanged (a changed mapping is 422 INVALID_REQUEST). Supported tasks include eyes closed (resting) vs eyes open (resting).
Unavailable tasks answer 422 NO_REFERENCE_DATA; use labeled trials instead. Follow the returned calibration warnings.
- Reference data preparation: when an estimate (zero-shot, or a few-shot estimate that relies on reference data) needs
reference data that is not ready yet, the estimate starts preparing it and answers `"ready": false` with `preparation` (`status`
queued | running | failed | required, `retryable`, `estimated_seconds`) instead of evidence. No create fee or hosting is
charged, and there is no separate call to start it: repeat the same estimate after a few minutes until `ready` is not false,
then create. A create sent before that answers 202 `task_model.preparing` with no task model, create fee or hosting charge;
retry it later with the SAME Idempotency-Key.
- Refused with 422 TASK_UNSUPPORTED (at dry run and create), only on positive evidence in the description or labels:
emotion or affect recognition, clinical or diagnostic uses, identifying or retrieving a specific person
(biometrics), and profiling age, sex or gender (model license terms; not a medical device). Fewer than
5 trials in a class: 422 TOO_FEW_TRIALS.
- Epoch per trial, from the event marker: ERP 0-1 s after the stimulus; motor imagery 0-4 s after the cue onset (the
examples' choice; 1-4 s also works and leaves out the response to the cue itself); sleep 30 s scoring epochs (`mne.events_from_annotations(raw, chunk_duration=30)` splits stage annotations).
- Channels: compatible methods read the supplied layout. Methods with a fixed 19-site layout (Fp1 Fp2 F7 F3 Fz F4 F8
T7 C3 Cz C4 T8 P7 P3 Pz P4 P8 O1 O2) fill a missing site from the nearest electrode sent within 60 mm, else leave it
silent. Methods that read positions need every channel to have a 10-10/10-20/10-05 name (EXG1 rules them out). A
method that cannot read the data is not tried; the create is refused only when none can.
T3/T4/T5/T6 are accepted as T7/T8/P7/P8 (renaming is optional).
- Logs show task_models.create_fee, task_models.hosting and task_models.predict. All from the prepaid balance.
- Performance: Dydema makes no accuracy claim for any task. Do not claim one method or task is better; point the user at
the validation scores in `training_report` (measured on their own held-out trials).
Install in a virtual environment (system pip fails on Homebrew/Debian with externally-managed-environment):
macOS/Linux `python3 -m venv .venv && source .venv/bin/activate`, Windows `py -m venv .venv` then
`.venv\Scripts\activate`; then `python -m pip install numpy requests`.
Create a task model (Python, complete and runnable; synthetic trials, NOT real EEG; saves the id to task_model_id.txt):
```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"])
```
Predict with it:
```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"])
```
Dataset format: `x` (trials, channels, samples) in uV/mV/V at the declared rate; one label per trial; channels = one
10-10/10-20 name per row, any layout (3, 19, 64...). Clean file labels ("EEG Fp1-REF" -> "Fp1"; the API does not);
re-reference bipolar channels ("Fp1-F7"); drop EOG/ECG/EMG/trigger channels and channels without a 10-10 name. Read an
EDF with `mne.io.read_raw_edf(path)`, pick the EEG channels, resample, take `raw.get_data(units="uV")` and cut one epoch
per event (`mne.events_from_annotations(raw)`). The docs' Dataset format page has a helper (dydema_dataset.py: MNE read,
label cleaning, resample to 256 Hz, epoching, batching with defer_fit) and EDF, BIDS (mne-bids) and NumPy scripts. Save
the task model id after the create, so a 402 or 422 later does not lose what was paid for.
## GET /v1/usage
Today's and this month's totals and the account's limits: `{"today": {...}|null, "month": {"usd": "...", "windows": "...",
"task_model_create_usd": "...", "task_model_hosting_usd": "..."}|null, "limits": {"rps": ..., "burst": ...,
"concurrency": ...}}`. `month.usd` is everything charged (create fees, hosting, predictions); the two task_model_* counters
appear once such a charge is recorded (never with a test key). The blocks carry more counters and limits than
these; read only the ones you need. Money and counters are decimal strings; null means nothing recorded.
## Errors
Every error has one JSON shape: `{"type", "code", "message", "field", "alternatives", "details"}`. Switch on `code`;
show `message` and `alternatives` (how to fix it) to the user. `details.request_id` identifies the request.
- 401 UNAUTHENTICATED / KEY_REVOKED / KEY_EXPIRED: bad or revoked key.
- 403 SCOPE_MISSING: the key cannot use this endpoint. 403 TENANT_SUSPENDED: account paused. 403 IP_NOT_ALLOWED: the
key is restricted to other IP addresses.
- 402 INSUFFICIENT_CREDIT: insufficient credits; add credit. 402
BUDGET_EXCEEDED: the account's own daily or monthly budget reached; a person raises it under Limits.
- 404 CLASSIFIER_NOT_FOUND: no such task model on this account. 404 NOT_FOUND: unknown path.
- 413 PAYLOAD_TOO_LARGE: over 4,000,000 numbers as sent or after resampling; send batches with defer_fit and /trials.
- 422 INVALID_REQUEST (field names the problem) / TASK_UNSUPPORTED / NO_REFERENCE_DATA / TOO_FEW_TRIALS / CLASSIFIER_FIT_FAILED (not
charged) / UNITS_REQUIRED (units must be uV, mV or V) / IDEMPOTENCY_KEY_REUSED.
- 409 CONFLICT: same Idempotency-Key still running. 429 RATE_LIMITED / CONCURRENCY_LIMITED: short-term, retry.
429 USAGE_CAP_EXCEEDED: a daily or monthly usage cap; its Retry-After is the seconds until the next day or month.
- 503 POOL_UNAVAILABLE / POOL_SATURATED / SERVING_DISABLED / BILLING_UNAVAILABLE. 502 BACKEND_FAILURE (failed on our
side, not charged). 500 INTERNAL.
Retry 409, 5xx, and 429 only when `code` is RATE_LIMITED or CONCURRENCY_LIMITED, after the `Retry-After` header
(seconds), or with exponential backoff (1 s, 2 s, 4 s…) if it is absent; catch requests.ConnectionError and
requests.Timeout too; 5 attempts is a sensible cap, then stop with an error. Do not retry other 4xx unchanged —
including USAGE_CAP_EXCEEDED and both 402s, which need time or a person to act. A create tests methods on your trials
and can take minutes; use a generous timeout (predictions normally answer in seconds).
## Safe retries
Send header `Idempotency-Key: <uuid4>` on POST /v1/task-models, /trials, /fit and /predictions, and reuse the same key
(with the same body) for every retry of that request, including a create retried after 202 `task_model.preparing`: it
is answered once and billed once. A different body with a used
key is 422 IDEMPOTENCY_KEY_REUSED.
## Prediction output
Use /v1/task-models/{id}/predictions for class labels and probabilities from your task model.
## Tips for the code you write
- Use numpy arrays shaped (trials, channels, samples) and `.tolist()` for JSON, or base64 float32 with `shape` for
large batches.
- Dry run first (repeat it while `ready` is false), then create, then predict. Delete the task model or set `expires_at` when done, since hosting is
charged monthly.