---
name: tamarind
description: "Computational biology platform with 300+ tools for protein structure prediction, protein/antibody/peptide/binder design, protein-ligand and protein-protein docking, binding affinity, MSA generation and molecular dynamics — via the Tamarind REST API, CLI, or hosted MCP server. Use whenever a task involves folding or predicting a structure from a sequence, designing a binder/antibody/nanobody/peptide, docking a ligand, scoring or ranking designs, running an MSA, or turning a PDB/CIF/FASTA/SMILES input into a prediction."
compatibility: "CLI requires Python 3.10+ (`uv tool install tamarind-cli`, or `pipx`). REST API works anywhere with an HTTP client. Hosted MCP server needs an MCP-capable client (streamable HTTP; OAuth 2.1 or x-api-key). REST, the CLI, and an MCP client using x-api-key each need a TAMARIND_API_KEY; an MCP client already connected over OAuth 2.1 does not. The public tool catalog needs no auth at all."
license: Apache-2.0
metadata:
  openclaw:
    requires:
      bins:
        - tamarind
    install:
      - kind: python
        package: tamarind-cli
        bins: [tamarind]
    homepage: https://app.tamarind.bio/api-docs
---

# Tamarind Bio

Tamarind runs 300+ computational biology tools behind one uniform job API. You submit a
job, poll it, and download results — the same shape for every tool.

