Drive Research Paper Generator from your own code
Everything the page does is available over HTTP: send a subject and a choice of register, discipline and length, get back a structured spoof research paper as JSON. The natural uses are a bot that posts one absurd abstract a day, a seminar-slide generator, and filling a demo corpus with documents that are unmistakably not real papers.
Everything this API returns is parody. No response describes a real study or a real finding. Every author, institution, number and reference is invented. Responses are watermarked, and any document you build from one should stay watermarked — that is what the notice fields in the JSON are for.
The app will not name a real journal, a real researcher, or produce a DOI, arXiv id, PMID or ISBN, and a run whose generated text contains one is blocked server-side before you receive it. Do not build anything that strips the marking.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every
response uses the same envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Send your token as Authorization: Bearer … on every call.
You do not send an app slug on ordinary calls — the token is
already scoped to this app when it is minted. The slug appears in exactly one place, the
body of POST /guest. Sending it as an X-App-Slug header
instead returns 400 slug is required; several apps in this fleet document
the header form and it does not work.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. A 401 on a first call from a browser that has never signed in is normal, not a fault. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Writing a paper needs a personal token. |
not_found | 404 | Unknown job id or unknown collection. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
invalid_request | 400 | Malformed JSON, or a missing slug on /guest. |
rate_limited | 429 | Too many requests. Back off; do not tight-loop. |
internal | 5xx | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. Get a token
The token page is the shortest path: it shows the token this browser already holds, reveals it, and copies a shell export. From code, mint a guest token by posting the slug in the body.
# The shortest path is the token page: https://research-paper-generator.skillsafe.ai/tokens.html
# It shows the token this browser already holds and copies a shell export for you.
#
# To mint a GUEST token from code, POST the slug IN THE BODY. There is no
# X-App-Slug header on this endpoint - sending one and omitting the body field
# returns 400 "slug is required".
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"research-paper-generator"}'
# -> {"ok":true,"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}
#
# A guest token can call /me and /estimate. Writing a paper is metered, so /run
# and /run-stream need a PERSONAL token, which comes from signing in on the
# token page. The token is already scoped to this app: no slug header is needed
# on any later call.
export SKILLSAFE_TOKEN="aut_YOUR_TOKEN"
# Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
# Guest token from code - note the slug goes in the BODY, not in a header:
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "research-paper-generator"}).encode(),
headers={"Content-Type": "application/json"},
method="POST")
guest = json.load(urllib.request.urlopen(req))["data"]["token"]
TOKEN = "YOUR_TOKEN" # a personal token; a guest token cannot run a paper
// Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
// Guest token from code - the slug goes in the BODY, not in a header:
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "research-paper-generator" })
});
const guest = (await res.json()).data.token;
const TOKEN = "YOUR_TOKEN"; // personal; a guest token cannot run a paper
// Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
// Guest token from code - the slug goes in the BODY, not in a header.
body := bytes.NewReader([]byte(`{"slug":"research-paper-generator"}`))
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/guest", body)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
const TOKEN = "YOUR_TOKEN" // personal; a guest token cannot run a paper
// Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
// Guest token from code - the slug goes in the BODY, not in a header.
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"research-paper-generator\"}"))
.build();
var guestRes = HttpClient.newHttpClient()
.send(guestReq, HttpResponse.BodyHandlers.ofString());
static final String TOKEN = "YOUR_TOKEN"; // personal; a guest cannot run a paper
# Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
# Guest token from code - the slug goes in the BODY, not in a header.
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
res = Net::HTTP.post(uri, {slug: "research-paper-generator"}.to_json,
"Content-Type" => "application/json")
guest = JSON.parse(res.body)["data"]["token"]
TOKEN = "YOUR_TOKEN" # personal; a guest token cannot run a paper
<?php
// Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
// Guest token from code - the slug goes in the BODY, not in a header.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "research-paper-generator"]),
CURLOPT_RETURNTRANSFER => true,
]);
$guest = json_decode(curl_exec($ch), true)["data"]["token"];
const TOKEN = "YOUR_TOKEN"; // personal; a guest token cannot run a paper
// Personal token: https://research-paper-generator.skillsafe.ai/tokens.html -> "Copy token".
// Guest token from code - the slug goes in the BODY, not in a header.
var guestReq = new HttpRequestMessage(HttpMethod.Post,
"https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\":\"research-paper-generator\"}",
Encoding.UTF8, "application/json");
var guestRes = await new HttpClient().SendAsync(guestReq);
const string TOKEN = "YOUR_TOKEN"; // personal; a guest cannot run a paper
2. A tiny client, and what /me tells you
Every call is the same three things: the base URL, the bearer token, and a JSON body on the POSTs. /me returns three fields and no more.
BASE="https://api.skillsafe.ai/v1/app-api"
AUTH="Authorization: Bearer $SKILLSAFE_TOKEN"
# /me tells you three things and only three things.
curl -sS "$BASE/me" -H "$AUTH"
# -> {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":41200}}
#
# There is no email and no display name in this response. "Signed in" is
# subject_type == "user"; a guest token reports subject_type == "guest".
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method="POST" if data else "GET",
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
me = call("/me")
# {"subject_type": "user", "subject_id": "usr_...", "credits": 41200}
# Three fields, no more. Signed in means subject_type == "user".
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, body) {
const res = await fetch(BASE + path, {
method: body ? "POST" : "GET",
headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!json.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
return json.data;
}
const me = await call("/me");
// { subject_type: "user", subject_id: "usr_...", credits: 41200 }
// Signed in means subject_type === "user". There is no name field.
const BASE = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body []byte) ([]byte, error) {
method := "GET"
var r io.Reader
if body != nil { method, r = "POST", bytes.NewReader(body) }
req, _ := http.NewRequest(method, BASE+path, r)
req.Header.Set("Authorization", "Bearer "+TOKEN)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
me, _ := call("/me", nil)
// {"subject_type":"user","subject_id":"usr_...","credits":41200}
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String path, String body) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
var req = (body == null ? b.GET() : b.POST(HttpRequest.BodyPublishers.ofString(body))).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
String me = call("/me", null);
// {"subject_type":"user","subject_id":"usr_...","credits":41200}
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, body = nil)
uri = URI(BASE + path)
req = (body ? Net::HTTP::Post : Net::HTTP::Get).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(body) if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["data"]
end
me = call("/me")
# {"subject_type"=>"user", "subject_id"=>"usr_...", "credits"=>41200}
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . $path);
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN, "Content-Type: application/json"]];
if ($body !== null) { $opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = json_encode($body); }
curl_setopt_array($ch, $opts);
return json_decode(curl_exec($ch), true)["data"];
}
$me = call("/me");
// ["subject_type" => "user", "subject_id" => "usr_...", "credits" => 41200]
const string BASE = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new HttpClient();
static async Task<string> Call(string path, string body = null) {
var req = new HttpRequestMessage(body == null ? HttpMethod.Get : HttpMethod.Post, BASE + path);
req.Headers.Add("Authorization", "Bearer " + TOKEN);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var me = await Call("/me");
// {"subject_type":"user","subject_id":"usr_...","credits":41200}
3. Price it for free with /estimate
/estimate costs nothing, creates no job and charges no credits. It reports what the run will reserve, the floor below which it will not start, and which model is bound.
# The body IS the input object. There is no wrapper: sending {"input": {...}}
# returns 200 and prices an empty request, because this endpoint performs NO
# body validation at all. A bare string, a number, null and [] each come back
# ok:true with an identical plausible hold. So a passing estimate proves your
# MODEL BINDING and tells you nothing whatever about your input shape.
curl -sS -X POST "$BASE/estimate" -H "$AUTH" \
-H "Content-Type: application/json" \
--data-binary @input.json
# -> {"ok":true,"data":{"hold_credits":2118,"min_credits":204,
# "model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
# "sponsor_enabled":false,"byok":false}}
#
# hold_credits is RESERVED, not charged. The settled charge is usually far
# lower, because the hold prices the full output cap.
# The body IS the input object - no {"input": ...} wrapper.
est = call("/estimate", paper_input)
print(est["hold_credits"], est["min_credits"], est["model_alias"])
# WARNING worth internalising: /estimate does no body validation. Passing a
# string, a number, None or [] all return ok:true with a plausible hold. A
# passing estimate proves the model binding, never the input shape - so
# validate the object yourself before you spend anything.
assert isinstance(paper_input, dict) and paper_input.get("subject")
// The body IS the input object - no { input: ... } wrapper.
const est = await call("/estimate", paperInput);
console.log(est.hold_credits, est.min_credits, est.model_alias);
// /estimate performs no body validation: a string, a number, null and []
// all return ok:true with a plausible hold. It proves the model binding and
// nothing about your input, so check the shape yourself before spending.
if (!paperInput || typeof paperInput !== "object" || !paperInput.subject) {
throw new Error("input is not a run-input object");
}
// The body IS the input object - no {"input": ...} wrapper.
est, _ := call("/estimate", paperInput)
// {"hold_credits":2118,"min_credits":204,"model":"gpt-5.6-terra",
// "model_alias":"gpt-terra","markup_bps":1000}
//
// /estimate does not validate the body. Any JSON at all returns a plausible
// hold, so validate the shape yourself before spending.
// The body IS the input object - no {"input": ...} wrapper.
String est = call("/estimate", paperInputJson);
// {"hold_credits":2118,"min_credits":204,"model_alias":"gpt-terra", ...}
//
// /estimate does not validate the body. Any JSON returns a plausible hold, so
// it proves the model binding only. Validate the shape before spending.
# The body IS the input object - no {"input" => ...} wrapper.
est = call("/estimate", paper_input)
puts est["hold_credits"], est["model_alias"]
# /estimate does not validate the body: a String, an Integer, nil and []
# all return a plausible hold. Validate the shape yourself before spending.
raise "not a run input" unless paper_input.is_a?(Hash) && paper_input["subject"]
<?php
// The body IS the input object - no ["input" => ...] wrapper.
$est = call("/estimate", $paperInput);
echo $est["hold_credits"], " ", $est["model_alias"];
// /estimate does not validate the body; anything JSON returns a plausible
// hold. Validate the shape yourself before spending.
if (!is_array($paperInput) || empty($paperInput["subject"])) {
throw new RuntimeException("input is not a run-input object");
}
// The body IS the input object - no {"input": ...} wrapper.
var est = await Call("/estimate", paperInputJson);
// {"hold_credits":2118,"min_credits":204,"model_alias":"gpt-terra", ...}
//
// /estimate does not validate the body. Any JSON returns a plausible hold,
// so validate the shape yourself before spending.
Read the warning in those samples. /estimate performs no body validation at all: a bare string, a number, null and [] each return ok:true with an identical plausible hold. A passing estimate proves your model binding and proves nothing whatever about your input shape. Validate the object on your side before you spend anything — that is the only place the mistake is catchable.
4. Run it
A run is metered and needs a personal token. Send an Idempotency-Key on every one: replaying the same key with the same body returns the original result rather than billing twice, and replaying it with a different body is a 409. If you retry after a network failure, reuse the key.
# Send an Idempotency-Key on EVERY run. Replaying the same key with the same
# body returns the original result instead of billing a second time; replaying
# it with a DIFFERENT body is a 409.
curl -sS -X POST "$BASE/run" -H "$AUTH" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: research-paper-generator-4f2a9c-0" \
--data-binary @input.json
# -> {"ok":true,"data":{"job_id":"job_...","status":"queued"}}
# Then poll until the status is terminal.
curl -sS "$BASE/run/job_..." -H "$AUTH"
# -> {"ok":true,"data":{"status":"succeeded","output_text":"{...}",
# "charged_credits":just_over_a_third_of_the_hold,"truncated":false}}
import time, uuid
key = "research-paper-generator-" + uuid.uuid4().hex[:8] + "-0"
job = call("/run", paper_input) # send Idempotency-Key: key
while True:
state = call("/run/" + job["job_id"])
if state["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(1.5)
paper = json.loads(state["output_text"]) # the object documented below
print(paper["title"])
print(state["charged_credits"], "credits charged")
const key = `research-paper-generator-${crypto.randomUUID().slice(0, 8)}-0`;
const job = await call("/run", paperInput); // send Idempotency-Key: key
let state;
do {
await new Promise(r => setTimeout(r, 1500));
state = await call(`/run/${job.job_id}`);
} while (!["succeeded", "failed", "cancelled"].includes(state.status));
const paper = JSON.parse(state.output_text);
console.log(paper.title, state.charged_credits);
// POST /run with an Idempotency-Key header, then poll GET /run/{job_id}
// until status is succeeded, failed or cancelled.
job, _ := call("/run", paperInput)
for {
state, _ := call("/run/"+jobID, nil)
if terminal(state) { break }
time.Sleep(1500 * time.Millisecond)
}
// state.output_text holds the JSON object documented below.
// POST /run with an Idempotency-Key header, then poll GET /run/{jobId}
// until status is succeeded, failed or cancelled.
String job = call("/run", paperInputJson);
String state;
do {
Thread.sleep(1500);
state = call("/run/" + jobId, null);
} while (!isTerminal(state));
// state.output_text holds the JSON object documented below.
key = "research-paper-generator-#{SecureRandom.hex(4)}-0"
job = call("/run", paper_input) # send Idempotency-Key: key
state = nil
loop do
state = call("/run/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(state["status"])
sleep 1.5
end
paper = JSON.parse(state["output_text"])
puts paper["title"], state["charged_credits"]
<?php
$key = "research-paper-generator-" . bin2hex(random_bytes(4)) . "-0";
$job = call("/run", $paperInput); // send Idempotency-Key: $key
do {
sleep(2);
$state = call("/run/" . $job["job_id"]);
} while (!in_array($state["status"], ["succeeded", "failed", "cancelled"], true));
$paper = json_decode($state["output_text"], true);
echo $paper["title"], " ", $state["charged_credits"];
var key = $"research-paper-generator-{Guid.NewGuid().ToString("N")[..8]}-0";
var job = await Call("/run", paperInputJson); // send Idempotency-Key: key
string state;
do {
await Task.Delay(1500);
state = await Call($"/run/{jobId}");
} while (!IsTerminal(state));
// state.output_text holds the JSON object documented below.
hold_credits is reserved, not charged; charged_credits on the terminal job is what you actually paid, and it is usually far lower because the hold prices the full output cap. If truncated is true the balance sat between min_credits and hold_credits and the run finished with a reduced cap — the paper is short, not complete.
5. Stream it
Same body, same idempotency discipline, server-sent events. Worth using: a paper takes long enough that a progress indicator matters, and the section keys arrive in order so you can drive one from the stream.
# Server-sent events. Same body, same Idempotency-Key discipline.
curl -sSN -X POST "$BASE/run-stream" -H "$AUTH" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: research-paper-generator-4f2a9c-0" \
-H "Accept: text/event-stream" \
--data-binary @input.json
# event: delta
# data: {"text":"{\"title\":\"Seat"}
#
# event: delta
# data: {"text":" Elevation and ..."}
#
# event: done
# data: {"charged_credits":812,"truncated":false}
#
# Event names are job, delta, done, pending, error. A delta carries its text at
# .text; there is no {"type":"delta"} envelope and that shape never fires.
#
# The deltas concatenate into the JSON object. If the stream dies mid-object,
# the partial text is still worth parsing - walk string state and bracket
# depth, drop the uncompletable tail and close what is open.
# The deltas concatenate into one JSON object. Keep the partial text on
# failure: a stream that dies mid-object still holds whole sections.
buf = ""
for line in stream_lines("/run-stream", paper_input):
if not line.startswith("data: "):
continue
event = json.loads(line[6:])
if event["type"] == "delta":
buf += event["text"]
elif event["type"] == "done":
charged = event["charged_credits"]
paper = json.loads(buf)
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
"Accept": "text/event-stream"
},
body: JSON.stringify(paperInput)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "";
for (;;) {
const { done, value } = 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.type === "delta") text += ev.text;
}
}
const paper = JSON.parse(text);
// POST /run-stream with Accept: text/event-stream and read the body line by
// line. "event: " names each frame and "data: " carries its JSON; concatenate
// the .text of every "event: delta" frame into the output object. There is no
// {"type":"delta"} envelope - that shape never fires.
//
// Keep the partial buffer on error - a stream that dies mid-object still
// holds whole sections worth rendering.
// POST /run-stream with Accept: text/event-stream and read the response as a
// line stream. "event: " names each frame and "data: " carries its JSON;
// concatenate the .text of every "event: delta" frame into the output object.
// There is no {"type":"delta"} envelope - that shape never fires.
//
// Keep the partial buffer on error - a stream that dies mid-object still
// holds whole sections worth rendering.
# POST /run-stream with Accept: text/event-stream, then read the body in
# chunks. Lines beginning "data: " carry a JSON event; concatenate every
# {"type" => "delta"} text field into the output object.
buf = ""
stream_lines("/run-stream", paper_input) do |line|
next unless line.start_with?("data: ")
ev = JSON.parse(line[6..])
buf << ev["text"] if ev["type"] == "delta"
end
paper = JSON.parse(buf)
<?php
// POST /run-stream with Accept: text/event-stream and a CURLOPT_WRITEFUNCTION
// that accumulates the body. Lines beginning "data: " carry a JSON event;
// concatenate every ["type" => "delta"] text field into the output object.
//
// Keep the partial buffer on error - a stream that dies mid-object still
// holds whole sections worth rendering.
// POST /run-stream with Accept: text/event-stream and read the response
// stream line by line. "event: " names each frame and "data: " carries its
// JSON; concatenate the .text of every "event: delta" frame into the output
// object. There is no {"type":"delta"} envelope - that shape never fires.
//
// Keep the partial buffer on error - a stream that dies mid-object still
// holds whole sections worth rendering.
6. The input object, field by field
This is the exact object app.js submits, and the exact object
/estimate, /run and /run-stream accept as their
whole body. There is one shape — this app has no task router and no lane field.
{
"subject": "the correct height of an office chair",
"steer": "a reviewer demanded a second cohort",
"register": {
"id": "clinical_trial",
"label": "Clinical trial report",
"pull": "The register of a randomised trial write-up. ...",
"avoid": ["speculation in the Results"],
"overrides": "H7 NO-NARRATIVE - a participant-flow account is REQUIRED ...",
"shape": { "wps": [18, 30], "varies": "low", "hedge": "mid",
"passive": "high", "cites": "mid" }
},
"field": { "id": "psychology", "label": "Psychology", "pull": "...",
"conventions": ["a validated scale"], "avoid": ["..."] },
"length": { "id": "standard", "label": "Abstract and limitations",
"target_words": 480, "band": [370, 640], "note": "..." },
"brief": {
"seed": "pp_zuwte8",
"length_id": "standard",
"citations": 3,
"engine": [ { "axis": "operation", "label": "...", "value": "..." } ],
"telling": [ { "axis": "hedge", "label": "...", "value": "..." } ],
"still_near": null
},
"apparatus": {
"authors": [ { "name": "Quill M", "affiliation": 1 } ],
"affiliations": ["Unit for the Investigation of Settled Questions"],
"citation_keys": [1, 2, 3]
},
"scan": { "chars": 37, "words": 7, "question": false, "quantity": true,
"claim": false, "population": false, "comparison": false,
"proper_nouns": [] },
"house_rules": { "H1": "IMPERSONAL - ...", "H2": "HEDGED - ..." },
"parody_rules": { "P1": "NEVER-REAL-CITE - ...", "P5": "NO-WINK - ..." },
"banned": ["huge effect", "call for research"]
}
| field | required | what it does |
|---|---|---|
subject | yes | What the paper is about. Three characters is the floor; anything past 1600 characters is clipped from the middle, with a marker, and the clip note is reported back. |
steer | no | One extra line of direction, at most 240 characters. Obeyed, but it never overrides the parody rules. |
register | yes | The document type. Nine available: clinical_trial, bench_science, field_observational, theory_paper, meta_analysis, case_report, survey_instrument, computational, economics_paper. shape is the structural target the finished paper is measured against, so send it. |
field | yes | The discipline. Ten available: biomedical, psychology, physics, computer_science, economics, ecology, linguistics, materials, sociology, library_science. |
length | yes | abstract, standard or extended. Controls the word budget, whether a limitations paragraph and a discussion appear, and how many references are issued. |
brief | yes | The drawn coordinate: five engine axes deciding what the paper does and seven telling axes deciding how it sounds, plus the exact number of citation markers to place. This is what makes two papers on one subject two different papers. |
apparatus | yes | The byline, the affiliations and the citation keys, all invented before the model is called. The model may use these keys and no others, and may not restate a venue or a year. |
scan | yes | What a free client-side pass found in the subject. Reconciled against the finished paper afterwards. |
house_rules, parody_rules | yes | The craft defaults and the five prohibitions nothing overrides. |
banned | yes | Machinery vocabulary plus this run's own brief phrasings. None of these may appear in the output; one that does means the model transcribed its instructions rather than writing from them. |
revise | no | { previous, note } on a second pass over a paper you already have. The coordinate stays; the requested change is made and nothing else moves. |
7. The output contract
output_text is a JSON string holding exactly this object. Parse it; do not
treat it as prose.
{
"title": "...",
"abstract": { "background": "...", "methods": "...",
"results": "...", "conclusions": "..." },
"methods_detail": "...",
"measured": [ { "label": "...", "value": "..." } ],
"limitations": "...",
"figure": { "number": 1, "caption": "..." },
"discussion": null,
"declarations": { "funding": "...", "conflicts": "...",
"ethics": "...", "data": "..." },
"references": [ { "key": 1, "title": "..." } ],
"keywords": ["...", "..."]
}
methods_detailis""atlength.id === "abstract".limitationsis""atlength.id === "abstract".discussionisnullunlesslength.id === "extended".figureisnullunless the length or the brief asks for one.referencescarries one entry per key inapparatus.citation_keys, each with atitleand nothing else. The venue, year, volume, issue and pages come from your copy of the apparatus; join onkey.- Bracketed markers in the prose are
[n]and everynis one of the issued keys. - On failure the object is
{"error": "one sentence"}and carries no other field.
8. What you must not strip
The browser marks its output on four page surfaces, in the browser tab, through every
download and in anything copied to the clipboard. Over the API you get the raw object,
so the marking becomes your responsibility. If you render or republish a paper from this
API, carry a visible notice that it is parody — the string
PARODY - NOT A REAL PAPER is what the rest of this app uses and what
crawlers are told to look for in
llms.txt.
Do not present a response as a real finding, do not remove the invented venue or the future-dated year from a reference, and do not add an identifier of any kind to one. The whole reason the reference apparatus is generated client-side from a closed pool is that a fabricated citation which looks real outlives the joke.
9. Collections
Papers written while signed in are stored in a declared collection named
papers, readable only by their owner. Records come back nested:
{ "record_id": "...", "doc": { ... } } — read rec.doc,
never the fields flat. The coord and payload fields are JSON
strings.