Drive Free Tool Desk from your own code
The page does its free work in the browser: it reads what your company does and your notes on one free-tool idea
(labelled lines or prose), works out what a lead is worth (stated, or your deal size times the share of leads that
become customers), the leads a month (stated, or visitors times the visitor-to-lead rate), the build in money and
engineer-weeks, the monthly upkeep, and over 12 months the first year's lead value against the first year's cost, the
payback and the leads a month the first year needs, with flags FT-01..FT-12 (a first year that costs more than its leads
are worth, scraping or third-party APIs, features to skip at first, a build past 4 engineer-weeks, vanity metrics, no
gate). Its two paid lanes are available over HTTP. You send one JSON object with task set to
"score" or "plan", and the model replies with one JSON object. The easiest way to build the
facts string is the page itself:
ToolKit.buildInput(lane, ToolKit.analyze(you, tool, {company, goal, question, scores}), {scoreHandoff})
in toolkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"score" | The skill's evaluation: a verdict of build, build smaller or don't build (don't build under 15; build only at 25 or more with no FT-01 or FT-08), the tool_type, the goal, the eight-factor scorecard with quotes, the total and band, a reading of the page's math, the closest_case, the pitfalls quoted, the gating, the build_route, conditions and what to find_out. |
"plan" | The minimum viable tool: one_job, inputs, output, what to skip, lead_capture, the path_to_product, seo, distribution, the build route, 4-8 milestones ordered scope, build, launch, measure, three metrics and maintenance. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | score or plan |
company | required | Your company's name (may be empty; the page then reads it from the first line of you). |
goal | optional | What the tool is for: leads, seo, brand or education. |
you | required | What your company does: what you sell and who buys, the deal size, the share of leads that become customers or what a lead is worth, who could build. Past 5,000 characters the page cuts the middle on line boundaries and leaves a [free-tool-desk: ...] marker. |
tool | required | Your notes on the tool idea: what it does, the type, the keyword and its searches, the existing tools, the build cost and effort, the upkeep, the traffic and visitor-to-lead rate, the gate. Cut the same way past 6,000 characters. |
facts | required | A JSON string: company, goal, today, currency, tool{name, type, keyword, searches, visitors, conversion, leads_stated, competitors[], gating, build_cost, build_weeks, maintenance_month, launch_date, external, skip_features[], vanity}, you{product, acv, lead_to_customer, lead_value_stated}, lead_value, self_scores{}, self_total, math{leads_month, leads_basis, value_year, cost_year, ratio, payback_months, breakeven_leads_month}, flags[{id, severity, text}], notes[]. |
question | optional | What you want to know. |
score_handoff | plan only | A JSON string of the score the plan follows: verdict, tool_type, goal, total, gating, route, conditions[]. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, headline, questions[], summary.
task: "score"
verdict: build|build smaller|don't build, reason, tool_type: calculator|generator|analyzer|tester|library|interactive, goal: leads|seo|brand|education, scorecard[{factor, score, why, evidence[]}] (exactly eight, in order: search demand exists, audience match to buyers, uniqueness vs existing, natural path to product, build feasibility, maintenance burden (inverse), link-building potential, share-worthiness; each score a whole number 1-5), total (the sum), band: strong candidate|promising|reconsider, math_reading, closest_case{case, why} (Unsplash, HubSpot Website Grader, Baremetrics Forecasting, CoSchedule Headline Analyzer, Moz Keyword Explorer, Buffer Salary Calculator, Shopify Hatchful or none), pitfalls[{pitfall, evidence[]}] (over-engineering, poor product integration, maintenance and security debt, vanity metrics), gating{approach: fully gated|partially gated|ungated + optional|ungated entirely, why}, build_route{route: build custom|no-code|embed existing, why}, conditions[], find_out[].
task: "plan"
source (score handoff|from the input), tool_type, goal, one_job, inputs[] (2 to 6), output, skip[] (1 to 5), lead_capture{approach, value_exchange, fields[], qualifying_question} (email first), path_to_product, seo{landing_keyword, supporting[]}, distribution[], build{route, why}, milestones[{step: scope|build|launch|measure, when, owner, what}] (4 to 8, in step order), metrics[{metric, target}] (exactly three, in order: leads, qualification rate, trial or signup conversion), maintenance{owner, cadence, watch}.
Worked examples
These are the page's own bundled examples (invented companies, people and numbers), with the input as app.js builds it and the saved model reply the page replays for free. One worked example per verdict and per lane, and the score-to-plan handoff.
Score: a contractor vs employee cost calculator with 9,500 searches a month (build)
Input (description, notes and facts shortened here):
{
"task": "score",
"company": "Wagequill",
"you": "Wagequill - payroll and contractor payments for agencies and studios with 10-80 people.\nOur buyer is the founder or the operations lead. ACV is about $4,800.\nAbout 3% of our inbound leads become customers.\nLeads today come from paid search at roughly $95 each and from partner referrals.\nWe have one full-stack engineer and a designer who can spare time this quarter.\n",
"tool": "Contractor vs employee cost calculator\nType: calculator\nWhat it does: enter a salary or a day rate, the country and the hours, and see the full cost of a contractor against an employee, side by side.\nKeyword: \"contractor vs employee cost calculator\" - about 9,500 searches a month (Ahrefs, US + UK)\nExisting tools: two state-government pages, one accounting firm's spreadsheet\nNone of them compares day rates with salari... (789 characters in all)",
"facts": "(a JSON string; decoded: company Wagequill, goal leads, tool Contractor vs employee cost calculator (calculator), keyword contractor vs employee cost calculator at 9500 searches a month, build 8000 over 2 engineer-weeks, maintenance 150 a month, a lead worth 144, leads a month 150, first-year value 259200 against cost 9800, ratio 26.4, payback 0.4 months, break-even 6 leads a month; flags none)",
"goal": "leads",
"question": "Is this worth a two-week sprint?"
}
Reply:
{
"task": "score",
"verdict": "build",
"reason": "It scores 32 of 40 with no flags, the build is small, and the page's math shows the first year paying for itself many times over.",
"tool_type": "calculator",
"goal": "leads",
"scorecard": [
{
"factor": "search demand exists",
"score": 5,
"why": "The notes give a measured search volume for the exact keyword.",
"evidence": [
"about 9,500 searches a month (Ahrefs, US + UK)"
]
},
{
"factor": "audience match to buyers",
"score": 4,
"why": "Agencies weighing contractors against employees are the people who pay contractors and run payroll.",
"evidence": [
"payroll and contractor payments for agencies and studios with 10-80 people",
"Our buyer is the founder or the operations lead"
]
},
{
"factor": "uniqueness vs existing",
"score": 4,
"why": "Existing tools are government pages and a spreadsheet, and none compares day rates with salaries.",
"evidence": [
"None of them compares day rates with salaries"
]
},
{
"factor": "natural path to product",
"score": 4,
"why": "Someone costing a contractor against an employee is close to needing a way to pay them, and success is counted in trials.",
"evidence": [
"Success: leads and trials that started on the calculator"
]
},
{
"factor": "build feasibility",
"score": 5,
"why": "One engineer for two weeks is the size of the skill's winning cases.",
"evidence": [
"Build: one engineer, 2 weeks, about $8,000"
]
},
{
"factor": "maintenance burden (inverse)",
"score": 4,
"why": "Upkeep is small and known, though tax tables need updating twice a year.",
"evidence": [
"Maintenance: $150 a month (tax tables updated twice a year)"
]
},
{
"factor": "l
... (shortened here; the page shows it whole)
Plan: the same calculator, carrying the score
Input (with the score handed over):
{
"task": "plan",
"company": "Wagequill",
"you": "Wagequill - payroll and contractor payments for agencies and studios with 10-80 people.\nOur buyer is the founder or the operations lead. ACV is about $4,800.\nAbout 3% of our inbound leads become customers.\nLeads today come from paid search at roughly $95 each and from partner referrals.\nWe have one full-stack engineer and a designer who can spare time this quarter.\n",
"tool": "Contractor vs employee cost calculator\nType: calculator\nWhat it does: enter a salary or a day rate, the country and the hours, and see the full cost of a contractor against an employee, side by side.\nKeyword: \"contractor vs employee cost calculator\" - about 9,500 searches a month (Ahrefs, US + UK)\nExisting tools: two state-government pages, one accounting firm's spreadsheet\nNone of them compares day rates with salari... (789 characters in all)",
"facts": "(a JSON string; decoded: company Wagequill, goal leads, tool Contractor vs employee cost calculator (calculator), keyword contractor vs employee cost calculator at 9500 searches a month, build 8000 over 2 engineer-weeks, maintenance 150 a month, a lead worth 144, leads a month 150, first-year value 259200 against cost 9800, ratio 26.4, payback 0.4 months, break-even 6 leads a month; flags none)",
"goal": "leads",
"question": "Is this worth a two-week sprint?",
"score_handoff": "{\"verdict\":\"build\",\"tool_type\":\"calculator\",\"goal\":\"leads\",\"total\":32,\"gating\":\"partially gated\",\"route\":\"build custom\",\"conditions\":[]}"
}
Reply:
{
"task": "plan",
"source": "score handoff",
"tool_type": "calculator",
"goal": "leads",
"one_job": "Show the full cost of a contractor against an employee, side by side.",
"inputs": [
"salary or day rate",
"country",
"hours"
],
"output": "The full cost of a contractor and of an employee side by side, with a full PDF breakdown for an email.",
"skip": [
"account creation",
"saving results",
"advanced features",
"perfect design",
"every edge case"
],
"lead_capture": {
"approach": "partially gated",
"value_exchange": "Get the full PDF breakdown",
"fields": [
"email"
],
"qualifying_question": "How many people work at your agency or studio?"
},
"path_to_product": "The PDF breakdown shows how Wagequill runs payroll and contractor payments for agencies and studios and invites the reader to start a trial.",
"seo": {
"landing_keyword": "contractor vs employee cost calculator",
"supporting": [
"How to compare a day rate with a salary",
"What does an employee really cost compared with a contractor?"
]
},
"distribution": [
"Share it with partner referrals to pass on to their agency and studio clients",
"Launch it in communities where agency and studio founders and operations leads gather",
"Offer it as a linkable resource to accounting and payroll blogs, since none of the existing tools compares day rates with salaries"
],
"build": {
"route": "build custom",
"why": "One full-stack engineer can build it in 2 weeks for about $8,000, and the cost logic needs its own tax tables."
},
"milestones": [
{
"step": "scope",
"when": "this quarter",
"owner": "full-stack engineer",
"what": "Fix the inputs, the countries covered and the side-by-side result, leaving out accounts and saved results."
},
{
"st
... (shortened here; the page shows it whole)
Score: a $72,000 website grader with accounts and two paid APIs (build smaller)
Input:
{
"task": "score",
"company": "Crawlwren",
"you": "Crawlwren - a technical SEO agency for B2B software companies.\nWe sell a monthly retainer; the average client pays about $4,000 a month.\nA lead is worth about $120 to us (from last year's closed deals over all inbound leads).\nTwo developers and one designer could be moved onto an internal project.\n",
"tool": "Crawlwren Site Grader - a website grader for B2B SaaS marketing sites\nIt crawls up to 500 pages of any site, calls the PageSpeed Insights API and the Ahrefs API, and scores technical SEO, speed and content.\nUsers create an account to save their reports, see a dashboard of past scores and invite their team.\nExisting tools: HubSpot Website Grader, Seoptimer, Ubersuggest\nBuild: 3 developers for 8 weeks, about $72,000\nMa... (610 characters in all)",
"facts": "(a JSON string; decoded: company Crawlwren, goal leads, tool Crawlwren Site Grader (analyzer), keyword - at None searches a month, build 72000 over 24 engineer-weeks, maintenance 1800 a month, a lead worth 120, leads a month 40, first-year value 57600 against cost 93600, ratio 0.6, payback 24 months, break-even 65 leads a month; flags FT-01, FT-06, FT-07, FT-08, FT-09)",
"goal": "leads"
}
Reply:
{
"task": "score",
"verdict": "build smaller",
"reason": "The idea fits Crawlwren's buyers, but as scoped the first year's leads do not cover the build and running cost and the build is far past the skill's tiny-tool size.",
"tool_type": "analyzer",
"goal": "leads",
"scorecard": [
{
"factor": "search demand exists",
"score": 2,
"why": "The notes name no keyword and no search volume, only a visitor goal.",
"evidence": []
},
{
"factor": "audience match to buyers",
"score": 5,
"why": "The grader targets the same B2B software companies the agency sells to.",
"evidence": [
"a website grader for B2B SaaS marketing sites",
"a technical SEO agency for B2B software companies"
]
},
{
"factor": "uniqueness vs existing",
"score": 2,
"why": "Three well-known graders already do this and the notes do not say what this one does better.",
"evidence": [
"Existing tools: HubSpot Website Grader, Seoptimer, Ubersuggest"
]
},
{
"factor": "natural path to product",
"score": 4,
"why": "A technical SEO score leads straight to the agency's technical SEO retainer.",
"evidence": [
"scores technical SEO, speed and content"
]
},
{
"factor": "build feasibility",
"score": 2,
"why": "The build is 24 engineer-weeks, well past a tiny first version.",
"evidence": [
"Build: 3 developers for 8 weeks, about $72,000"
]
},
{
"factor": "maintenance burden (inverse)",
"score": 2,
"why": "A crawler and two third-party APIs carry ongoing fees, hosting and fixes.",
"evidence": [
"Maintenance: $1,800 a month (API fees, crawler hosting, fixes)"
]
},
{
"factor": "link-building potential",
"score": 3,
"why": "The notes say nothing about links, and the grader space is al
... (shortened here; the page shows it whole)
Score: dental software and a crypto tax estimator (don't build)
Input:
{
"task": "score",
"company": "Molarly",
"you": "Molarly - scheduling and billing software for independent dental practices.\nOur buyer is the practice owner or the office manager.\nOur marketing site gets most of its leads from dental conferences and referrals.\n",
"tool": "Crypto tax estimator\nCrypto is trending and a calculator like this could go viral on social media.\nVisitors connect a wallet and the tool pulls live data from exchange APIs to estimate capital gains tax for the year.\nExisting tools: Koinly, CoinTracker and TurboTax all have free crypto tax calculators\nBuild: one contractor, 6 weeks, about $18,000\nWe would put our logo on it and a link to Molarly at the bottom.\n",
"facts": "(a JSON string; decoded: company Molarly, goal brand, tool Crypto tax estimator (calculator), keyword - at None searches a month, build 18000 over 6 engineer-weeks, maintenance None a month, a lead worth None, leads a month None, first-year value None against cost 18000, ratio None, payback None months, break-even None leads a month; flags FT-02, FT-04, FT-05, FT-06, FT-08, FT-10)",
"goal": "brand",
"question": "Could this go viral and bring us dentists?"
}
Reply:
{
"task": "score",
"verdict": "don't build",
"reason": "It scores 12 of 40: a crypto tax tool is not adjacent to dental practice software, three free versions already exist, and it depends on live exchange APIs.",
"tool_type": "calculator",
"goal": "brand",
"scorecard": [
{
"factor": "search demand exists",
"score": 2,
"why": "The notes give no keyword and no search volume, only that crypto is trending.",
"evidence": [
"Crypto is trending"
]
},
{
"factor": "audience match to buyers",
"score": 1,
"why": "Crypto tax users are not the dental practice owners and office managers who buy Molarly.",
"evidence": [
"Our buyer is the practice owner or the office manager"
]
},
{
"factor": "uniqueness vs existing",
"score": 1,
"why": "Three well-known products already give this away free.",
"evidence": [
"Koinly, CoinTracker and TurboTax all have free crypto tax calculators"
]
},
{
"factor": "natural path to product",
"score": 1,
"why": "A logo and a footer link are the only connection to scheduling and billing for dental practices.",
"evidence": [
"We would put our logo on it and a link to Molarly at the bottom"
]
},
{
"factor": "build feasibility",
"score": 2,
"why": "Six weeks of a contractor plus wallet connections is past the skill's small builds, and the page flags it.",
"evidence": [
"Build: one contractor, 6 weeks, about $18,000"
]
},
{
"factor": "maintenance burden (inverse)",
"score": 1,
"why": "Live exchange data and tax rules need constant upkeep, and no maintenance is stated.",
"evidence": [
"pulls live data from exchange APIs"
]
},
{
"factor": "link-building potential",
"score": 2,
"why": "The notes say nothing abou
... (shortened here; the page shows it whole)
Plan: a free SSL certificate checker, from the input
Input:
{
"task": "plan",
"company": "Stackcrate",
"you": "Stackcrate - uptime and certificate monitoring for small engineering teams.\nOur buyer is the engineering lead or the solo founder who gets paged.\nACV: $1,200. Lead-to-customer: 5%.\nMost signups come from a free plan; developers find us through our blog.\n",
"tool": "SSL certificate checker\nType: tester\nWhat it does: enter a domain and see the certificate's issuer, expiry date, chain and common misconfigurations, in under two seconds.\nKeyword: \"ssl checker\" - 40,000 searches a month\nExisting tools: SSL Labs, DigiCert's checker\nBuild: one engineer, 1 week, $3,500\nMaintenance: $80 a month\nGating: ungated + optional - email to get an alert 14 days before the certificate expires\nLaun... (506 characters in all)",
"facts": "(a JSON string; decoded: company Stackcrate, goal leads, tool SSL certificate checker (tester), keyword ssl checker at 40000 searches a month, build 3500 over 1 engineer-weeks, maintenance 80 a month, a lead worth 60, leads a month 200, first-year value 144000 against cost 4460, ratio 32.3, payback 0.3 months, break-even 7 leads a month; flags none)",
"goal": "leads"
}
Reply:
{
"task": "plan",
"source": "from the input",
"tool_type": "tester",
"goal": "leads",
"one_job": "Check a domain's SSL certificate and show its issuer, expiry date, chain and common misconfigurations.",
"inputs": [
"domain",
"email for the expiry alert (optional)"
],
"output": "The certificate's issuer, expiry date, chain and common misconfigurations, in under two seconds.",
"skip": [
"account creation",
"saving results or check history",
"advanced features beyond the one check",
"perfect design",
"every edge case"
],
"lead_capture": {
"approach": "ungated + optional",
"value_exchange": "Get an alert 14 days before this certificate expires",
"fields": [
"email"
],
"qualifying_question": "How many domains does your team look after?"
},
"path_to_product": "The expiry alert is a free taste of Stackcrate's certificate monitoring, so each alert email invites the user onto the free plan to monitor uptime and certificates.",
"seo": {
"landing_keyword": "ssl checker",
"supporting": [
"How to check when an SSL certificate expires",
"What is an SSL certificate chain?",
"How to fix common SSL misconfigurations"
]
},
"distribution": [
"Launch it on the Stackcrate blog, where developers already find you",
"Link it from blog posts on certificates and uptime",
"Share it in developer communities with an example check",
"Pitch it to [resource pages] that list SSL tools"
],
"build": {
"route": "build custom",
"why": "The notes scope it to one engineer and one week, and certificate checks are close to what Stackcrate already does."
},
"milestones": [
{
"step": "scope",
"when": "[date]",
"owner": "Ines Varrowby (growth engineer)",
"what": "Fix the one check, the result screen and the optional alert, and
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): for a score, the eight factors in the skill's order, each a whole number from 1 to 5, a 4 or 5 only with a quote, the caps for an oversized build (FT-08) and for scraping or third-party APIs (FT-06), the total as the sum and the band that follows it, don't build under 15, build only at 25 or more with neither FT-01 nor FT-08, conditions for build smaller, the goal you chose, and only the skill's cases, pitfalls (quoted), gates and routes. For a plan, the goal you chose, one job, 2 to 6 inputs, 1 to 5 things to skip, a gate with email first, a path to the product, your keyword, 4 to 8 milestones ordered scope, build, launch, measure, and exactly leads, qualification rate and trial or signup conversion. In both, quotes are checked word for word and no cost, count, rate, date or duration may be invented. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same input into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://free-tool-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://free-tool-desk.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// free-tool-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://free-tool-desk.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://free-tool-desk.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// FreeToolDesk.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class FreeToolDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://free-tool-desk.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://free-tool-desk.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://free-tool-desk.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://free-tool-desk.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"free-tool-desk"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "free-tool-desk"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "free-tool-desk" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "free-tool-desk"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "free-tool-desk"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "free-tool-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "free-tool-desk"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "free-tool-desk" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "score", "company": "...", "goal": "leads", "you": "...", "tool": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "score", "company": "...", "goal": "leads", "you": "...", "tool": "...", "facts": "...", ...}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, company, goal, you, tool, question, facts, ...}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses free-tool-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="free-tool-desk:score:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "free-tool-desk:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `free-tool-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "free-tool-desk:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "free-tool-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "free-tool-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "free-tool-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"free-tool-desk:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream (plan)
The same run as Server-Sent Events. plan.json is a plan-lane input, with score_handoff when it follows a score.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: free-tool-desk:plan:$(shasum -a 256 plan.json | cut -c1-16):a1" --data @plan.json
rev = json.load(open("plan.json")) # {"task": "plan", "company": "...", "goal": "...", "you": "...", "tool": "...", "facts": "...", "score_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "free-tool-desk:plan:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("plan.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `free-tool-desk:plan:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("plan.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "free-tool-desk:plan:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("plan.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "free-tool-desk:plan:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("plan.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "free-tool-desk:plan:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("plan.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: free-tool-desk:plan:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("plan.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"free-tool-desk:plan:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);