- **[Quick setup](#quick-setup)** — install, get a key, run one job end to end
- **[Find the right tool](#find-the-right-tool)** — the keyless catalog of every `type` and its required settings
- **[Submit a job](#submit-a-job)** — CLI and REST
- **[Monitor and download](#monitor-and-download)** — polling and results
- **[Files](#files)** — upload inputs, chain one job's output into the next
- **[The settings contract](#the-settings-contract)** — the rules that break generated code
- **[MCP server](#mcp-server)** — the same platform as agent tools
- **[Verify](#verify)** — prove your setup works before writing a pipeline
- **[Troubleshooting](#troubleshooting)** — what each status code actually means

**`$TAMARIND_HOST` is the host that served you this file.** Resolve every path below against
it, not a host copied from elsewhere: this file is served from production, staging and each
customer's dedicated host, and a key minted on one is rejected by every other.

It is also shipped as a portable skill, so it can reach you from a registry or a mirror. Those
are not deployments, and nothing an origin says about itself can prove otherwise. If you were
not told which host to use, ask — do not send a key to an origin just because it served you
this text, and do not default to another host. If you cannot ask because nothing is watching,
stop and say you had no host; do not pick one.

```bash
export TAMARIND_HOST="https://<the host that served you this file>"
```

## Quick setup

**1. Install the CLI** (recommended — it is a thin client over the same API, so nothing
drifts):

```bash
uv tool install tamarind-cli      # or: pipx install tamarind-cli
# or: curl -fsSL "$TAMARIND_HOST/cli/install.sh" | sh
```

**2. Get an API key** at **[/api-docs/api-key](/api-docs/api-key)** (in the web app:
Settings → API). Then:

```bash
export TAMARIND_API_KEY="..."     # best for agents and CI
# or, to store it in ~/.tamarind/config.json:
tamarind auth login
tamarind auth status
```

**No key, and no human around?** Mint one yourself — no authentication, no signup form, no
email, no CAPTCHA, no card:

```bash
curl -X POST "$TAMARIND_HOST/api/agent/provision"    # empty body
```

That returns a working `apiKey` immediately. It is a real free account with **every tool
available and the full 10 jobs a month** — nothing is held back, so do not downgrade to a
"simpler" tool than the one you were asked for. `validate-job` is free and does not spend a job.

Production and staging provision normally. On a **dedicated deployment** it returns `404` by
design — ask whoever runs it for a key, or create one at [/api-docs/api-key](/api-docs/api-key).
Do not switch hosts to get around it; keys are not portable between deployments.

The response also carries a `claimUrl`. **Surface it to your user.** One sign-in there moves the
jobs you ran into their account so they can see them, and stops the key expiring (unclaimed keys
stop working after ~30 days). Afterwards the same key acts as them.

Store the key you get. Provisioning is rate-limited, so asking for a second one because the
first was not kept is the usual way to earn a `429`. Details: [/auth.md](/auth.md).

A human can instead create a permanent key in the web app at
[/api-docs/api-key](/api-docs/api-key) (Settings → API). That key does not expire and carries no
trial runtime cap — but the free tier is **the same 10 jobs a month**. Claiming, or signing up,
removes the expiry; neither raises the job count.

**3. Run one job end to end:**

```bash
tamarind submit esmfold \
  --set sequence=MTYKLILNGKTLKGETTTEAVDAATAEKVFKQYANDNGVDGEWTYDDATKTFTVTE \
  --name my-first-fold --wait --timeout 900 --download ./out
```

`esmfold` on purpose: it runs in minutes with no MSA, so a first run finishes inside the
trial's one-hour cap and inside your patience. Any tool works on a trial key — `alphafold` or
`boltz` are better models for real work — but a first-run example should not be the one that
teaches you what a 40-minute MSA feels like.

## Find the right tool

**`GET $TAMARIND_HOST/tools.json` needs no API key.** It lists every publicly
submittable tool with its exact `type` string, a description, intent tags, and its
**required settings** — enough to build a correct payload before you have a key. It is
generated from the live tool registry on every request, so it cannot go stale.

```bash
curl -s "$TAMARIND_HOST/tools.json?tag=protein-ligand-docking"   # narrow it
curl -s "$TAMARIND_HOST/tools.json?type=alphafold"               # one tool
curl -s "$TAMARIND_HOST/tools.md"                                # same, as a table
```

Keep the `?` **inside** the quotes. In zsh — the default macOS shell — an unquoted `?` is a
glob, and the shell aborts with `no matches found:` before curl ever runs.

Filter with `?type=` or `?tag=` rather than pulling the whole 160KB document. The response
always carries the full `tags` list, so a wrong `?tag=` guess self-corrects in one request.

Or from the CLI:

```bash
tamarind tools --search boltz
tamarind tools --function structure-prediction --modality protein
tamarind schema boltz                      # full parameters (needs a key)
tamarind schema boltz --example > job.yaml # a runnable starting point, where one exists
```

**Never guess a tool name.** `type` is case-sensitive and is *not* the display name —
"Boltz-2" is the label, `boltz` is the identifier; RFantibody is `rfantibody`. A name that
is absent from the catalog is not necessarily invalid: feature-flagged, org-restricted and
custom tools are omitted but may be submittable with your key — check `GET /api/tools`
once you have one.

## Submit a job

```bash
tamarind validate boltz --input job.yaml           # free, same validator as submit
tamarind submit boltz --input job.yaml --name my-run --wait --download ./out
tamarind submit boltz --set inputFormat=sequence --set sequence=MKTV... --name quick-fold
```

REST equivalent:

```bash
curl -X POST "$TAMARIND_HOST/api/submit-job" \
  -H "x-api-key: $TAMARIND_API_KEY" -H 'Content-Type: application/json' \
  -d '{"jobName":"my-run","type":"alphafold","settings":{"sequence":"MTYK..."}}'
```

`jobName`, `type` and `settings` are all required. **`jobName` is NORMALIZED, not validated.** `cleanName` strips every character outside
`[A-Za-z0-9_.-]` and turns whitespace into `_`, then stores that. There is no length check
and no rejection — so `"my run!"` is accepted and the job is saved as `my_run`, and polling
for `"my run!"` afterwards reports an unknown job. **Send a name that is already clean, no
more than 200 characters, and poll with exactly what you sent.** For an existing name longer
than 200 characters, page through `GET /api/jobs` with `startKey` and match `JobName`.
Names are unique per account; one already in use is a **400** whose message names it (409 exists, but only for
the narrow concurrent-submit lock race, so branch on the message rather than the code).

**`projectTag` — required for some organizations.** An optional top-level field (NOT inside
`settings`) on `/submit-job`, `/submit-batch` and `/submit-pipeline`, naming the organization
project to file the job under. It takes the project's **id or its name**. Orgs that turn on
"require a project tag" reject every untagged submission with a **400**, and a *personal*
project does not satisfy the policy. You do not have to look the project up first — that 400
carries the ones you may use:

```jsonc
{ "error": "Your org requires a project tag. ...",
  "availableProjects": [ { "id": "8f1c2b64-...", "name": "Programme A" } ],
  "hint": "Retry with `projectTag` set to one of `availableProjects` ..." }
```

Retry the same submission with `"projectTag": "Programme A"` (or its id). If you are unsure
which project a piece of work belongs to, ask the user — do not pick one for them.

You can also list them up front, and create one:

```bash
curl "$TAMARIND_HOST/api/projects/list" -H "x-api-key: $TAMARIND_API_KEY"
curl -X POST "$TAMARIND_HOST/api/projects/create" \
  -H "x-api-key: $TAMARIND_API_KEY" -H 'Content-Type: application/json' \
  -d '{"name":"Programme A","description":"Q3 binder campaign"}'
```

`GET /api/projects/list` returns each project's `ProjectId`, `ProjectName` and a `scope` of
`org`, `personal` or `shared`. **Only `scope: "org"` satisfies a required-project policy** —
submitting with a personal or shared project is refused exactly as if you had sent none.
The 400's `availableProjects` is capped at 25 and is absent when the bounded lookup finds
nothing, so treat this endpoint as the complete answer and that field as a shortcut.

Two traps in the response. **Send `ProjectId`, not `ProjectName`, for a `shared` project** —
names are only resolved within your own org and personal partitions, so a shared project's
name comes back "not found". And when `isInOrg` is false you have no organization, so every
project is reported as `scope: "org"` and there is no required-project policy to satisfy;
branch on `isInOrg` before you branch on `scope`.

`POST /api/projects/create` makes an **org**-scoped project by default. Prefer reusing an
existing one: an organization that requires tagging is usually curating that list on
purpose, so ask the user before inventing a project rather than creating a near-duplicate.

**Always dry-run first.** `POST /api/validate-job` runs the identical validator and costs
no compute — but it is **not free of auth**: without a key it answers 400, so it is not a
step you can take straight from the keyless catalog. It answers **200 even when the payload
is invalid**, so you cannot read the status code alone.

**Check the status FIRST, then branch on `valid`.** In that order, because an auth failure
wears the same clothes as a science failure: a keyless call answers `400` with
`{"valid": false, "error": "Missing or incorrect api key"}`. Read `valid` on its own and
you will conclude your payload is wrong and start mutating settings to fix a missing key.
Treat non-2xx as an auth/transport problem and only interpret `valid` on a 200.

The response is one shape or the other, never both:

```jsonc
{ "valid": true,  "normalized": { ...settings with defaults filled in } }
{ "valid": false, "error": "<first problem>", "missing_fields": [ ... ] }
```

`normalized` is **absent** when invalid, so read `valid` before you touch it or you submit
`undefined`. Either shape may also carry `unrecognized_settings: ["seq"]` — that is the one
place the platform names a key you misspelled, so log it.

Validation stops at the FIRST error, so `missing_fields` can be short (or empty) while more
problems remain. Re-validate after each fix rather than assuming one pass is exhaustive.

## What success looks like

Everything above tells you how things fail. These are the three ways they *succeed* in a
shape you would not predict — each one breaks code written against the obvious assumption.

- **A successful submit returns plain text, not JSON, and carries no job id.** The body is
  literally `<jobName> submitted to queue.` — so `resp.json()["id"]` raises on the *happy*
  path. **The `jobName` you sent is the handle**, which is exactly why the normalization
  rule above matters. Useful detail: the name in that text is the name that was actually
  stored, so echoing it back tells you what to poll for.
- **An `X` in a sequence may be silently deleted, not rejected.** A field publishing
  `unknownResidue: "X-stripped"` (`alphafold`, `esmfold`) accepts `X` and removes it before
  the run, which shifts every residue index after it — your numbering no longer matches the
  output. Other tools (`boltz`, `chai`) keep `X` as a real residue. Same input, different
  science, no error either way. Check the field in `/tools.json` before relying on indices.
- **A misspelled OPTIONAL key never errors at all.** The "missing required field" symptom
  only appears when the key you fumbled was required. `numDesign` for `numDesigns` simply
  leaves the default in place — and generative defaults are large on purpose, so the job
  runs, bills, and silently ignores the number you meant to set.

## Monitor and download

```bash
tamarind status my-run
tamarind wait my-run --timeout 3600
tamarind results my-run --download ./out
tamarind --json jobs | jq '.jobs[] | select(.JobStatus=="Running")'
```

REST: `GET /api/jobs?jobName=<name>` returns **the job row directly** — not wrapped in
`{"jobs": [...]}`, and its `Settings` may arrive as a JSON-encoded
**string** rather than an object, so decode it defensively. Statuses are `In Queue`, `Running`, `Complete`, `Stopped`, and
`Failed`. **Treat `Complete`, `Stopped`, and `Failed` as terminal** — stop polling
on any of them. Exact-name lookup accepts up to 200 characters; use the bounded paginated
list for an existing longer name.

Status alone does not prove useful scientific output. Download results after `Complete`.
After `Stopped` or `Failed`, call `POST /api/result` with
`{"jobName":"<name>","fileName":"output.log"}` to retrieve the log.

**A pipeline RUN and its execution rows are not pollable here.** They are excluded from this
endpoint, so `GET /api/jobs?jobName=<a pipeline run>` answers the same **400 exact-miss
problem** as a misspelled name. Poll
`GET /api/pipelines/runs/{run_id}` for the run; its statuses are a separate lowercase
set (`queued`/`running`/`finished`/`partial`/`stopped`/`failed`) that does not interchange with
`JobStatus`.

`POST /api/result` with `{"jobName": "..."}` returns a presigned S3 URL **as a
JSON-encoded string** (quoted), which needs no API key to fetch. A `202 {"status":"preparing"}`
means the archive is still being built — retry.

## Files

A file-typed setting takes the **bare filename of something you already uploaded**, not a
local path and not the file's contents. A path-shaped value you never uploaded is a clean
**400** — *File "x.pdb" has not been uploaded* — not a silent mis-read. What IS treated as
inline file content is a value that does not look like a path at all (e.g. multi-line text),
so pasting a PDB body into the field works, while pointing at a local file does not.

```bash
tamarind files upload ./target.pdb
tamarind submit rfdiffusion --set task="Binder Design" --set pdbFile=target.pdb \
  --set targetChains='["A"]' --set binderLength=80 --name binder-1
```

**Note the explicit `task`.** `rfdiffusion` is branch-shaped and its selector defaults to
`Motif Scaffolding`, not binder design — so a payload that omits `task` runs a different
protocol and succeeds. `interfaceResidues` belongs to the *Motif Scaffolding* branch;
Binder Design takes `targetChains` (a `list`, so a JSON array) and `binderLength`. This is
the [settings-contract](#the-settings-contract) rule about task selectors, in the one place
it costs a GPU run to get wrong.

REST: `PUT /api/upload/target.pdb`, then pass `"pdbFile": "target.pdb"`.

**Chaining:** reference a previous job's output as `"<JobName>/<file>"` — e.g.
`"binder-1/design_0.pdb"` — instead of downloading and re-uploading.

## The settings contract

These are the rules that most often break generated code. All of them are enforced by the
validator, and all of them fail in ways that do not name the real problem.

- **Use the exact `name` from the catalog or schema.** An unrecognised settings key is
  **not** rejected and **not** dropped — it is carried through, so a synonym (`seq`,
  `target_sequence`, `protein_file`) surfaces later as *"missing required field"*, pointing
  at the field you thought you had set.
- **Defaults are filled BEFORE requiredness is checked.** A field marked required in the
  registry that carries a default is one you may omit — `/tools.json` already accounts for
  this and lists only what you must actually send.
- **Many tools are branch-shaped, not checklists.** A task selector (often `inputFormat` or
  `task`) picks the branch, and it usually has a default. `esmfold2` defaults
  `inputFormat` to `sequence`, so a payload carrying only `molecules` is read as the
  sequence branch and rejected for a missing `sequence`. Set the selector explicitly.
- **`type: "sequence"` does not mean protein.** Alphabets are per-tool and enforced:
  `disco`'s `dnaSequence` accepts `ATGCN`, `rna-fm` accepts `ACGU`. A protein chain sent to
  either is a 400 that names the allowed set.
- **Length caps are real and per-tool**, from 14 to 20,000 residues (`nbforge` is 150 — a
  VHH domain, not a VHH-Fc fusion). Whitespace is stripped before counting.
- **`:` separates chains** of a multimer. Some tools reject multi-chain input entirely.
- **A `list: true` setting needs a JSON array**, not a comma-joined string.
- **PDB fields generally accept CIF too — but only as an UPLOADED file.** The catalog
  lists `cif` on a `pdb` field because the server converts an uploaded .cif. Pasting an
  mmCIF *body* inline is classified against the field's own declared extensions, which
  are usually pdb-only (188 of 194), so it is read as PDB and refused with *"not a valid
  PDB file"*. Upload it and pass the filename, or send PDB text.
- **Never send `submit_method`, `msa`, or `monomer_msa`** — platform-internal routing fields.

## MCP server

`https://mcp.tamarind.bio/mcp` — streamable HTTP, OAuth 2.1 or `x-api-key`. Prefer it over
raw HTTP when your client speaks MCP; it exposes the same discovery/submit/monitor surface
as tools. Setup: </api-docs/mcp-server>

## Verify

Run these three before building anything on top:

```bash
curl -s "$TAMARIND_HOST/tools.json?type=esmfold" | head -c 300    # no key needed
tamarind auth status                                                       # key is live
tamarind validate esmfold --set sequence=MTYKLILNGKTLKGETTTEAVDAATAEK --name probe
```

(`esmfold` rather than `alphafold` so the check returns quickly — see step 3.)

If the first works and the second fails, you have a key problem, not an API problem.

## Troubleshooting

| Symptom | Meaning |
|---|---|
| `401` or `403` on `/api/jobs` | Authentication failed in the application or at API Gateway. Create a key at `/api-docs/api-key`, set `TAMARIND_API_KEY`, and branch on "not 2xx" rather than one status. |
| `400` on `/api/jobs` | The limit or cursor is invalid, the selected member is inaccessible, or no exact job/batch matches. |
| `400 Missing or incorrect api key` | No/invalid key on a classic endpoint. Get one at `/api-docs/api-key`, set `TAMARIND_API_KEY`. |
| `400` on a job you believe is correct | A settings key is misspelled, or you are on a different task branch than you think. Run `validate-job`. |
| `403` | Org/team budget exceeded, or that tool is not available to your account. |
| `409` | Concurrent submit lost a lock race on the same `jobName`. The ordinary duplicate is a 400. |
| `400` on an unknown job name | No visible job has that exact name. Check the normalized name and account host. |
| `400` `... already exists` | `jobName` is taken. Names are unique per account. |
| Job is `Complete` but the output is empty | A classic job that failed usually reads `Complete` or `Stopped`, not `Failed`; check `logs`. |
| Work never appears in your workspace | You submitted to `app.tamarind.bio` from a dedicated-deployment account. Use your org's host. |

Most endpoints in this guide answer a missing key with the same JSON object (`error`, plus
`getApiKey`, `agentGuide`, `toolCatalog`, `hint`), so you can usually read `getApiKey` from
one of them. **These documented exceptions are why you branch on "not 2xx" rather
than on a status:**

- The `/api/jobs` application contract declares **401** and uses the same shared authentication boundary as pipelines and molecules, but API Gateway can reject bad credentials earlier with a generic **403**; branch on "not 2xx".
- `/api/models` and `/api/finetuned-models` answer **401** with a bare scalar (`-1` and
  `Unauthenticated`) and carry no recovery fields.
- `/api/usage-statistics` also answers **401** and does not use the classic recovery body.
- `GET /api/projects/list` and `POST /api/projects/create` answer **401** as
  `{"error": "Unauthorized"}`, with no recovery fields. They are session-first UI routes
  that gained the api-key path, and the browser relies on that status.
- `PUT /api/upload/{filename}` is **not served by the API layer at all** — it redirects to
  a CloudFront host, so an unauthenticated call gets that redirect, not this JSON. A client
  that does not follow PUT redirects sees the 3xx itself.

Treat the recovery fields as present-if-JSON, never as guaranteed.

## Safety notes

- Jobs cost compute. Use `validate-job` (free) before `submit-job`, and prefer one job with
  the right settings over a sweep.
- Generative tools default to large design counts on purpose — check `numDesigns` and
  similar before submitting.
- `--show-url`, and `POST /api/result`, both return a credential-bearing presigned URL; keep it
  out of agent and CI logs. The REST route is the one agents hit, and agents log responses.
- Destructive CLI commands (`delete`, `files delete`) require `--yes` when non-interactive.

## Key URLs

| | |
|---|---|
| Tool catalog (no key) | </tools.json> · [as a table](/tools.md) |
| Full agent guide | </llms-full.txt> |
| OpenAPI spec | </api/openapi.json> |
| Get an API key | </api-docs/api-key> |
| CLI reference | </api-docs/cli> · [source](https://github.com/Tamarind-Bio/tamarind-cli) |
| MCP server | <https://mcp.tamarind.bio/mcp> |
| Human docs | </api-docs> |
| Contact | info@tamarind.bio |
