[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M73KATEQ37JEDHHGKYHSZK40":3,"skill-same:01M73KATEQ37JEDHHGKYHSZK40":48},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":29,"ownerHtmlUrl":30,"ownerBlogUrl":31,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M73KATEQ37JEDHHGKYHSZK40","zinc-database",null,"Query the ZINC22 virtual compound library (CartBlanche API, billions of make-on-demand + purchasable molecules). Look up substances by ZINC ID, resolve a SMILES to its ZINC ID (exact match), inspect purchasability\u002Fcatalogs, and assemble compound sets for docking. Property (MW\u002FlogP) filtering is done locally with RDKit. For bioactivity use chembl-database-bioactivity; for approved drugs use drugbank-database-access.","bg-szy\u002FTOP-SKILLS","bg-szy","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F106576190?v=4","01M73A4E8KPV67SFXRY8XZGNBX","ShuZhiyong","01M73A4E96MQ294MHM74TZBVQD",6,"CC-BY-4.0","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fzinc-database\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: \"zinc-database\"\ndescription: \"Query the ZINC22 virtual compound library (CartBlanche API, billions of make-on-demand + purchasable molecules). Look up substances by ZINC ID, resolve a SMILES to its ZINC ID (exact match), inspect purchasability\u002Fcatalogs, and assemble compound sets for docking. Property (MW\u002FlogP) filtering is done locally with RDKit. For bioactivity use chembl-database-bioactivity; for approved drugs use drugbank-database-access.\"\nlicense: \"CC-BY-4.0\"\n---\n\n# ZINC Chemical Library Database (ZINC22 \u002F CartBlanche)\n\n## Overview\n\nZINC (ZINC Is Not Commercial) is a free database of commercially available compounds curated for virtual screening. The current generation, **ZINC22**, holds billions of make-on-demand and in-stock molecules and is served through the **CartBlanche** web application at `cartblanche22.docking.org`. CartBlanche exposes a small JSON API to look up substances by ZINC ID, resolve a SMILES to its ZINC identifier, and inspect supplier\u002Fcatalog purchasability.\n\n> **Important — the old ZINC15 REST API no longer works for automated access.**\n> `zinc15.docking.org` (and `zinc.docking.org`) now sits behind a site-wide CAPTCHA: every request to `\u002Fsubstances.json`, `\u002Ftranches\u002F...`, `\u002Fsubstances\u002F{id}.json`, etc. is redirected to a `\u002Fcaptcha\u002F` page or returns `403 Forbidden`. Any script using the `mwt__gte` \u002F `availability` \u002F `similarity` query parameters against `zinc15.docking.org` will fail. Use the ZINC22 CartBlanche endpoints documented below instead.\n\nThree things about ZINC22\u002FCartBlanche that change how you use this skill:\n\n1. **There is no server-side property-range query.** ZINC22\u002FCartBlanche has no `mwt__gte` \u002F `logp__lte` \u002F `hbd__lte` search. You filter by molecular property by either (a) selecting **tranches** (a 2D MW x logP grid) for bulk download, or (b) retrieving compounds and filtering locally with RDKit. This skill shows the local-RDKit approach.\n2. **SMILES lookup is asynchronous.** You POST a SMILES, receive a `task` id, and poll for the result. Exact-match lookup completes in seconds.\n3. **Reliable programmatic search = exact match.** The API's broader analog search (`dist` > 0) is slow and frequently times out on the public server, and the Smallworld similarity search is a website-only flow. Treat exact SMILES → ZINC ID lookup as the dependable primitive; see the \"Analog \u002F Similarity Search\" note for the (limited) alternatives.\n\n## When to Use\n\n- Looking up a known ZINC ID to get its SMILES, computed properties, and purchasability\u002Fsuppliers\n- Resolving a molecule you have as a SMILES to its ZINC22 identifier(s) to check availability\n- Checking whether a hit compound is purchasable and from which catalogs before ordering\n- Assembling a SMILES\u002FZINC-ID set to feed into a docking campaign\n- For property-filtered library building, combine SMILES\u002Ftranche retrieval here with local RDKit filtering (`rdkit-cheminformatics`)\n- For known drug bioactivity data use `chembl-database-bioactivity`; for approved drug structures use `drugbank-database-access`\n\n## Prerequisites\n\n- **Python packages**: `requests`, `pandas` (and `rdkit` for local property filtering)\n- **Data requirements**: a ZINC ID, or a SMILES string\n- **Environment**: internet connection; no API key needed; SMILES lookups are async (poll for the result)\n- **Rate limits**: be courteous — serialize searches, cache results, and do not poll the task endpoint faster than once every few seconds\n\n```bash\npip install requests pandas\n# optional, for local property filtering:\npip install rdkit\n```\n\n## Quick Start\n\n```python\nimport requests\n\nBASE = \"https:\u002F\u002Fcartblanche22.docking.org\"\nHEADERS = {\"User-Agent\": \"sciagent-zinc-skill\u002F1.0\"}\n\n# Look up a substance by ZINC ID (synchronous, returns JSON immediately)\nr = requests.get(f\"{BASE}\u002Fsubstance\u002FZINC000000029632.json\", headers=HEADERS, timeout=30)\nr.raise_for_status()\nc = r.json()\n\ntd = c[\"tranche_details\"]\nprint(f\"ZINC ID : {c['zinc_id']}  (db: {c['db']})\")\nprint(f\"SMILES  : {c['smiles']}\")\nprint(f\"MW      : {td['mwt']:.2f}   logP: {td['logp']:.2f}   heavy atoms: {td['heavy_atoms']}\")\nprint(f\"InChIKey: {td['inchikey']}\")\nprint(f\"Catalogs: {len(c.get('catalogs', []))} supplier entries\")\n```\n\nExpected output (abridged):\n\n```\nZINC ID : ZINC000000029632  (db: zinc20)\nSMILES  : O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1\nMW      : 387.48   logP: 3.29   heavy atoms: 29\nInChIKey: QCEMBLKSDIWPBK-JOCHJYFZSA-N\nCatalogs: 4 supplier entries\n```\n\n## Core API\n\nThe CartBlanche base URL and two reusable helpers used throughout:\n\n```python\nimport requests, time\n\nBASE = \"https:\u002F\u002Fcartblanche22.docking.org\"\nHEADERS = {\"User-Agent\": \"sciagent-zinc-skill\u002F1.0\"}\n\ndef get_substance(zinc_id):\n    \"\"\"Fetch full record for one ZINC ID (synchronous).\"\"\"\n    r = requests.get(f\"{BASE}\u002Fsubstance\u002F{zinc_id}.json\", headers=HEADERS, timeout=30)\n    r.raise_for_status()\n    return r.json()\n\ndef smiles_lookup(smiles, dist=0, adist=0, database=\"zinc22\", timeout=120, poll=5):\n    \"\"\"Resolve a SMILES to ZINC22 substance dicts via the async task API.\n\n    dist=0, adist=0 -> exact match (fast, reliable; use this).\n    dist\u002Fadist > 0  -> near-neighbor analog search (slow; may time out — see note).\n    Returns a list of substance dicts (possibly empty).\n    \"\"\"\n    files = {\"smiles\": (None, smiles), \"dist\": (None, str(dist)),\n             \"adist\": (None, str(adist)), \"database\": (None, database)}\n    sub = requests.post(f\"{BASE}\u002Fsmiles.json\", files=files, headers=HEADERS, timeout=60)\n    sub.raise_for_status()\n    task = sub.json()[\"task\"]                       # async: returns {\"task\": \"\u003Cuuid>\"}\n\n    deadline = time.time() + timeout\n    while time.time() \u003C deadline:\n        res = requests.get(f\"{BASE}\u002Fsearch\u002Fresult\u002F{task}\", headers=HEADERS, timeout=30).json()\n        if res.get(\"status\") == \"SUCCESS\":\n            data = res.get(\"result\")\n            # result is {\"zinc22\": [...], \"zinc20\": [...]} when hits exist,\n            # or an empty list when there are none.\n            if isinstance(data, dict):\n                return data.get(\"zinc22\", []) + data.get(\"zinc20\", [])\n            return data or []\n        time.sleep(poll)                            # status == \"PROGRESS\" until done\n    raise TimeoutError(f\"ZINC22 search task {task} did not finish in {timeout}s\")\n```\n\n### Query 1: Retrieve a Compound by ZINC ID\n\nFetch the full record (SMILES, computed properties, formula, ring\u002Fheteroatom counts, supplier catalogs) for a known ZINC identifier.\n\n```python\nc = get_substance(\"ZINC000000029632\")\ntd = c[\"tranche_details\"]\n\nprint(f\"ZINC ID    : {c['zinc_id']}\")\nprint(f\"SMILES     : {c['smiles']}\")\nprint(f\"Formula    : {c['mol_formula']}\")\nprint(f\"MW         : {td['mwt']:.2f}\")\nprint(f\"logP       : {td['logp']:.2f}\")\nprint(f\"Heavy atoms: {td['heavy_atoms']}\")\nprint(f\"Rings      : {c['rings']}   Hetero atoms: {c['hetero_atoms']}\")\nprint(f\"InChIKey   : {td['inchikey']}\")\nprint(f\"Suppliers  : {len(c.get('catalogs', []))}\")\n```\n\nBoth the modern `ZINCbq...`\u002F`ZINCbj...` style IDs and the legacy 12-digit `ZINC000...` IDs resolve through this endpoint.\n\n### Query 2: Resolve a ZINC ID from a SMILES (Exact Match)\n\nFind the ZINC22 identifier(s) for a molecule you have as a SMILES string. This is the dependable search primitive.\n\n```python\nhits = smiles_lookup(\"CC(=O)Nc1ccc(O)cc1\")   # acetaminophen, exact (dist=0)\nprint(f\"Exact matches: {len(hits)}\")\nfor h in hits[:5]:\n    td = h[\"tranche_details\"]\n    print(f\"  {h['zinc_id']:22s}  MW {td['mwt']:.1f}  logP {td['logp']:.2f}  {h['smiles']}\")\n```\n\nA single molecule can map to several ZINC IDs across ZINC22 sub-databases (e.g. `ZINCbq...` and `ZINCbj...`) — each represents the same structure in a different build.\n\n### Query 3: Purchasability and Supplier Catalogs\n\nPurchasability is embedded in the substance record under `catalogs` — no separate request needed.\n\n```python\nc = get_substance(\"ZINC000000029632\")\ncatalogs = c.get(\"catalogs\", [])\nprint(f\"Supplier entries: {len(catalogs)}\")\nfor cat in catalogs:\n    print(f\"  {cat.get('catalog_name', 'n\u002Fa'):24s} | \"\n          f\"purchasable: {cat.get('purchase')} | \"\n          f\"code: {cat.get('supplier_code', 'n\u002Fa')} | \"\n          f\"price: {cat.get('price', 'n\u002Fa')} {cat.get('unit', '')}\")\n```\n\n`purchase: 1` indicates the compound is orderable from that catalog; entries may include price, quantity, and lead time (`shipping`).\n\n### Query 4: Property-Filtered Library (Local RDKit Filtering)\n\nZINC22 has no server-side MW\u002FlogP query. The portable pattern is: gather candidate SMILES (resolved ZINC IDs, a downloaded tranche file, or your own enumeration), then filter locally with RDKit.\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors, Lipinski\n\n# Candidate SMILES (here: a small hand list; in practice load a tranche .smi or your own set)\nsmiles_list = [\n    \"CC(=O)Nc1ccc(O)cc1\",            # acetaminophen  (MW 151 - too small, filtered out)\n    \"CC(C)NCC(O)COc1cccc2ccccc12\",   # propranolol    (MW 259, logP 2.6 - lead-like)\n    \"CN(C)CCOC(c1ccccc1)c1ccccc1\",   # diphenhydramine\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",      # benzanilide    (MW 197 - too small, filtered out)\n]\n\ndef lead_like(smi):\n    m = Chem.MolFromSmiles(smi)\n    if m is None:\n        return None\n    mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)\n    hbd, hba = Lipinski.NumHDonors(m), Lipinski.NumHAcceptors(m)\n    ok = 250 \u003C= mw \u003C= 350 and 1 \u003C= logp \u003C= 3 and hbd \u003C= 3 and hba \u003C= 7\n    return {\"smiles\": Chem.MolToSmiles(m), \"mw\": round(mw, 1), \"logp\": round(logp, 2),\n            \"hbd\": hbd, \"hba\": hba} if ok else None\n\nrows = [r for r in (lead_like(s) for s in smiles_list) if r]\ndf = pd.DataFrame(rows).drop_duplicates(subset=[\"smiles\"])\nprint(f\"Lead-like compounds after RDKit filter: {len(df)}\")\nprint(df.head())\n```\n\n### Analog \u002F Similarity Search (limited — read before relying on it)\n\nThe submit endpoint accepts `dist` (topological graph-edit distance, 0–3) and `adist` (\"anon\" distance, scaffold hops) for a near-neighbor **analog** search:\n\n```python\n# Near-neighbor analogs. Keep dist small (1) and the timeout generous; expect FEW results.\ntry:\n    analogs = smiles_lookup(\"c1ccc(NC(=O)c2ccccc2)cc1\", dist=1, adist=0, timeout=240)\n    print(f\"Analogs: {len(analogs)}\")\n    for h in analogs[:10]:\n        print(\"  \", h[\"zinc_id\"], h[\"smiles\"])\nexcept TimeoutError as e:\n    print(\"Analog search timed out:\", e)\n```\n\nCaveats verified against the live public server:\n- `dist=0` (exact) is fast and reliable. `dist >= 1` runs across billions of molecules on a shared cluster and **frequently times out** (e.g. `dist=3` did not return within ~270 s); even when it succeeds it often returns only the query and a handful of near-identical structures.\n- For broad **whole-molecule similarity** (Tanimoto over graph-edit distance), CartBlanche provides a separate **Smallworld** search on the website (`cartblanche22.docking.org` → Similarity → Smallworld, `\u002Fsimilarity\u002Fsw`). It is an interactive flow and is not exposed as a simple GET\u002FPOST JSON endpoint here — use the web UI for large similarity jobs.\n- Do not build automated pipelines that assume analog search returns a rich set. Base workflows on exact lookup + local RDKit\u002Fcheminformatics expansion instead.\n\n## Key Concepts\n\n### ZINC22 IDs\n\nZINC22 introduced short alphanumeric IDs such as `ZINCbq0000001gLA` (the prefix letters encode the sub-database \u002F tranche). Legacy 12-digit IDs like `ZINC000000029632` (from ZINC15\u002FZINC20) still resolve through `\u002Fsubstance\u002F{id}.json`. Always pass the ID exactly as given.\n\n### Asynchronous Search Model\n\nSMILES searches are queued as tasks:\n\n1. `POST \u002Fsmiles.json` (multipart form: `smiles`, `dist`, `adist`, `database`) → `{\"task\": \"\u003Cuuid>\"}`\n2. `GET \u002Fsearch\u002Fresult\u002F{task}` → `{\"status\": \"PROGRESS\"}` while running, then `{\"status\": \"SUCCESS\", \"result\": {...}}`\n3. On success, `result` is a dict keyed by sub-database (`zinc22`, sometimes `zinc20`) when there are hits, or an empty list when there are none. The helper above normalizes both shapes.\n\n### Tranches\n\nZINC organizes molecules into a 2D grid of \"tranches\" by heavy-atom count \u002F MW (H-codes, e.g. `H11`) and logP (letter bins). The CartBlanche **Tranches** browser (`cartblanche22.docking.org`, \"3D\"\u002F\"2D\" tranche pages) lets you select property regions and bulk-download SMILES\u002FSDF for docking. Tranche bulk download is interactive (and large); for programmatic property filtering prefer the RDKit approach in Query 4, or fetch tranche files from `files.docking.org` and filter locally.\n\n### Result Fields\n\nEach search\u002Fsubstance record exposes: `zinc_id`, `smiles`, `mol_formula`, `rings`, `hetero_atoms`, `db`, `catalogs[]`, and `tranche_details` (`mwt`, `logp`, `heavy_atoms`, `inchi`, `inchikey`). SMILES search rows additionally include `sub_id`, `matched_smiles`, and the `tranche` code.\n\n## Common Workflows\n\n### Workflow 1: Check Purchasability of a Hit List\n\n**Goal**: Given SMILES of docking hits, resolve their ZINC22 IDs and report which are purchasable.\n\n```python\nimport pandas as pd\n\nhit_smiles = [\n    \"CC(=O)Nc1ccc(O)cc1\",            # acetaminophen\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",      # benzanilide\n]\n\nrows = []\nfor smi in hit_smiles:\n    matches = smiles_lookup(smi)                   # exact\n    if not matches:\n        rows.append({\"query_smiles\": smi, \"zinc_id\": None, \"purchasable\": False})\n        continue\n    for m in matches:\n        cats = m.get(\"catalogs\", [])\n        rows.append({\n            \"query_smiles\": smi,\n            \"zinc_id\": m[\"zinc_id\"],\n            \"mw\": m[\"tranche_details\"][\"mwt\"],\n            \"n_catalogs\": len(cats),\n            \"purchasable\": any(c.get(\"purchase\") for c in cats) or bool(cats),\n        })\n\ndf = pd.DataFrame(rows)\nprint(df)\ndf.to_csv(\"hit_purchasability.csv\", index=False)\nprint(\"Saved: hit_purchasability.csv\")\n```\n\n### Workflow 2: Resolve and Property-Filter a Candidate Set for Docking\n\n**Goal**: Take a list of candidate SMILES (your own enumeration or a tranche file), keep the ones present\u002Fpurchasable in ZINC22, filter to lead-like space, and export SMILES for docking.\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors\n\ncandidates = [\n    \"CC(=O)Nc1ccc(O)cc1\",\n    \"O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1\",\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",\n]\n\nrows = []\nfor smi in candidates:\n    m = Chem.MolFromSmiles(smi)\n    if m is None:\n        continue\n    mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)\n    if not (200 \u003C= mw \u003C= 500 and -1 \u003C= logp \u003C= 5):     # lead-like local filter\n        continue\n    matches = smiles_lookup(smi)                        # confirm it exists in ZINC22\n    if not matches:\n        continue\n    m0 = matches[0]\n    rows.append({\"zinc_id\": m0[\"zinc_id\"], \"smiles\": Chem.MolToSmiles(m),\n                 \"mw\": round(mw, 1), \"logp\": round(logp, 2),\n                 \"purchasable\": bool(m0.get(\"catalogs\"))})\n\ndf = pd.DataFrame(rows).drop_duplicates(subset=[\"smiles\"])\nprint(f\"ZINC22-confirmed lead-like compounds: {len(df)}\")\ndf[[\"smiles\", \"zinc_id\"]].to_csv(\"docking_library.smi\", sep=\" \", index=False, header=False)\nprint(\"Saved: docking_library.smi\")\nprint(df)\n```\n\n## Key Parameters\n\n| Parameter | Endpoint | Default | Range \u002F Options | Effect |\n|-----------|----------|---------|-----------------|--------|\n| (path) | `GET \u002Fsubstance\u002F{zinc_id}.json` | — | ZINC ID | Synchronous substance lookup |\n| `smiles` | `POST \u002Fsmiles.json` | — | valid SMILES | Query molecule |\n| `dist` | `POST \u002Fsmiles.json` | `0` | `0`–`3` | Topological distance; `0` = exact (reliable), `>=1` = analog search (slow, may time out) |\n| `adist` | `POST \u002Fsmiles.json` | `0` | `0`–`3` | Anonymous-graph distance (scaffold hopping); `0` = off |\n| `database` | `POST \u002Fsmiles.json` | `zinc22` | `\"zinc22\"`, `\"zinc20\"` | Which ZINC build to search |\n| `task` | `GET \u002Fsearch\u002Fresult\u002F{task}` | — | UUID from submit | Poll handle for async result |\n\n## Best Practices\n\n1. **Use ZINC22 (CartBlanche), not ZINC15.** The `zinc15.docking.org` REST API is CAPTCHA-walled and will not work from scripts. All endpoints here target `cartblanche22.docking.org`.\n\n2. **Lean on exact lookup; don't depend on analog search.** `dist=0` is the reliable primitive. For analogs\u002Fsimilarity, expand locally (RDKit, `rdkit-cheminformatics`) or use the Smallworld web UI — the analog API times out on broad queries.\n\n3. **Filter properties locally with RDKit.** There is no server-side MW\u002FlogP\u002FHBD query in ZINC22. Retrieve candidates, then filter with RDKit — this is also where you apply PAINS\u002FBrenk alerts.\n\n4. **Poll politely.** SMILES lookups are async; poll `\u002Fsearch\u002Fresult\u002F{task}` every few seconds, set a sensible timeout, and handle the `PROGRESS` → `SUCCESS` transition (and `TimeoutError`).\n\n5. **Deduplicate by canonical SMILES.** A structure can appear under multiple ZINC IDs \u002F catalogs. Canonicalize with RDKit (`Chem.MolToSmiles(Chem.MolFromSmiles(smi))`) before docking.\n\n6. **Verify purchasability via `catalogs`.** Treat `catalogs` entries with `purchase: 1` as orderable; record `supplier_code` and `catalog_name` for procurement.\n\n7. **Cache results.** ZINC data updates periodically; cache substance\u002Fsearch JSON with a date-stamped filename and avoid re-querying within a project.\n\n## Common Recipes\n\n### Recipe: Lookup ZINC22 ID and purchasability from a SMILES\n\n```python\nmatches = smiles_lookup(\"CC(=O)Nc1ccc(O)cc1\")   # acetaminophen\nfor m in matches:\n    cats = m.get(\"catalogs\", [])\n    print(f\"{m['zinc_id']} | MW {m['tranche_details']['mwt']:.1f} | \"\n          f\"catalogs {len(cats)} | purchasable {bool(cats)}\")\n```\n\n### Recipe: Batch-resolve a list of ZINC IDs\n\n```python\nzinc_ids = [\"ZINC000000029632\", \"ZINCbq0000001gLA\"]\nfor zid in zinc_ids:\n    try:\n        c = get_substance(zid)\n        print(f\"{zid}: {c['smiles']}  MW {c['tranche_details']['mwt']:.1f}\")\n    except requests.HTTPError as e:\n        print(f\"{zid}: not found ({e.response.status_code})\")\n```\n\n### Recipe: Property Distribution of a Library\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors\n\ndf = pd.read_csv(\"docking_library.smi\", sep=\" \", names=[\"smiles\", \"zinc_id\"])\ndf[\"mw\"] = df[\"smiles\"].map(lambda s: Descriptors.MolWt(Chem.MolFromSmiles(s)))\ndf[\"logp\"] = df[\"smiles\"].map(lambda s: Descriptors.MolLogP(Chem.MolFromSmiles(s)))\nprint(f\"Library size: {len(df)}\")\nprint(df[[\"mw\", \"logp\"]].describe())\n```\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| Requests to `zinc15.docking.org` redirect to `\u002Fcaptcha\u002F` or return `403` | The legacy ZINC15 REST API is CAPTCHA-walled and unusable from scripts | Switch all calls to `cartblanche22.docking.org` (ZINC22 \u002F CartBlanche) as shown here |\n| `GET \u002Fsubstances\u002F{id}.json` (plural) returns HTML | Wrong path — that is the SPA catch-all | Use the **singular** `\u002Fsubstance\u002F{id}.json` |\n| Endpoint returns `200` with `content-type: application\u002Fjson` but body is HTML | CartBlanche serves the SPA shell for unknown routes (misleading content-type) | Check the body actually parses as JSON; only the documented endpoints return data |\n| `POST \u002Fsmiles.json` returns `{\"task\": ...}` but you expected results | SMILES search is asynchronous | Poll `GET \u002Fsearch\u002Fresult\u002F{task}` until `status == \"SUCCESS\"` |\n| `400 No Valid SMILES, please try again` | SMILES rejected (or wrong request form) | Validate the SMILES with RDKit; submit via multipart form fields `smiles`\u002F`dist`\u002F`adist`\u002F`database` |\n| `KeyError: 'data'` \u002F `'zinc22'` on a successful task | `result` is a dict when there are hits, but an empty list when there are none | Handle both shapes (see `smiles_lookup` helper) |\n| Analog search (`dist >= 1`) times out or returns only the exact match | Very large search space on a shared cluster | Use `dist=0` (exact); for similarity use the Smallworld web UI; expand analogs locally with RDKit |\n| `\u002Fsubstance\u002Frandom.json` returns `400`; `\u002Fsubstance\u002Frandom\u002F{n}.json` returns `{\"status\":\"PENDING\"}` | The random endpoint is async\u002Fnon-trivial | Not needed for scripting; pick known ZINC IDs instead |\n| `HTTP 404` for a compound ID | ID typo or not present in this build | Verify the ZINC ID; try `database=\"zinc20\"` for legacy IDs |\n\n## Related Skills\n\n- `rdkit-cheminformatics` — Compute properties and apply PAINS\u002FBrenk filters on retrieved ZINC compounds (required for property filtering here)\n- `autodock-vina-docking` — Use exported ZINC SMILES\u002FSDF files for molecular docking campaigns\n- `chembl-database-bioactivity` — Bioactivity data for compounds identified in ZINC virtual screens\n- `drugbank-database-access` — Approved-drug structures and annotations\n\n## References\n\n- [CartBlanche \u002F ZINC22 web app](https:\u002F\u002Fcartblanche22.docking.org\u002F) — Substance lookup, SMILES search, tranche browser\n- [ZINC22 paper](https:\u002F\u002Fdoi.org\u002F10.1021\u002Facs.jcim.2c01253) — Tingle et al., \"ZINC-22, A Free Multi-Billion-Scale Database of Tangible Compounds for Ligand Discovery,\" J. Chem. Inf. Model. 2023\n- [docking.org wiki](https:\u002F\u002Fwiki.docking.org\u002F) — CartBlanche usage and ZINC22 download documentation\n- [files.docking.org](https:\u002F\u002Ffiles.docking.org\u002F) — Bulk tranche file downloads for ZINC22\n","# ZINC Chemical Library Database (ZINC22 \u002F CartBlanche)\n\n## Overview\n\nZINC (ZINC Is Not Commercial) is a free database of commercially available compounds curated for virtual screening. The current generation, **ZINC22**, holds billions of make-on-demand and in-stock molecules and is served through the **CartBlanche** web application at `cartblanche22.docking.org`. CartBlanche exposes a small JSON API to look up substances by ZINC ID, resolve a SMILES to its ZINC identifier, and inspect supplier\u002Fcatalog purchasability.\n\n> **Important — the old ZINC15 REST API no longer works for automated access.**\n> `zinc15.docking.org` (and `zinc.docking.org`) now sits behind a site-wide CAPTCHA: every request to `\u002Fsubstances.json`, `\u002Ftranches\u002F...`, `\u002Fsubstances\u002F{id}.json`, etc. is redirected to a `\u002Fcaptcha\u002F` page or returns `403 Forbidden`. Any script using the `mwt__gte` \u002F `availability` \u002F `similarity` query parameters against `zinc15.docking.org` will fail. Use the ZINC22 CartBlanche endpoints documented below instead.\n\nThree things about ZINC22\u002FCartBlanche that change how you use this skill:\n\n1. **There is no server-side property-range query.** ZINC22\u002FCartBlanche has no `mwt__gte` \u002F `logp__lte` \u002F `hbd__lte` search. You filter by molecular property by either (a) selecting **tranches** (a 2D MW x logP grid) for bulk download, or (b) retrieving compounds and filtering locally with RDKit. This skill shows the local-RDKit approach.\n2. **SMILES lookup is asynchronous.** You POST a SMILES, receive a `task` id, and poll for the result. Exact-match lookup completes in seconds.\n3. **Reliable programmatic search = exact match.** The API's broader analog search (`dist` > 0) is slow and frequently times out on the public server, and the Smallworld similarity search is a website-only flow. Treat exact SMILES → ZINC ID lookup as the dependable primitive; see the \"Analog \u002F Similarity Search\" note for the (limited) alternatives.\n\n## When to Use\n\n- Looking up a known ZINC ID to get its SMILES, computed properties, and purchasability\u002Fsuppliers\n- Resolving a molecule you have as a SMILES to its ZINC22 identifier(s) to check availability\n- Checking whether a hit compound is purchasable and from which catalogs before ordering\n- Assembling a SMILES\u002FZINC-ID set to feed into a docking campaign\n- For property-filtered library building, combine SMILES\u002Ftranche retrieval here with local RDKit filtering (`rdkit-cheminformatics`)\n- For known drug bioactivity data use `chembl-database-bioactivity`; for approved drug structures use `drugbank-database-access`\n\n## Prerequisites\n\n- **Python packages**: `requests`, `pandas` (and `rdkit` for local property filtering)\n- **Data requirements**: a ZINC ID, or a SMILES string\n- **Environment**: internet connection; no API key needed; SMILES lookups are async (poll for the result)\n- **Rate limits**: be courteous — serialize searches, cache results, and do not poll the task endpoint faster than once every few seconds\n\n```bash\npip install requests pandas\n# optional, for local property filtering:\npip install rdkit\n```\n\n## Quick Start\n\n```python\nimport requests\n\nBASE = \"https:\u002F\u002Fcartblanche22.docking.org\"\nHEADERS = {\"User-Agent\": \"sciagent-zinc-skill\u002F1.0\"}\n\n# Look up a substance by ZINC ID (synchronous, returns JSON immediately)\nr = requests.get(f\"{BASE}\u002Fsubstance\u002FZINC000000029632.json\", headers=HEADERS, timeout=30)\nr.raise_for_status()\nc = r.json()\n\ntd = c[\"tranche_details\"]\nprint(f\"ZINC ID : {c['zinc_id']}  (db: {c['db']})\")\nprint(f\"SMILES  : {c['smiles']}\")\nprint(f\"MW      : {td['mwt']:.2f}   logP: {td['logp']:.2f}   heavy atoms: {td['heavy_atoms']}\")\nprint(f\"InChIKey: {td['inchikey']}\")\nprint(f\"Catalogs: {len(c.get('catalogs', []))} supplier entries\")\n```\n\nExpected output (abridged):\n\n```\nZINC ID : ZINC000000029632  (db: zinc20)\nSMILES  : O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1\nMW      : 387.48   logP: 3.29   heavy atoms: 29\nInChIKey: QCEMBLKSDIWPBK-JOCHJYFZSA-N\nCatalogs: 4 supplier entries\n```\n\n## Core API\n\nThe CartBlanche base URL and two reusable helpers used throughout:\n\n```python\nimport requests, time\n\nBASE = \"https:\u002F\u002Fcartblanche22.docking.org\"\nHEADERS = {\"User-Agent\": \"sciagent-zinc-skill\u002F1.0\"}\n\ndef get_substance(zinc_id):\n    \"\"\"Fetch full record for one ZINC ID (synchronous).\"\"\"\n    r = requests.get(f\"{BASE}\u002Fsubstance\u002F{zinc_id}.json\", headers=HEADERS, timeout=30)\n    r.raise_for_status()\n    return r.json()\n\ndef smiles_lookup(smiles, dist=0, adist=0, database=\"zinc22\", timeout=120, poll=5):\n    \"\"\"Resolve a SMILES to ZINC22 substance dicts via the async task API.\n\n    dist=0, adist=0 -> exact match (fast, reliable; use this).\n    dist\u002Fadist > 0  -> near-neighbor analog search (slow; may time out — see note).\n    Returns a list of substance dicts (possibly empty).\n    \"\"\"\n    files = {\"smiles\": (None, smiles), \"dist\": (None, str(dist)),\n             \"adist\": (None, str(adist)), \"database\": (None, database)}\n    sub = requests.post(f\"{BASE}\u002Fsmiles.json\", files=files, headers=HEADERS, timeout=60)\n    sub.raise_for_status()\n    task = sub.json()[\"task\"]                       # async: returns {\"task\": \"\u003Cuuid>\"}\n\n    deadline = time.time() + timeout\n    while time.time() \u003C deadline:\n        res = requests.get(f\"{BASE}\u002Fsearch\u002Fresult\u002F{task}\", headers=HEADERS, timeout=30).json()\n        if res.get(\"status\") == \"SUCCESS\":\n            data = res.get(\"result\")\n            # result is {\"zinc22\": [...], \"zinc20\": [...]} when hits exist,\n            # or an empty list when there are none.\n            if isinstance(data, dict):\n                return data.get(\"zinc22\", []) + data.get(\"zinc20\", [])\n            return data or []\n        time.sleep(poll)                            # status == \"PROGRESS\" until done\n    raise TimeoutError(f\"ZINC22 search task {task} did not finish in {timeout}s\")\n```\n\n### Query 1: Retrieve a Compound by ZINC ID\n\nFetch the full record (SMILES, computed properties, formula, ring\u002Fheteroatom counts, supplier catalogs) for a known ZINC identifier.\n\n```python\nc = get_substance(\"ZINC000000029632\")\ntd = c[\"tranche_details\"]\n\nprint(f\"ZINC ID    : {c['zinc_id']}\")\nprint(f\"SMILES     : {c['smiles']}\")\nprint(f\"Formula    : {c['mol_formula']}\")\nprint(f\"MW         : {td['mwt']:.2f}\")\nprint(f\"logP       : {td['logp']:.2f}\")\nprint(f\"Heavy atoms: {td['heavy_atoms']}\")\nprint(f\"Rings      : {c['rings']}   Hetero atoms: {c['hetero_atoms']}\")\nprint(f\"InChIKey   : {td['inchikey']}\")\nprint(f\"Suppliers  : {len(c.get('catalogs', []))}\")\n```\n\nBoth the modern `ZINCbq...`\u002F`ZINCbj...` style IDs and the legacy 12-digit `ZINC000...` IDs resolve through this endpoint.\n\n### Query 2: Resolve a ZINC ID from a SMILES (Exact Match)\n\nFind the ZINC22 identifier(s) for a molecule you have as a SMILES string. This is the dependable search primitive.\n\n```python\nhits = smiles_lookup(\"CC(=O)Nc1ccc(O)cc1\")   # acetaminophen, exact (dist=0)\nprint(f\"Exact matches: {len(hits)}\")\nfor h in hits[:5]:\n    td = h[\"tranche_details\"]\n    print(f\"  {h['zinc_id']:22s}  MW {td['mwt']:.1f}  logP {td['logp']:.2f}  {h['smiles']}\")\n```\n\nA single molecule can map to several ZINC IDs across ZINC22 sub-databases (e.g. `ZINCbq...` and `ZINCbj...`) — each represents the same structure in a different build.\n\n### Query 3: Purchasability and Supplier Catalogs\n\nPurchasability is embedded in the substance record under `catalogs` — no separate request needed.\n\n```python\nc = get_substance(\"ZINC000000029632\")\ncatalogs = c.get(\"catalogs\", [])\nprint(f\"Supplier entries: {len(catalogs)}\")\nfor cat in catalogs:\n    print(f\"  {cat.get('catalog_name', 'n\u002Fa'):24s} | \"\n          f\"purchasable: {cat.get('purchase')} | \"\n          f\"code: {cat.get('supplier_code', 'n\u002Fa')} | \"\n          f\"price: {cat.get('price', 'n\u002Fa')} {cat.get('unit', '')}\")\n```\n\n`purchase: 1` indicates the compound is orderable from that catalog; entries may include price, quantity, and lead time (`shipping`).\n\n### Query 4: Property-Filtered Library (Local RDKit Filtering)\n\nZINC22 has no server-side MW\u002FlogP query. The portable pattern is: gather candidate SMILES (resolved ZINC IDs, a downloaded tranche file, or your own enumeration), then filter locally with RDKit.\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors, Lipinski\n\n# Candidate SMILES (here: a small hand list; in practice load a tranche .smi or your own set)\nsmiles_list = [\n    \"CC(=O)Nc1ccc(O)cc1\",            # acetaminophen  (MW 151 - too small, filtered out)\n    \"CC(C)NCC(O)COc1cccc2ccccc12\",   # propranolol    (MW 259, logP 2.6 - lead-like)\n    \"CN(C)CCOC(c1ccccc1)c1ccccc1\",   # diphenhydramine\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",      # benzanilide    (MW 197 - too small, filtered out)\n]\n\ndef lead_like(smi):\n    m = Chem.MolFromSmiles(smi)\n    if m is None:\n        return None\n    mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)\n    hbd, hba = Lipinski.NumHDonors(m), Lipinski.NumHAcceptors(m)\n    ok = 250 \u003C= mw \u003C= 350 and 1 \u003C= logp \u003C= 3 and hbd \u003C= 3 and hba \u003C= 7\n    return {\"smiles\": Chem.MolToSmiles(m), \"mw\": round(mw, 1), \"logp\": round(logp, 2),\n            \"hbd\": hbd, \"hba\": hba} if ok else None\n\nrows = [r for r in (lead_like(s) for s in smiles_list) if r]\ndf = pd.DataFrame(rows).drop_duplicates(subset=[\"smiles\"])\nprint(f\"Lead-like compounds after RDKit filter: {len(df)}\")\nprint(df.head())\n```\n\n### Analog \u002F Similarity Search (limited — read before relying on it)\n\nThe submit endpoint accepts `dist` (topological graph-edit distance, 0–3) and `adist` (\"anon\" distance, scaffold hops) for a near-neighbor **analog** search:\n\n```python\n# Near-neighbor analogs. Keep dist small (1) and the timeout generous; expect FEW results.\ntry:\n    analogs = smiles_lookup(\"c1ccc(NC(=O)c2ccccc2)cc1\", dist=1, adist=0, timeout=240)\n    print(f\"Analogs: {len(analogs)}\")\n    for h in analogs[:10]:\n        print(\"  \", h[\"zinc_id\"], h[\"smiles\"])\nexcept TimeoutError as e:\n    print(\"Analog search timed out:\", e)\n```\n\nCaveats verified against the live public server:\n- `dist=0` (exact) is fast and reliable. `dist >= 1` runs across billions of molecules on a shared cluster and **frequently times out** (e.g. `dist=3` did not return within ~270 s); even when it succeeds it often returns only the query and a handful of near-identical structures.\n- For broad **whole-molecule similarity** (Tanimoto over graph-edit distance), CartBlanche provides a separate **Smallworld** search on the website (`cartblanche22.docking.org` → Similarity → Smallworld, `\u002Fsimilarity\u002Fsw`). It is an interactive flow and is not exposed as a simple GET\u002FPOST JSON endpoint here — use the web UI for large similarity jobs.\n- Do not build automated pipelines that assume analog search returns a rich set. Base workflows on exact lookup + local RDKit\u002Fcheminformatics expansion instead.\n\n## Key Concepts\n\n### ZINC22 IDs\n\nZINC22 introduced short alphanumeric IDs such as `ZINCbq0000001gLA` (the prefix letters encode the sub-database \u002F tranche). Legacy 12-digit IDs like `ZINC000000029632` (from ZINC15\u002FZINC20) still resolve through `\u002Fsubstance\u002F{id}.json`. Always pass the ID exactly as given.\n\n### Asynchronous Search Model\n\nSMILES searches are queued as tasks:\n\n1. `POST \u002Fsmiles.json` (multipart form: `smiles`, `dist`, `adist`, `database`) → `{\"task\": \"\u003Cuuid>\"}`\n2. `GET \u002Fsearch\u002Fresult\u002F{task}` → `{\"status\": \"PROGRESS\"}` while running, then `{\"status\": \"SUCCESS\", \"result\": {...}}`\n3. On success, `result` is a dict keyed by sub-database (`zinc22`, sometimes `zinc20`) when there are hits, or an empty list when there are none. The helper above normalizes both shapes.\n\n### Tranches\n\nZINC organizes molecules into a 2D grid of \"tranches\" by heavy-atom count \u002F MW (H-codes, e.g. `H11`) and logP (letter bins). The CartBlanche **Tranches** browser (`cartblanche22.docking.org`, \"3D\"\u002F\"2D\" tranche pages) lets you select property regions and bulk-download SMILES\u002FSDF for docking. Tranche bulk download is interactive (and large); for programmatic property filtering prefer the RDKit approach in Query 4, or fetch tranche files from `files.docking.org` and filter locally.\n\n### Result Fields\n\nEach search\u002Fsubstance record exposes: `zinc_id`, `smiles`, `mol_formula`, `rings`, `hetero_atoms`, `db`, `catalogs[]`, and `tranche_details` (`mwt`, `logp`, `heavy_atoms`, `inchi`, `inchikey`). SMILES search rows additionally include `sub_id`, `matched_smiles`, and the `tranche` code.\n\n## Common Workflows\n\n### Workflow 1: Check Purchasability of a Hit List\n\n**Goal**: Given SMILES of docking hits, resolve their ZINC22 IDs and report which are purchasable.\n\n```python\nimport pandas as pd\n\nhit_smiles = [\n    \"CC(=O)Nc1ccc(O)cc1\",            # acetaminophen\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",      # benzanilide\n]\n\nrows = []\nfor smi in hit_smiles:\n    matches = smiles_lookup(smi)                   # exact\n    if not matches:\n        rows.append({\"query_smiles\": smi, \"zinc_id\": None, \"purchasable\": False})\n        continue\n    for m in matches:\n        cats = m.get(\"catalogs\", [])\n        rows.append({\n            \"query_smiles\": smi,\n            \"zinc_id\": m[\"zinc_id\"],\n            \"mw\": m[\"tranche_details\"][\"mwt\"],\n            \"n_catalogs\": len(cats),\n            \"purchasable\": any(c.get(\"purchase\") for c in cats) or bool(cats),\n        })\n\ndf = pd.DataFrame(rows)\nprint(df)\ndf.to_csv(\"hit_purchasability.csv\", index=False)\nprint(\"Saved: hit_purchasability.csv\")\n```\n\n### Workflow 2: Resolve and Property-Filter a Candidate Set for Docking\n\n**Goal**: Take a list of candidate SMILES (your own enumeration or a tranche file), keep the ones present\u002Fpurchasable in ZINC22, filter to lead-like space, and export SMILES for docking.\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors\n\ncandidates = [\n    \"CC(=O)Nc1ccc(O)cc1\",\n    \"O=C([C@H]1CCCN1C(=O)Cc1c[nH]c2ccccc12)N1CCc2ccccc2C1\",\n    \"c1ccc(NC(=O)c2ccccc2)cc1\",\n]\n\nrows = []\nfor smi in candidates:\n    m = Chem.MolFromSmiles(smi)\n    if m is None:\n        continue\n    mw, logp = Descriptors.MolWt(m), Descriptors.MolLogP(m)\n    if not (200 \u003C= mw \u003C= 500 and -1 \u003C= logp \u003C= 5):     # lead-like local filter\n        continue\n    matches = smiles_lookup(smi)                        # confirm it exists in ZINC22\n    if not matches:\n        continue\n    m0 = matches[0]\n    rows.append({\"zinc_id\": m0[\"zinc_id\"], \"smiles\": Chem.MolToSmiles(m),\n                 \"mw\": round(mw, 1), \"logp\": round(logp, 2),\n                 \"purchasable\": bool(m0.get(\"catalogs\"))})\n\ndf = pd.DataFrame(rows).drop_duplicates(subset=[\"smiles\"])\nprint(f\"ZINC22-confirmed lead-like compounds: {len(df)}\")\ndf[[\"smiles\", \"zinc_id\"]].to_csv(\"docking_library.smi\", sep=\" \", index=False, header=False)\nprint(\"Saved: docking_library.smi\")\nprint(df)\n```\n\n## Key Parameters\n\n| Parameter | Endpoint | Default | Range \u002F Options | Effect |\n|-----------|----------|---------|-----------------|--------|\n| (path) | `GET \u002Fsubstance\u002F{zinc_id}.json` | — | ZINC ID | Synchronous substance lookup |\n| `smiles` | `POST \u002Fsmiles.json` | — | valid SMILES | Query molecule |\n| `dist` | `POST \u002Fsmiles.json` | `0` | `0`–`3` | Topological distance; `0` = exact (reliable), `>=1` = analog search (slow, may time out) |\n| `adist` | `POST \u002Fsmiles.json` | `0` | `0`–`3` | Anonymous-graph distance (scaffold hopping); `0` = off |\n| `database` | `POST \u002Fsmiles.json` | `zinc22` | `\"zinc22\"`, `\"zinc20\"` | Which ZINC build to search |\n| `task` | `GET \u002Fsearch\u002Fresult\u002F{task}` | — | UUID from submit | Poll handle for async result |\n\n## Best Practices\n\n1. **Use ZINC22 (CartBlanche), not ZINC15.** The `zinc15.docking.org` REST API is CAPTCHA-walled and will not work from scripts. All endpoints here target `cartblanche22.docking.org`.\n\n2. **Lean on exact lookup; don't depend on analog search.** `dist=0` is the reliable primitive. For analogs\u002Fsimilarity, expand locally (RDKit, `rdkit-cheminformatics`) or use the Smallworld web UI — the analog API times out on broad queries.\n\n3. **Filter properties locally with RDKit.** There is no server-side MW\u002FlogP\u002FHBD query in ZINC22. Retrieve candidates, then filter with RDKit — this is also where you apply PAINS\u002FBrenk alerts.\n\n4. **Poll politely.** SMILES lookups are async; poll `\u002Fsearch\u002Fresult\u002F{task}` every few seconds, set a sensible timeout, and handle the `PROGRESS` → `SUCCESS` transition (and `TimeoutError`).\n\n5. **Deduplicate by canonical SMILES.** A structure can appear under multiple ZINC IDs \u002F catalogs. Canonicalize with RDKit (`Chem.MolToSmiles(Chem.MolFromSmiles(smi))`) before docking.\n\n6. **Verify purchasability via `catalogs`.** Treat `catalogs` entries with `purchase: 1` as orderable; record `supplier_code` and `catalog_name` for procurement.\n\n7. **Cache results.** ZINC data updates periodically; cache substance\u002Fsearch JSON with a date-stamped filename and avoid re-querying within a project.\n\n## Common Recipes\n\n### Recipe: Lookup ZINC22 ID and purchasability from a SMILES\n\n```python\nmatches = smiles_lookup(\"CC(=O)Nc1ccc(O)cc1\")   # acetaminophen\nfor m in matches:\n    cats = m.get(\"catalogs\", [])\n    print(f\"{m['zinc_id']} | MW {m['tranche_details']['mwt']:.1f} | \"\n          f\"catalogs {len(cats)} | purchasable {bool(cats)}\")\n```\n\n### Recipe: Batch-resolve a list of ZINC IDs\n\n```python\nzinc_ids = [\"ZINC000000029632\", \"ZINCbq0000001gLA\"]\nfor zid in zinc_ids:\n    try:\n        c = get_substance(zid)\n        print(f\"{zid}: {c['smiles']}  MW {c['tranche_details']['mwt']:.1f}\")\n    except requests.HTTPError as e:\n        print(f\"{zid}: not found ({e.response.status_code})\")\n```\n\n### Recipe: Property Distribution of a Library\n\n```python\nimport pandas as pd\nfrom rdkit import Chem\nfrom rdkit.Chem import Descriptors\n\ndf = pd.read_csv(\"docking_library.smi\", sep=\" \", names=[\"smiles\", \"zinc_id\"])\ndf[\"mw\"] = df[\"smiles\"].map(lambda s: Descriptors.MolWt(Chem.MolFromSmiles(s)))\ndf[\"logp\"] = df[\"smiles\"].map(lambda s: Descriptors.MolLogP(Chem.MolFromSmiles(s)))\nprint(f\"Library size: {len(df)}\")\nprint(df[[\"mw\", \"logp\"]].describe())\n```\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| Requests to `zinc15.docking.org` redirect to `\u002Fcaptcha\u002F` or return `403` | The legacy ZINC15 REST API is CAPTCHA-walled and unusable from scripts | Switch all calls to `cartblanche22.docking.org` (ZINC22 \u002F CartBlanche) as shown here |\n| `GET \u002Fsubstances\u002F{id}.json` (plural) returns HTML | Wrong path — that is the SPA catch-all | Use the **singular** `\u002Fsubstance\u002F{id}.json` |\n| Endpoint returns `200` with `content-type: application\u002Fjson` but body is HTML | CartBlanche serves the SPA shell for unknown routes (misleading content-type) | Check the body actually parses as JSON; only the documented endpoints return data |\n| `POST \u002Fsmiles.json` returns `{\"task\": ...}` but you expected results | SMILES search is asynchronous | Poll `GET \u002Fsearch\u002Fresult\u002F{task}` until `status == \"SUCCESS\"` |\n| `400 No Valid SMILES, please try again` | SMILES rejected (or wrong request form) | Validate the SMILES with RDKit; submit via multipart form fields `smiles`\u002F`dist`\u002F`adist`\u002F`database` |\n| `KeyError: 'data'` \u002F `'zinc22'` on a successful task | `result` is a dict when there are hits, but an empty list when there are none | Handle both shapes (see `smiles_lookup` helper) |\n| Analog search (`dist >= 1`) times out or returns only the exact match | Very large search space on a shared cluster | Use `dist=0` (exact); for similarity use the Smallworld web UI; expand analogs locally with RDKit |\n| `\u002Fsubstance\u002Frandom.json` returns `400`; `\u002Fsubstance\u002Frandom\u002F{n}.json` returns `{\"status\":\"PENDING\"}` | The random endpoint is async\u002Fnon-trivial | Not needed for scripting; pick known ZINC IDs instead |\n| `HTTP 404` for a compound ID | ID typo or not present in this build | Verify the ZINC ID; try `database=\"zinc20\"` for legacy IDs |\n\n## Related Skills\n\n- `rdkit-cheminformatics` — Compute properties and apply PAINS\u002FBrenk filters on retrieved ZINC compounds (required for property filtering here)\n- `autodock-vina-docking` — Use exported ZINC SMILES\u002FSDF files for molecular docking campaigns\n- `chembl-database-bioactivity` — Bioactivity data for compounds identified in ZINC virtual screens\n- `drugbank-database-access` — Approved-drug structures and annotations\n\n## References\n\n- [CartBlanche \u002F ZINC22 web app](https:\u002F\u002Fcartblanche22.docking.org\u002F) — Substance lookup, SMILES search, tranche browser\n- [ZINC22 paper](https:\u002F\u002Fdoi.org\u002F10.1021\u002Facs.jcim.2c01253) — Tingle et al., \"ZINC-22, A Free Multi-Billion-Scale Database of Tangible Compounds for Ligand Discovery,\" J. Chem. Inf. Model. 2023\n- [docking.org wiki](https:\u002F\u002Fwiki.docking.org\u002F) — CartBlanche usage and ZINC22 download documentation\n- [files.docking.org](https:\u002F\u002Ffiles.docking.org\u002F) — Bulk tranche file downloads for ZINC22\n",{"name":5,"license":15,"description":7},"master","fd76f882a04e445e899d5cfb2732f2105ea054ee","skills\u002Fawesome-skills\u002Fzinc-database",0,"https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS","https:\u002F\u002Fbg-szy.github.io\u002FTOP-SKILLS\u002F","https:\u002F\u002Fgithub.com\u002Fbg-szy","https:\u002F\u002Fwww.ecust.edu.cn\u002F","a student of East China University of Science and Technology","2026-11-15 03:38:02",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add bg-szy\u002FTOP-SKILLS --skill zinc-database",{"tool":40,"command":41},"bunx","bunx skills add bg-szy\u002FTOP-SKILLS --skill zinc-database",{"tool":43,"command":44},"pnpm","pnpm dlx skills add bg-szy\u002FTOP-SKILLS --skill zinc-database","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fzinc-database\u002FSKILL.md",true,[],[49,59,67,75,84,91],{"skillNo":50,"name":51,"nameZh":6,"description":52,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":54,"sourceHtmlUrl":55,"categories":56,"keywords":57,"publishedAt":58},"01M748668J70YKWKS2VFVZ0ZRF","xlsx","Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .xltx, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fxlsx\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fxlsx\u002FSKILL.md",[],[],"2026-11-12 13:55:43",{"skillNo":60,"name":61,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":63,"sourceHtmlUrl":64,"categories":65,"keywords":66,"publishedAt":58},"01M747X09GNGYJ7V4YE3NGGH7A","pptx","Use this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates (.potx), layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx or .potx filename, regardless of what they plan to do with the content afterward. If a .pptx or .potx file needs to be opened, created, or touched, use this skill.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fpptx\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fpptx\u002FSKILL.md",[],[],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":71,"sourceHtmlUrl":72,"categories":73,"keywords":74,"publishedAt":58},"01M747QR3VN12KXYDZGRXETTGS","docx","Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files) or Word templates (.dotx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', '.dotx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx or .dotx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fdocx\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fdocx\u002FSKILL.md",[],[],{"skillNo":76,"name":77,"nameZh":6,"description":78,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":79,"licenseFileUrl":80,"sourceHtmlUrl":81,"categories":82,"keywords":83,"publishedAt":58},"01M76KTXGSTXVVDC7YSBM41ZZS","wxpusher-integration","Use when（适用场景）：用户明确询问、指定或上下文已选定 WxPusher，需要了解、接入或排查通知（含微信 ClawBot\u002FiLink、appToken、UID、Topic、SPT）；仅说“微信通知”、未选服务商，或只出现通用 UID\u002FTopic 时不触发。","MIT","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Fwxpusher-integration\u002FLICENSE.md","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Fwxpusher-integration\u002FSKILL.md",[],[],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":79,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":19},"01M748C5DS5BQBVBMG22FCGG34","branding","When the user wants to define, audit, or apply brand strategy—purpose, values, positioning, storytelling, voice, narrative (not only visuals). Also use when the user mentions \"brand strategy\", \"brand story\", \"brand storytelling\" or \"brand voice\". For typography, colors, design tokens, and frontend visuals, use brand-visual-generator.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fsuperskills\u002Fbranding\u002FSKILL.md",[],[],{"skillNo":92,"name":93,"nameZh":6,"description":94,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":79,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":97,"publishedAt":19},"01M748C1GXJ9WRKP18X45DKP6G","brand-visual-generator","When the user wants to define, audit, or apply visual identity (typography, colors, spacing, design tokens, frontend aesthetics). Also use when the user mentions \"brand style guide\", \"visual identity\", \"design system\" or \"typography\". For brand story, positioning, and voice, use branding.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fsuperskills\u002Fbrand-visual-generator\u002FSKILL.md",[],[]]