Drive MedChem Desk from your own code
MedChem Desk takes a screening hit list — identifiers, SMILES and assay values — and returns one of four medicinal-chemistry reports over it. Everything below is the same HTTP surface the page itself uses, so anything the app can do, a script can do.
Base URL: https://api.skillsafe.ai/v1/app-api. App slug: medchem-desk.
Every response is either {"ok":true,"data":{…}} or
{"ok":false,"error":{"code":"…","message":"…"}}.
Two things that will cost you an afternoon if you skip them
-
The run body is the input object itself. There is no
inputwrapper key. A wrapped body still returns200with a plausible-looking hold, because the platform accepts any JSON object — but the model then never seestaskand reviews an empty hit list at full price. If you are unsure whether your payload is reaching the prompt, send it to/estimatetwice: once complete, once with only{"task":"triage"}. The hold must drop materially. If the two holds are the same, your fields are not being priced and they are not being read. -
There is no app-slug header. The slug travels in the
POST /guesttoken exchange and nowhere else; the request headers areContent-TypeandAuthorizationonly. A bogus slug header is ignored and still returns200, so do not use one as a check that you are talking to the right app.
The lanes
task is the first field of the payload and the router of the whole prompt. Send exactly
one of these:
| task | stage | what comes back in body |
|---|---|---|
series | inspect | clusters, singletons, sar, property_landscape, outliers, data_quality |
triage | decide | calls, ranking, rank_basis, kill_reasons, hold_unblockers, progress_set_note |
admet | verify | risks, panel, insilico, sequence |
design | produce | proposals, design_rules, do_not, round_shape |
The input fields
Taken from readForm() in app.js — this is what the page actually sends.
| field | type | meaning |
|---|---|---|
task | string, required | series, triage, admet or design. |
compounds | string, required | A tab-separated table. One leading # comment line saying how many compounds were sent out of how many were scanned, then a header line beginning row, then one line per compound. Columns: row id smiles formula mw hac rings ar_rings rotb hbd hba tpsa fsp3 charge potency pActivity LE logD selectivity solubility liabilities call_hint. The page builds this in the browser from the user's paste; a script may build it directly. |
target_class | string | kinase, gpcr, protease, ion-channel, nuclear-receptor, ppi, phenotypic, other. |
stage | string | hit-finding, hit-to-lead, lead-optimisation, candidate-selection. |
route | string | oral, cns-oral, iv, inhaled, topical. |
modality | string | reversible or covalent. In a covalent programme a warhead is graded as an intended feature rather than a liability. |
focus_ids | string | Whitespace- or comma-separated compound identifiers. Confines compound-level output to those. |
context | string | Free-text project context. This is what makes the report yours rather than a lecture. |
carry | string | Conclusions from a previous lane, handed forward. The page fills it from a handoff button. |
prescan_facts | object | {counts, posture_hint, flags[], flag_count, sampling}. Each flag is {id, severity, label, compound_ids, detail}, and the model must reconcile every id exactly once in coverage_check. flag_count is the array's own length — never a separate tally. |
retry_note | string | Only on a reformat retry. Tells the model its previous reply did not parse. |
The output envelope
Identical across all four lanes; only body differs. This is the shape
normalize() in app.js parses, so a field it requires is a field a client
should require too.
{
"lane": "triage",
"report_name": "…",
"series_name": "…",
"posture": "advance | iterate | deprioritise",
"verdict": "one sentence naming the deciding fact",
"compound_count": 22,
"exec_summary": "…",
"assumptions": ["…"],
"open_questions": ["…"],
"findings": [{
"id": "MD-001",
"severity": "critical | high | medium | low | info",
"family": "reactive | genotoxic | metabolic | interference | chelator | property | data",
"title": "…", "compound_ids": ["KIN-003"],
"evidence": "…", "why_it_matters": "…", "action": "…"
}],
"coverage_check": [{"id": "alert-genotoxic", "addressed": true, "note": "…"}],
"body": { },
"summary": "…"
}
Error codes
| code | HTTP | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Mint a new one (step 2). |
forbidden | 403 | The token belongs to a different app. Tokens are per-slug. |
payment_required | 402 | The balance cannot cover the hold. Call /estimate first and compare against /me; a 402 after submit is a client-side omission, not a user error. |
validation_error | 400 | The body was not a JSON object, or a field was the wrong type. Check that you did not wrap the payload in an input key. |
rate_limited | 429 | Back off and retry. Never tight-loop. |
not_found | 404 | Wrong job id, or a collection the release does not declare. |
internal | 500 | Retry once with the SAME idempotency key. A retry with a new key is a second billable run. |
1. Get a token
The friendly route is /tokens.html: it reads the token this browser
already holds for medchem-desk, shows whether it is a guest or a personal token, and copies
it or a ready-made shell export. No developer console needed.
A guest token is enough for /me and /estimate — the whole
pricing path is free. Running a lane is metered, so it needs a personal token,
which comes from signing in.
2. Mint a guest token from code
This is the only call that names the app slug. Everything after it is bearer-token only.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug":"medchem-desk"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
guest = call("/guest", {"slug":"medchem-desk"})
print(guest)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const guest = await call("/guest", {"slug":"medchem-desk"});
console.log(guest);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"slug":"medchem-desk"}`
out, err := call("/guest", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"slug":"medchem-desk"}""";
System.out.println(call("/guest", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
guest = call("/guest", JSON.parse("{\"slug\":\"medchem-desk\"}"))
puts guest
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"slug\":\"medchem-desk\"}", true);
print_r(call("/guest", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""slug"":""medchem-desk""}";
Console.WriteLine(await Call("/guest", body));
Answers 201 with {"token":"aut_…","guest_id":"…","expires_at":"…"} — note
there is no subject_type in this response; that field comes from /me.
Store the token: a fresh POST /guest mints a NEW subject, and any per-user record
written under the old one stays with the old one.
There is no user_id and no is_guest field anywhere in this API.
/me returns {"subject_type","subject_id","credits"} and
subject_type — "guest" or "user" — is the only correct thing
to branch on.
3. Check the session and the balance
curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $TOKEN"
import json, urllib.request
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": "Bearer " + TOKEN})
with urllib.request.urlopen(req) as r:
me = json.load(r)["data"]
print(me["credits"], me["subject_type"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
headers: { "Authorization": "Bearer " + TOKEN }
});
const me = (await r.json()).data;
console.log(me.credits, me.subject_type);
req, _ := http.NewRequest("GET", base+"/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/me"))
.header("Authorization", "Bearer " + TOKEN)
.GET().build();
System.out.println(HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body());
uri = URI(BASE + "/me")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer " + TOKEN)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$ch = curl_init($base . "/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token],
]);
$me = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
echo $me["credits"], " ", $me["subject_type"];
var res = await http.GetAsync(Base + "/me");
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var me = doc.RootElement.GetProperty("data");
Console.WriteLine(me.GetProperty("credits"));
subject_type is guest or user; credits is the
balance in credits (10,000 credits = $1.00). Compare it against the hold from step 4 before
you run, and you will never see a 402.
4. Price the run for free
/estimate creates no job, spends nothing, and returns the model binding as well as the
price. It is also the fastest way to confirm your payload is real: hold_credits must
drop materially when you strip the hit list out.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
est = call("/estimate", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(est)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const est = await call("/estimate", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(est);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/estimate", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/estimate", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
est = call("/estimate", JSON.parse("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts est
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/estimate", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""triage"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":"""",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/estimate", body));
Returns model (gpt-5.6-terra), model_alias
(gpt-terra), markup_bps (1000), hold_credits,
min_credits and sponsor_enabled. Show the hold as reserved, never
as the price — the actual charge is usually far lower, because the hold prices the full output cap.
Re-estimate on every lane switch: the four lanes have different output caps and therefore different
holds.
5. Run a lane and poll the job
Send an Idempotency-Key header. A content hash of the payload plus the
lane plus an attempt counter. Without one, a network blip on the response leg bills you twice for
one review. With one, the retry is free.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: triage-9f2c1a-a1" \
-d '{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": "triage-9f2c1a-a1"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
job = call("/run", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(job)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json",
"Idempotency-Key": "triage-9f2c1a-a1" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const job = await call("/run", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(job);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "triage-9f2c1a-a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/run", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "triage-9f2c1a-a1")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/run", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json",
"Idempotency-Key" => "triage-9f2c1a-a1")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
job = call("/run", JSON.parse("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts job
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json", "Idempotency-Key: triage-9f2c1a-a1"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
content.Headers.Add("Idempotency-Key", "triage-9f2c1a-a1");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""triage"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":"""",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/run", body));
Returns {"job_id":"job_…","status":"queued"}. Poll
GET /jobs/{job_id} until status is succeeded,
failed or cancelled. On success the report text is at
data.output.output, the actual charge at data.charged_credits, and
data.truncated is true when the balance forced a reduced output cap — in
which case render what arrived and say it was cut short rather than presenting a clipped report as
complete.
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/job_abc123" -H "Authorization: Bearer $TOKEN"
import json, urllib.request
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/jobs/job_abc123",
headers={"Authorization": "Bearer " + TOKEN})
with urllib.request.urlopen(req) as r:
me = json.load(r)["data"]
print(me["credits"], me["subject_type"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/jobs/job_abc123", {
headers: { "Authorization": "Bearer " + TOKEN }
});
const me = (await r.json()).data;
console.log(me.credits, me.subject_type);
req, _ := http.NewRequest("GET", base+"/jobs/job_abc123", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/jobs/job_abc123"))
.header("Authorization", "Bearer " + TOKEN)
.GET().build();
System.out.println(HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body());
uri = URI(BASE + "/jobs/job_abc123")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer " + TOKEN)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$ch = curl_init($base . "/jobs/job_abc123");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token],
]);
$me = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
echo $me["credits"], " ", $me["subject_type"];
var res = await http.GetAsync(Base + "/jobs/job_abc123");
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var me = doc.RootElement.GetProperty("data");
Console.WriteLine(me.GetProperty("credits"));
6. Or stream it
POST /run-stream is the same body and the same headers, with
Accept: text/event-stream. Deltas arrive as SSE data: lines; the terminal
event carries the job. This is what the page uses, which is why the progress card can advance on
real section headings arriving rather than on a timer.
curl -sN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: triage-9f2c1a-a1" \
-d '{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/run-stream",
data=json.dumps(BODY).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": "triage-9f2c1a-a1"},
method="POST")
raw = ""
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if not line.startswith("data: "):
continue
ev = json.loads(line[6:])
if ev.get("delta"):
raw += ev["delta"]
if ev.get("job"):
print("terminal:", ev["job"]["status"])
report = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])
print(report["posture"], len(report["findings"]), "findings")
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": "triage-9f2c1a-a1"
},
body: JSON.stringify(BODY)
});
const reader = r.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const ev = JSON.parse(line.slice(6));
if (ev.delta) raw += ev.delta;
if (ev.job) console.log("terminal:", ev.job.status);
}
}
const report = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(report.posture, report.findings.length, "findings");
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", "triage-9f2c1a-a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var raw strings.Builder
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
var ev struct {
Delta string `json:"delta"`
Job json.RawMessage `json:"job"`
}
json.Unmarshal([]byte(line[6:]), &ev)
raw.WriteString(ev.Delta)
}
fmt.Println(raw.Len(), "characters of report")
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", "triage-9f2c1a-a1")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
StringBuilder raw = new StringBuilder();
HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data: "))
.forEach(l -> raw.append(l.substring(6)));
System.out.println(raw.length() + " characters of stream");
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json",
"Accept" => "text/event-stream",
"Idempotency-Key" => "triage-9f2c1a-a1")
req.body = JSON.dump(BODY)
raw = ""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data: ")
ev = JSON.parse(line[6..].strip) rescue next
raw << ev["delta"].to_s
end
end
end
end
puts JSON.parse(raw[raw.index("{")..raw.rindex("}")])["posture"]
<?php
$ch = curl_init($base . "/run-stream");
$raw = "";
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: triage-9f2c1a-a1",
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw) {
foreach (explode("\n", $chunk) as $line) {
if (strpos($line, "data: ") !== 0) continue;
$ev = json_decode(substr($line, 6), true);
if (isset($ev["delta"])) $raw .= $ev["delta"];
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$report = json_decode(substr($raw, strpos($raw, "{"), strrpos($raw, "}") - strpos($raw, "{") + 1), true);
echo $report["posture"];
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", "triage-9f2c1a-a1");
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
while (await reader.ReadLineAsync() is string line) {
if (!line.StartsWith("data: ")) continue;
var ev = JsonDocument.Parse(line[6..]).RootElement;
if (ev.TryGetProperty("delta", out var d)) raw.Append(d.GetString());
}
Console.WriteLine(raw.Length + " characters of report");
7. One worked example per lane
The same three-compound table through all four lanes. Only task changes — and, on the
later lanes, carry, which is how the page's handoff buttons pass one lane's conclusions
into the next.
Read the series — task: "series"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"series","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
job = call("/run", {"task":"series","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(job)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const job = await call("/run", {"task":"series","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(job);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"series","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/run", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"series","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/run", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
job = call("/run", JSON.parse("{\"task\":\"series\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts job
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"series\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""series"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":"""",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/run", body));
body.clusters groups by scaffold; each cluster's best_id is one of its own
compound_ids. body.sar carries a confidence of
supported, suggestive or unsupported, and anything resting on
fewer than three compounds comes back unsupported by contract — three points is where a
trend starts, and the prompt refuses to pretend otherwise.
Call every compound — task: "triage"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
job = call("/run", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(job)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const job = await call("/run", {"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(job);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/run", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"triage","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/run", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
job = call("/run", JSON.parse("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts job
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"triage\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""triage"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":"""",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/run", body));
body.calls holds exactly one entry per compound row, each with
call in progress / hold / kill, the
deciding_liability, and agrees_with_prescan — a boolean the page verifies
against the row's own call_hint column and flags when the claim is wrong. Disagreeing
with the scanner is legitimate and often right; claiming agreement that is not there is a defect.
Profile and panel — task: "admet"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"admet","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
job = call("/run", {"task":"admet","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(job)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const job = await call("/run", {"task":"admet","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(job);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"admet","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/run", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"admet","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/run", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
job = call("/run", JSON.parse("{\"task\":\"admet\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts job
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"admet\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro).\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""admet"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":""From the Compound triage on the CDK12 anilinopyrimidine series: posture iterate. progress: KIN-004. hold: KIN-013. kill: KIN-003 (aromatic nitro)."",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/run", body));
body.panel is a work order, not a wish list: tier-1 is what runs on
everything next week and tier-3 is the one-off on the compound you believe in. Every
gate is falsifiable, with a number wherever a number is honest.
body.insilico names the public ADME and toxicity collections by what they contain and
always carries the applicability-domain caveat — a model trained on marketed drugs says
very little about a covalent fragment.
Design the next round — task: "design"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"design","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body):
req = urllib.request.Request(
BASE + path,
data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"):
raise RuntimeError(out["error"]["code"] + ": " + out["error"]["message"])
return out["data"]
job = call("/run", {"task":"design","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}})
print(job)
const TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const r = await fetch(BASE + path, {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const out = await r.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const job = await call("/run", {"task":"design","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}});
console.log(job);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
const base = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body string) ([]byte, error) {
req, _ := http.NewRequest("POST", base+path, bytes.NewBufferString(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
body := `{"task":"design","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}`
out, err := call("/run", body)
if err != nil {
panic(err)
}
var parsed map[string]any
json.Unmarshal(out, &parsed)
fmt.Println(parsed)
}
import java.net.URI;
import java.net.http.*;
public class MedChemDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
String body = """
{"task":"design","compounds":"# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold","target_class":"kinase","stage":"hit-to-lead","route":"oral","modality":"reversible","focus_ids":"","context":"CDK12 programme, once-daily oral, we will not go past 450 daltons.","carry":"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.","prescan_facts":{"counts":{"rows":3,"parsed":3},"posture_hint":"iterate","flags":[{"id":"alert-genotoxic","severity":"critical","label":"2 compounds carry a genotoxic liability","compound_ids":["KIN-003","KIN-013"],"detail":"aromatic nitro group (1), unsubstituted aromatic amine (1)"},{"id":"no-potency","severity":"medium","label":"1 compound has no potency reading","compound_ids":["KIN-013"],"detail":"cannot be ranked on activity and must not be assumed inactive"}],"flag_count":2,"sampling":{"sent":3,"total":3,"dropped":0,"reason":""}}}""";
System.out.println(call("/run", body));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer " + TOKEN,
"Content-Type" => "application/json")
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
out = JSON.parse(res.body)
raise "#{out["error"]["code"]}: #{out["error"]["message"]}" unless out["ok"]
out["data"]
end
job = call("/run", JSON.parse("{\"task\":\"design\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}"))
puts job
<?php
$token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
$base = "https://api.skillsafe.ai/v1/app-api";
function call($path, $body) {
global $token, $base;
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($out["ok"])) throw new Exception($out["error"]["code"] . ": " . $out["error"]["message"]);
return $out["data"];
}
$body = json_decode("{\"task\":\"design\",\"compounds\":\"# 3 of 3 compounds. The whole list was sent.\\nrow\\tid\\tsmiles\\tformula\\tmw\\thac\\trings\\tar_rings\\trotb\\thbd\\thba\\ttpsa\\tfsp3\\tcharge\\tpotency\\tpActivity\\tLE\\tlogD\\tselectivity\\tsolubility\\tliabilities\\tcall_hint\\n2\\tKIN-004\\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\\tC20H22N6\\t346.44\\t26\\t4\\t3\\t3\\t1\\t6\\t56.4\\t0.3\\t0\\t7.5 nM\\t8.12\\t0.428\\t2\\t63\\t145uM\\tnone\\tprogress\\n3\\tKIN-003\\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\\tC20H20N6O3\\t392.42\\t29\\t4\\t3\\t4\\t1\\t9\\t102.19\\t0.2\\t0\\t>10000 nM\\t5\\t\\t3.1\\t2\\t15uM\\taromatic-nitro:critical panel-lead:medium\\tkill\\n4\\tKIN-013\\tNc1ncnc2c1c(-c1ccccc1)nn2C\\tC12H11N5\\t225.25\\t17\\t3\\t3\\t1\\t2\\t5\\t69.2\\t0.083\\t0\\tmissing\\t\\t\\t\\t\\t\\taromatic-amine:high\\thold\",\"target_class\":\"kinase\",\"stage\":\"hit-to-lead\",\"route\":\"oral\",\"modality\":\"reversible\",\"focus_ids\":\"\",\"context\":\"CDK12 programme, once-daily oral, we will not go past 450 daltons.\",\"carry\":\"From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15.\",\"prescan_facts\":{\"counts\":{\"rows\":3,\"parsed\":3},\"posture_hint\":\"iterate\",\"flags\":[{\"id\":\"alert-genotoxic\",\"severity\":\"critical\",\"label\":\"2 compounds carry a genotoxic liability\",\"compound_ids\":[\"KIN-003\",\"KIN-013\"],\"detail\":\"aromatic nitro group (1), unsubstituted aromatic amine (1)\"},{\"id\":\"no-potency\",\"severity\":\"medium\",\"label\":\"1 compound has no potency reading\",\"compound_ids\":[\"KIN-013\"],\"detail\":\"cannot be ranked on activity and must not be assumed inactive\"}],\"flag_count\":2,\"sampling\":{\"sent\":3,\"total\":3,\"dropped\":0,\"reason\":\"\"}}}", true);
print_r(call("/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, string body) {
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + path, content);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
var body = @"{""task"":""design"",""compounds"":""# 3 of 3 compounds. The whole list was sent.\nrow\tid\tsmiles\tformula\tmw\thac\trings\tar_rings\trotb\thbd\thba\ttpsa\tfsp3\tcharge\tpotency\tpActivity\tLE\tlogD\tselectivity\tsolubility\tliabilities\tcall_hint\n2\tKIN-004\tCN1CCN(c2ccc(Nc3nccc(-c4cccnc4)n3)cc2)CC1\tC20H22N6\t346.44\t26\t4\t3\t3\t1\t6\t56.4\t0.3\t0\t7.5 nM\t8.12\t0.428\t2\t63\t145uM\tnone\tprogress\n3\tKIN-003\tO=[N+]([O-])c1ccc(Nc2nccc(-c3cccnc3)n2)cc1CN1CCOCC1\tC20H20N6O3\t392.42\t29\t4\t3\t4\t1\t9\t102.19\t0.2\t0\t>10000 nM\t5\t\t3.1\t2\t15uM\taromatic-nitro:critical panel-lead:medium\tkill\n4\tKIN-013\tNc1ncnc2c1c(-c1ccccc1)nn2C\tC12H11N5\t225.25\t17\t3\t3\t1\t2\t5\t69.2\t0.083\t0\tmissing\t\t\t\t\t\taromatic-amine:high\thold"",""target_class"":""kinase"",""stage"":""hit-to-lead"",""route"":""oral"",""modality"":""reversible"",""focus_ids"":"""",""context"":""CDK12 programme, once-daily oral, we will not go past 450 daltons."",""carry"":""From the ADMET and panel lane: aromatic nitro on KIN-003 is a critical genotoxicity alert; KIN-013 has no potency reading; the series is flat with Fsp3 below 0.15."",""prescan_facts"":{""counts"":{""rows"":3,""parsed"":3},""posture_hint"":""iterate"",""flags"":[{""id"":""alert-genotoxic"",""severity"":""critical"",""label"":""2 compounds carry a genotoxic liability"",""compound_ids"":[""KIN-003"",""KIN-013""],""detail"":""aromatic nitro group (1), unsubstituted aromatic amine (1)""},{""id"":""no-potency"",""severity"":""medium"",""label"":""1 compound has no potency reading"",""compound_ids"":[""KIN-013""],""detail"":""cannot be ranked on activity and must not be assumed inactive""}],""flag_count"":2,""sampling"":{""sent"":3,""total"":3,""dropped"":0,""reason"":""""}}}";
Console.WriteLine(await Call("/run", body));
body.proposals holds four to twelve entries, each naming a real
parent_compound_id from the table and testing exactly one
hypothesis — a compound that changes three things at once teaches nothing.
body.do_not is the plausible-but-wrong move this dataset rules out, and it is the field
most worth reading twice.
8. What the report contract will not let you skip
-
Reconcile the flags. Every
prescan_facts.flags[].idyou send must come back exactly once incoverage_check. A flag the report never mentions is an omission the page displays; a flag it set aside with a reason is a legitimate answer and often the interesting one. -
Check the identifiers. Every id in
findings[].compound_idsand anywhere inbodymust exist in the table you sent. The page audits this and marks invented identifiers as unfounded, so a client should too. -
Honour the invariants. A
criticalfinding under anadvanceposture, adeprioritisewith nothing critical or high, duplicateMD-nnnids, or anadvancewith no compound calledprogressare all self-contradictions. -
Retry with the same key, not a new one. When a reply does not parse, resend with
retry_noteset and the attempt counter incremented so the new body is not a replay of the old key. Repairing truncated JSON by appending closing braces produces something that parses and is not what the model meant. -
Do not invent a logP. The prompt is explicit that no lipophilicity is predicted
anywhere in this app. If your
logDcolumn is empty, the lipophilicity-dependent limbs stayunassessable— filling them in downstream reintroduces exactly the fabricated number the design avoids.
Rate limits and quotas
/estimate, /me and /guest are free. Data endpoints share
120 requests per minute per IP; vector similarity is tighter at 30 per minute. Records cap at
64 KB per document, which is why long reports are stored with their lane detail trimmed rather than
refused.