Drive Co-Marketing Fit from your own code
The page does its free work in the browser: it reads what your company does and a shortlist of possible
co-marketing partners (CSV, TSV or a Markdown table), maps the columns, and scores what it can measure on its own 1-5
scale - each partner's audience size, engagement, the reciprocity between the two audiences and the ease of
execution - with flags CM-01..CM-10 (a competitor, audiences more than 10 times apart, missing sizes and engagement,
no relationship signal, duplicates). 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:
PartnerKit.buildInput(lane, PartnerKit.analyze(you, partners, {company, goal, question}), {partner, scoreHandoff})
in partnerkit.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 partner scorecard: for each partner the page sent, the co-marketing skill's six criteria 1-5 (audience_fit and brand_alignment judged with quotes; audience_size, engagement, reciprocity and ease exactly as the page computed them, or null), a partnership type, a verdict of pursue, maybe or pass, 1-2 campaign formats, risks and what to find out; then a ranking, a top_pick and a next_step. |
"plan" | A joint campaign with one partner: the campaign (a format from the skill's list, its category, a working title, a pitch, what customers get from both, the shared moment), alternatives, the assets each side brings, the eight-item agreement, a timeline, metrics, the outreach email and the call_prep. |
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 description's first words). |
you | required | What your company does: the product, the buyer, audience numbers, integrations, past partnerships, the goal and the timeline. Past 5,000 characters the page cuts the middle on line boundaries and leaves a [comarketing-fit: ...] marker. |
partners | required | The shortlist's header row and the rows of the partners in this run: up to 8 for score, the chosen one for plan, within 6,000 characters, cut on whole rows. |
facts | required | A JSON string: company, you_size, you_size_basis, icp_stated, icp_quote, you_history, columns, mapped, partners[] (name, row, does, audience, size, engagement, relation, competes, team, history, notes, ratio, computed{audience_size, engagement, reciprocity, ease}, flags[]), flags[], goal, notes. |
goal | optional | The campaign category you prefer: any, content, events, product or community. |
partner | plan only | The partner to plan with, spelled as the shortlist spells it. |
question | optional | What you want to know. |
score_handoff | plan only | A JSON string of that partner's scorecard entry: partner, verdict, type, scores{}, campaigns[], risks. |
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"
partners[{name, type: co-marketing|integrations|reseller|affiliate|marketplace, scores{audience_fit, audience_size, brand_alignment, engagement, reciprocity, ease: {score: 1-5|null, why, evidence[]}}, verdict: pursue|maybe|pass, reason, campaigns[], risks, find_out}] (one per partner sent, in order), ranking[], top_pick, next_step.
task: "plan"
source (score handoff|from the input), partner, campaign{format, category: content|events|product|community, title, pitch, combined_value, moment}, alternatives[{format, pitch}] (1 or 2), assets{yours[], theirs[]}, agreement[{item, terms}] (exactly eight, in order: campaign description, responsibilities, timeline, lead handling, promotion, branding, costs, metrics sharing), timeline[{phase, when, owner: you|partner|both, what}] (3 to 6), metrics[] (3 to 6), outreach{subject, body}, call_prep[{item, status: ready|missing, note}] (exactly five).
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 lane, the score-to-plan handoff, and a second scorecard.
Score: an invoicing app for freelancers and six possible partners
Input (description, shortlist and facts shortened here):
{
"task": "score",
"company": "Tallyho",
"you": "Tallyho - invoicing and payment reminders for freelancers and small creative agencies (1-10 people).\nOur buyer is a freelance designer, developer or writer who sends 5 to 30 invoices a month and hates chasing late payments.\nAudience: our newsletter \"Paid on Time\" has 14,200 subscribers with a 41% open rate; about 3,000 paying customers.\nIntegrations today: Stripe, PayPal and a Zapier app. Customers keep asking for a ... (641 characters in all)",
"partners": "partner,what_they_do,audience,audience_size,open_rate,relationship,competes,partnerships_team,past_co_marketing,notes\nTimesmith,time tracking with billable hours,freelancers and small agencies,22k newsletter,38%,native integration in beta; customers ask for it,no,yes (Dana runs partnerships),yes - podcast swaps with two design tools,Their users export hours to spreadsheets to invoice\nLedgerloop,invoicing app,freelanc... (1140 characters in all)",
"facts": "(a JSON string; decoded: company Tallyho, your audience 14200, buyer stated, past co-marketing yes, 6 partners: Timesmith (size 4, engagement 4, reciprocity 5, ease 5); Ledgerloop (size 3, engagement 3, reciprocity 5, ease 1); BigBooks (size 5, engagement -, reciprocity 1, ease 5); Freelance Friday (size 3, engagement 5, reciprocity 5, ease 3); Clauseful (size 2, engagement 3, reciprocity 3, ease 1); Pixelpost (size 4, engagement 2, reciprocity 3, ease 1); flags CM-01@Ledgerloop, CM-04@Ledgerloop, CM-07@Ledgerloop, CM-03@BigBooks, CM-09@BigBooks, CM-04@Clauseful, CM-07@Clauseful, CM-04@Pixelpost, CM-07@Pixelpost)",
"goal": "any",
"question": "Who should we approach first?"
}
Reply:
{
"task": "score",
"partners": [
{
"name": "Timesmith",
"type": "integrations",
"scores": {
"audience_fit": {
"score": 5,
"why": "Timesmith serves the same freelancers and small agencies, and sits right before invoicing in their workflow.",
"evidence": [
"freelancers and small agencies",
"Their users export hours to spreadsheets to invoice"
]
},
"audience_size": {
"score": 4,
"why": "Their 22k newsletter falls in the page's 20,000 to 99,999 band.",
"evidence": [
"22k newsletter"
]
},
"brand_alignment": {
"score": 4,
"why": "A billable-hours tool your customers already ask for is a partner you would be glad to be seen with.",
"evidence": [
"Customers keep asking for a time-tracking integration"
]
},
"engagement": {
"score": 4,
"why": "Their 38% open rate scores 4 on the page's scale.",
"evidence": [
"38%"
]
},
"reciprocity": {
"score": 5,
"why": "Their 22k newsletter against your 14,200 subscribers is a ratio of 1.5, well matched.",
"evidence": [
"22k newsletter"
]
},
"ease": {
"score": 5,
"why": "They have a named partnerships lead and past co-marketing.",
"evidence": [
"yes (Dana runs partnerships)",
"yes - podcast swaps with two design tools"
]
}
},
"verdict": "pursue",
"reason": "Same buyer, the next step in their workflow, an integration already in beta and a partnerships lead to talk to.",
"campaigns": [
"integration launch",
"podcast guest exchange"
],
"risks": "",
"find_out": "When the beta integration can launch publicly and whether Dana will co-own the launch."
},
{
"name": "Ledgerloop",
"type": "c
... (shortened here; the page shows it whole)
Plan: the top pick, carrying its scorecard entry
Input (with the scorecard entry handed over):
{
"task": "plan",
"company": "Tallyho",
"you": "Tallyho - invoicing and payment reminders for freelancers and small creative agencies (1-10 people).\nOur buyer is a freelance designer, developer or writer who sends 5 to 30 invoices a month and hates chasing late payments.\nAudience: our newsletter \"Paid on Time\" has 14,200 subscribers with a 41% open rate; about 3,000 paying customers.\nIntegrations today: Stripe, PayPal and a Zapier app. Customers keep asking for a ... (641 characters in all)",
"partners": "partner,what_they_do,audience,audience_size,open_rate,relationship,competes,partnerships_team,past_co_marketing,notes\nTimesmith,time tracking with billable hours,freelancers and small agencies,22k newsletter,38%,native integration in beta; customers ask for it,no,yes (Dana runs partnerships),yes - podcast swaps with two design tools,Their users export hours to spreadsheets to invoice",
"facts": "(a JSON string; decoded: company Tallyho, your audience 14200, buyer stated, past co-marketing yes, 1 partner: Timesmith (size 4, engagement 4, reciprocity 5, ease 5); flags none)",
"goal": "any",
"partner": "Timesmith",
"question": "Who should we approach first?",
"score_handoff": "{\"partner\":\"Timesmith\",\"verdict\":\"pursue\",\"type\":\"integrations\",\"scores\":{\"audience_fit\":5,\"audience_size\":4,\"brand_alignment\":4,\"engagement\":4,\"reciprocity\":5,\"ease\":5},\"campaigns\":[\"integration launch\",\"podcast guest exchange\"],\"risks\":\"\"}"
}
Reply:
{
"task": "plan",
"source": "score handoff",
"partner": "Timesmith",
"campaign": {
"format": "integration launch",
"category": "product",
"title": "Track it, bill it: Timesmith hours straight into Tallyho invoices",
"pitch": "Launch the Timesmith and Tallyho integration together so freelancers turn tracked billable hours into invoices without a spreadsheet.",
"combined_value": "Freelancers get tracked hours and a sent, chased invoice in one flow, where today Timesmith users export hours to spreadsheets to invoice.",
"moment": "Tallyho customers keep asking for a time-tracking integration, and Timesmith users export hours to spreadsheets to invoice."
},
"alternatives": [
{
"format": "podcast guest exchange",
"pitch": "Swap podcast guest spots on getting paid for billable hours, building on Timesmith's podcast swaps with design tools."
},
{
"format": "joint webinar",
"pitch": "A joint webinar showing freelancers the hours-to-invoice workflow, following Tallyho's earlier joint webinar with a tax app."
}
],
"assets": {
"yours": [
"The \"Paid on Time\" newsletter with 14,200 subscribers and a 41% open rate",
"About 3,000 paying customers who ask for a time-tracking integration",
"Experience running a joint webinar with a tax app for freelancers"
],
"theirs": [
"A 22k newsletter of freelancers and small agencies with a 38% open rate",
"A native integration with Tallyho in beta",
"Dana, who runs partnerships",
"Experience with podcast swaps with two design tools"
]
},
"agreement": [
{
"item": "campaign description",
"terms": "Tallyho and Timesmith jointly announce the native integration to freelancers and small agencies, with a shared \"better together\" message."
},
{
"item": "responsibiliti
... (shortened here; the page shows it whole)
Plan: a newsletter tool and a landing-page partner, before a launch date
Input:
{
"task": "plan",
"company": "Quillsend",
"you": "Quillsend - email newsletter software for independent creators (writers, illustrators, podcasters) who sell digital products.\nOur newsletter for creators has 31,000 subscribers (35% open rate) and we post weekly on YouTube.\nWe have never run a co-marketing campaign before.\nWe want to run something with a landing page partner before 14 November, when our paid-products feature launches.\nBudget: we can pay for a webinar... (457 characters in all)",
"partners": "partner,what_they_do,who_they_serve,audience_size,open_rate,shared_customers,competes,partnerships_team,past_co_marketing\nPageform,landing pages and link-in-bio pages,independent creators,45k newsletter,33%,\"about 600 customers use both (from our onboarding survey)\",no,yes - Mara Quintebrook leads partnerships,yes - co-hosted a workshop with a design tool",
"facts": "(a JSON string; decoded: company Quillsend, your audience 31000, buyer stated, past co-marketing no, 1 partner: Pageform (size 4, engagement 4, reciprocity 5, ease 5); flags CM-10)",
"goal": "events",
"partner": "Pageform"
}
Reply:
{
"task": "plan",
"source": "from the input",
"partner": "Pageform",
"campaign": {
"format": "joint webinar",
"category": "events",
"title": "From page to paid: selling digital products with your newsletter and landing page",
"pitch": "A joint webinar showing independent creators how to pair a Pageform landing page with a Quillsend newsletter to sell digital products, timed to the paid-products launch.",
"combined_value": "Creators see the full path from a link-in-bio page to an email list to a paid product, which neither tool shows alone.",
"moment": "Quillsend's paid-products feature launches on 14 November."
},
"alternatives": [
{
"format": "co-hosted workshop",
"pitch": "A hands-on workshop where creators build a product landing page and a launch email together, a format Pageform has run before."
},
{
"format": "joint AMA or Twitter Space",
"pitch": "A live AMA with both teams on selling digital products to a newsletter audience."
}
],
"assets": {
"yours": [
"A creator newsletter with 31,000 subscribers and a 35% open rate",
"A weekly YouTube channel",
"The paid-products feature launching 14 November",
"Budget for a webinar tool and design time"
],
"theirs": [
"A 45k newsletter of independent creators with a 33% open rate",
"Landing page and link-in-bio product",
"A partnerships lead, Mara Quintebrook",
"Experience co-hosting a workshop with a design tool"
]
},
"agreement": [
{
"item": "campaign description",
"terms": "Quillsend and Pageform co-host one live webinar for independent creators on selling digital products with a landing page and a newsletter."
},
{
"item": "responsibilities",
"terms": "Quillsend runs the webinar tool, design and the newsletter demo; Pageform pres
... (shortened here; the page shows it whole)
Score: a bare shortlist, three names and a note each
Input:
{
"task": "score",
"company": "Classquay",
"you": "Classquay - class booking and memberships software for independent yoga and pilates studios.\nStudio owners use us to run their schedule, sell class packs and send reminders.\nWe have never done co-marketing. We want to try one partnership this spring.\n",
"partners": "partner,notes\nMatwell,\"sells yoga mats and props wholesale to studios; their rep visits studios in person\"\nStudiolens,\"photo and video packages for fitness studios; small team\"\nStretchreel,\"on-demand yoga video platform for consumers; studios sometimes list classes there\"",
"facts": "(a JSON string; decoded: company Classquay, your audience not stated, buyer stated, past co-marketing no, 3 partners: Matwell (size -, engagement -, reciprocity -, ease -); Studiolens (size -, engagement -, reciprocity -, ease -); Stretchreel (size -, engagement -, reciprocity -, ease -); flags CM-02@Matwell, CM-07@Matwell, CM-09@Matwell, CM-02@Studiolens, CM-07@Studiolens, CM-09@Studiolens, CM-02@Stretchreel, CM-07@Stretchreel, CM-09@Stretchreel, CM-05)",
"goal": "any",
"question": "Is any of these worth a first partnership?"
}
Reply:
{
"task": "score",
"partners": [
{
"name": "Matwell",
"type": "co-marketing",
"scores": {
"audience_fit": {
"score": 5,
"why": "Matwell sells to the same studio owners you serve, solving a different problem: equipment, not scheduling.",
"evidence": [
"sells yoga mats and props wholesale to studios",
"independent yoga and pilates studios"
]
},
"audience_size": {
"score": null,
"why": "The shortlist gives no audience size for Matwell.",
"evidence": []
},
"brand_alignment": {
"score": 4,
"why": "A props supplier whose rep already visits studios in person is a natural, trusted name beside a studio software tool.",
"evidence": [
"their rep visits studios in person"
]
},
"engagement": {
"score": null,
"why": "No open or engagement rate is given for Matwell.",
"evidence": []
},
"reciprocity": {
"score": null,
"why": "Neither your audience size nor Matwell's is stated, so the two cannot be compared.",
"evidence": []
},
"ease": {
"score": null,
"why": "The shortlist does not say whether Matwell has a partnerships team or past co-marketing.",
"evidence": []
}
},
"verdict": "pursue",
"reason": "Same buyer, a different problem, and a sales rep who already meets your buyers face to face.",
"campaigns": [
"co-hosted workshop",
"bundle or discount"
],
"risks": "No audience size, engagement figure or relationship with you is stated, so reach and ease are unknown.",
"find_out": "How many studios Matwell supplies and who there would own a partnership."
},
{
"name": "Studiolens",
"type": "co-marketing",
"scores": {
"audience_fit": {
"score": 4,
"why": "Studi
... (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 scorecard, one entry per partner it sent, in order, names spelled the same; all six criteria; audience size, engagement, reciprocity and ease equal to the page's own scores and null where it had nothing; audience fit and brand alignment quoted when scored; pass for a competitor (CM-01) or a fit of 1 or 2, pursue only for a fit of 4 or 5 with no 1 on reciprocity or ease; a real partnership type; campaign formats from the skill's list; a ranking of every partner with pursue before maybe before pass and the top pick first. For a plan, the partner asked for; a campaign format from the list in its own category and the preferred one; the eight agreement items in order with lead handling stated; 3 to 6 timeline phases; metrics from the skill's list; an outreach email naming both companies, under 120 words, asking for a call; call preparation marked ready only when the input has it. In both, quotes are checked word for word and no size, rate, price, 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://comarketing-fit.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://comarketing-fit.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"]
// comarketing-fit.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://comarketing-fit.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://comarketing-fit.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
}
// CoMarketingFit.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 CoMarketingFit {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://comarketing-fit.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://comarketing-fit.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://comarketing-fit.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://comarketing-fit.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":"comarketing-fit"}' | 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": "comarketing-fit"})["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: "comarketing-fit" })).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": "comarketing-fit"}, 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", "comarketing-fit"), 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: "comarketing-fit" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "comarketing-fit"])["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 = "comarketing-fit" }))!["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": "...", "you": "...", "partners": "...", "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": "...", "you": "...", "partners": "...", "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, you, partners, goal, partner, 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 comarketing-fit:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="comarketing-fit: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 = "comarketing-fit:" + 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 = `comarketing-fit:${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 := "comarketing-fit:" + 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 = "comarketing-fit:" + 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 = "comarketing-fit:#{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 = "comarketing-fit:" . $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 = $"comarketing-fit:{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 scorecard.
# 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: comarketing-fit:plan:$(shasum -a 256 plan.json | cut -c1-16):a1" --data @plan.json
rev = json.load(open("plan.json")) # {"task": "plan", "company": "...", "you": "...", "partners": "...", "partner": "...", "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": "comarketing-fit: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": `comarketing-fit: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", "comarketing-fit: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", "comarketing-fit: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" => "comarketing-fit: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: comarketing-fit: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", $"comarketing-fit: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);