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

**Read every URL below against the host you fetched this file from.** Links are
root-relative on purpose. Most callers are on `https://app.tamarind.bio`, but an
organisation with a dedicated deployment (e.g. `https://acme.tamarind.bio`) has its own
jobs, files and **API keys** on that host — a key minted on the shared app does not work
there, and a job sent to `app.tamarind.bio` from a tenant account **succeeds** and silently
lands where that user will never see it. The shell snippets use `$TAMARIND_HOST` so there is
one place to change:

```bash
export TAMARIND_HOST="https://app.tamarind.bio"   # or https://yourorg.tamarind.bio
```

## 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
```

There is no way to mint a key programmatically — a human has to create it once in the web
app. If you are an agent and no key is present, stop and ask for one; say exactly where to
get it and which env var to set, rather than guessing at auth.

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

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

## 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, and
poll with exactly what you sent.** 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).

**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 branch on `valid`, never on the status code.

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": [...]}`. Statuses are `In Queue`, `Running`, `Complete`, `Stopped`, `Deleted`,
`Failed`. **Treat `Complete`, `Stopped`, `Failed` and `Deleted` as terminal** — stop polling
on any of them. But do not wait for `Failed` as the signal that something broke: it is how a
failed *pipeline run* surfaces, while a classic job that went wrong is usually `Stopped`, or
`Complete` with no useful output. Check `logs`, don't wait for a status.

`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=alphafold" | head -c 300   # no key needed
tamarind auth status                                                        # key is live
tamarind validate alphafold --set sequence=MTYKLILNGKTLKGETTTEAVDAATAEK --name probe
```

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

## Troubleshooting

| Symptom | Meaning |
|---|---|
| `400 Missing or incorrect api key` | No/invalid key. **Not 401.** 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 | Unknown job is **400, not 404**. Don't branch on 404. |
| `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. **Three documented exceptions, which is why you branch on "not 2xx" rather
than on a status:**

- `/api/models` and `/api/finetuned-models` answer **401** with a bare scalar (`-1` and
  `Unauthenticated`) and carry no recovery fields.
- `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` returns a credential-bearing URL; keep it out of agent and CI logs.
- 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 |
