# 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": {"": 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=" "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: ` 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.