← Research Paper Generator / API
Tokens

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

codestatuswhat to do
unauthorized401The 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_required402The balance is below min_credits. Call /estimate first and top up.
forbidden403The token is valid but not for this app, or a guest token tried a metered run. Writing a paper needs a personal token.
not_found404Unknown job id or unknown collection.
conflict409The same Idempotency-Key was replayed with a different body. Change the key or send the original input.
invalid_request400Malformed JSON, or a missing slug on /guest.
rate_limited429Too many requests. Back off; do not tight-loop.
internal5xxA 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"

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".

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.

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}}

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.

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"]
}
fieldrequiredwhat it does
subjectyesWhat 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.
steernoOne extra line of direction, at most 240 characters. Obeyed, but it never overrides the parody rules.
registeryesThe 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.
fieldyesThe discipline. Ten available: biomedical, psychology, physics, computer_science, economics, ecology, linguistics, materials, sociology, library_science.
lengthyesabstract, standard or extended. Controls the word budget, whether a limitations paragraph and a discussion appear, and how many references are issued.
briefyesThe 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.
apparatusyesThe 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.
scanyesWhat a free client-side pass found in the subject. Reconciled against the finished paper afterwards.
house_rules, parody_rulesyesThe craft defaults and the five prohibitions nothing overrides.
bannedyesMachinery 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.
reviseno{ 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": ["...", "..."]
}

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.