← Rulecut / API
Tokens

Drive Rulecut from your own code

The page does its free work in the browser: it splits off the frontmatter, replaces every code block with a marker line (@@KEEP_01@@ ...), lists what must survive and runs a port of the caveman-compress validator. Its two paid lanes are available over HTTP. You send one JSON object with task set to "compress" or "fix", and the model replies with one JSON object whose text lines you put back into the file. The easiest way to build the input is the page itself: TerseKit.buildInput(TerseKit.analyze(text, {name, compressed}), lane) in tersekit.js, which also runs under Node; TerseKit.assemble(A, lane, reply.compressed || reply.fixed) restores the markers and the frontmatter and runs the validator on the result.

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

HTTPcodemeaning
400VALIDATION_ERRORThe body is not a JSON object, or a field is the wrong type. Every field of the run input is a string.
401UNAUTHORIZEDNo token, or an expired one: get a new one (step 2).
402INSUFFICIENT_CREDITSThe balance is below the run's minimum. Top up at skillsafe.ai/account/billing/.
403FORBIDDENA guest token tried a metered run: sign in for a personal token.
404NOT_FOUNDUnknown job id.
429RATE_LIMITEDBack off and retry with the same Idempotency-Key.

The task field

Two lanes, in the order a person meets them:

Input fields (all strings)

fieldwhencontent
taskrequiredcompress or fix
bodyrequiredThe file without its frontmatter, every code block replaced by a marker line (@@KEEP_01@@, @@KEEP_02@@ ...). For compress, one part of a long file (up to 16,000 characters).
compressedfix onlyThe compressed version, masked with the original's markers wherever a block survived byte for byte; an altered block stays raw.
factsrequiredA JSON string: file{name, kind, frontmatter}, part{n, of, from_line, to_line} (compress), measure{chars, words, lines, tokens}, protect{markers[], headings[], inline_code, urls[], paths[], env[], versions[], bullets}, errors[{id, code, text, lost[], added[]}] (fix; for a code error lost names the missing markers).
retry_noteoptionalOnly on a reformat retry: what was wrong with the previous reply.

The reply contract

compress: task, task_note, compressed[] (one string per line, every marker line present once), changes[{kind: article|filler|pleasantry|hedging|phrase|fragment|synonym|merge|example, before, after}] (2-8, quoted from the body and from the result), at_risk[{before, note}] (0-5), headline, summary.

fix: task, task_note, fixed[] (one string per line), repairs[{id, action}] (one per error, same ids and order), headline, summary. Neither lane states a savings figure: the page measures them.

Worked examples

These are the page's own bundled examples (an invented shop, people and URLs), with the input as app.js builds it and the saved model reply the page replays for free - two for compress, one for fix.

compress: a shop's notes for its coding agent

Input (body shown separately, facts shortened here):

{
 "task": "compress",
 "body": "(a string - shown below)",
 "facts": "(a JSON string; decoded: part 1 of 1, about 821 tokens; protects 7 headings, 1 markers, 16 inline code spans, 4 URLs, 1 paths)"
}

body:

# Larkspur Supply storefront - notes for the coding agent

## About the project

Larkspur Supply is a small online shop that sells garden tools and seeds. The storefront is a Next.js 14 application
written in TypeScript, and it talks to a separate order service that we basically treat as a black box. You should
always keep in mind that the shop is run by three people, so it is really important to keep things simple and to avoid
adding new dependencies unless there is a very good reason for it.

The production site lives at https://larkspur-supply.example and the staging site lives at
https://staging.larkspur-supply.example. Please make sure that you never point anything at production while you are
testing.

## Getting started

In order to run the project locally, you will need Node.js 20.11 and pnpm 9. Once you have those installed, you can
simply run the following commands from the root of the repository:

@@KEEP_01@@

The development server will then be available at http://localhost:3100. The reason this port is used is because the
order service mock already runs on port 3000.

## Where things live

- Page routes are in `src/app/`, and each route folder has its own `page.tsx` file.
- Shared UI components are in `src/components/`. It might be worth checking there before you write a new component,
  because there is a good chance that something similar already exists.
- The cart logic lives in `src/lib/cart/` and it is generally the most fragile part of the codebase.
- Product data comes from the CMS, and the client for it is in `src/lib/cms/client.ts`.
- Tests live next to the code they test, in files that end in `.test.ts`.

## Environment

The app reads a handful of environment variables. You should never commit real values for any of them.

| Variable | What it is for |
| --- | --- |
| `NEXT_PUBLIC_SHOP_URL` | The public base URL of the shop, used in links and in the sitemap |
| `ORDER_API_BASE` | The base URL of the order service |
| `CMS_TOKEN` | The read-only token that is used to fetch product data from the CMS |

If you add a new variable, please remember to add it to `.env.example` as well, with a placeholder value, so that
other people know that it exists.

## Rules for changes

1. Always run `pnpm lint` and `pnpm test` before you consider a change to be finished.
2. Do not change anything in `src/lib/cart/` without also adding or updating a test that covers the change. The cart
   has broken in production twice, on 2026-03-14 and on 2026-06-02, and both times it was because of an untested
   change.
3. Prices are stored in cents as integers. You should never use floating point numbers for money, and you should
   format prices for display with the `formatPrice` helper rather than doing it by hand.
4. Keep pull requests small. As a general rule, a pull request should do one thing, and it should be possible to
   review it in about 15 minutes.

## Style

We use the default Prettier settings and the ESLint configuration in `eslint.config.mjs`. Additionally, we prefer
named exports over default exports, because named exports are easier to search for. However, Next.js requires default
exports for page files, so `page.tsx` files are the one exception to this rule.

For more background on how the order service works, see the design notes at
https://wiki.larkspur-supply.example/order-service.

Reply:

{
 "task": "compress",
 "task_note": "",
 "compressed": [
  "# Larkspur Supply storefront - notes for the coding agent",
  "",
  "## About the project",
  "",
  "Larkspur Supply: small online shop, garden tools and seeds. Storefront is Next.js 14 app in TypeScript; talks to separate order service treated as black box. Shop run by three people: keep things simple, no new dependencies without very good reason.",
  "",
  "Production: https://larkspur-supply.example. Staging: https://staging.larkspur-supply.example. Never point anything at production while testing.",
  "",
  "## Getting started",
  "",
  "Run locally: need Node.js 20.11 and pnpm 9. Then run from repository root:",
  "",
  "@@KEEP_01@@",
  "",
  "Dev server at http://localhost:3100, because order service mock already runs on port 3000.",
  "",
  "## Where things live",
  "",
  "- Page routes in `src/app/`; each route folder has own `page.tsx`.",
  "- Shared UI components in `src/components/`. Check there before writing new component; something similar likely exists.",
  "- Cart logic in `src/lib/cart/`; most fragile part of codebase.",
  "- Product data from CMS; client in `src/lib/cms/client.ts`.",
  "- Tests next to code they test, in files ending `.test.ts`.",
  "",
  "## Environment",
  "",
  "App reads a handful of environment variables. Never commit real values for any.",
  "",
  "| Variable | What it is for |",
  "| --- | --- |",
  "| `NEXT_PUBLIC_SHOP_URL` | Public base URL of shop, used in links and sitemap |",
  "| `ORDER_API_BASE` | Base URL of order service |",
  "| `CMS_TOKEN` | Read-only token to fetch product data from CMS |",
  "",
  "New variable: add it to `.env.example` too, with placeholder value, so others know it exists.",
  "",
  "## Rules for changes",
  "",
  "1. Always run `pnpm lint` and `pnpm test` before considering change finished.",
  "2. No changes in `src/lib/cart/` without adding or updating test covering change. Cart broke in production twice, 2026-03-14 and 2026-06-02, both from untested changes.",
  "3. Prices stored in cents as integers. Never use floating point for money; format prices for display with `formatPrice` helper, not by hand.",
  "4. Keep pull requests small. Rule of thumb: one pull request does one thing, reviewable in about 15 minutes.",
  "",
  "## Style",
  "",
  "Default Prettier settings and ESLint config in `eslint.config.mjs`. Prefer named exports over default exports: easier to search for. Next.js requires default exports for page files, so `page.tsx` files are the one exception.",
  "",
  "Order service background: design notes at https://wiki.larkspur-supply.example/order-service."
 ],
 "changes": [
  {
   "kind": "fragment",
   "before": "Please make sure that you never point anything at production while you are",
   "after": "Never point anything at production while testing."
  },
  {
   "kind": "hedging",
   "before": "It might be worth checking there before you write a new component,",
   "after": "Check there before writing new component; something similar likely exists."
  },
  {
   "kind": "phrase",
   "before": "The reason this port is used is because the",
   "after": "because order service mock already runs on port 3000."
  },
  {
   "kind": "filler",
   "before": "it is generally the most fragile part of the codebase.",
   "after": "most fragile part of codebase."
  },
  {
   "kind": "fragment",
   "before": "3. Prices are stored in cents as integers. You should never use floating point numbers for money, and you should",
   "after": "3. Prices stored in cents as integers. Never use floating point for 
... (shortened here; the page shows it whole)

compress: writing preferences with frontmatter and a duplicated rule

Input (the frontmatter is kept by the page and never sent):

{
 "task": "compress",
 "body": "(a string - shown below)",
 "facts": "(a JSON string; decoded: part 1 of 1, about 433 tokens; protects 6 headings, 1 markers, 0 inline code spans, 0 URLs, 1 paths)"
}

body:


# Writing preferences

## Tone

Sure! Here are my preferences. I generally prefer a plain, direct tone. It would be good to avoid marketing language,
and I would really rather not see words like "seamless", "robust" or "leverage" in anything you draft for me. When you
are not certain about something, you should say so clearly instead of hedging every single sentence.

## Drafts

- When I ask for a draft, give me one version, not three. If I want alternatives, I will ask for them.
- Keep paragraphs short. As a rule of thumb, a paragraph should be no longer than four sentences.
- Use British spelling (organise, colour, catalogue), because most of my readers are in the UK.
- Do not add a conclusion section that simply repeats what the document has already said.
- Do not add a conclusion that restates the earlier sections in different words.

## Reviews

When I ask you to review something that I have written, I would like you to:

1. Point out anything that is factually wrong first, before anything else.
2. Then point out sentences that are hard to follow, and suggest a shorter version for each of them.
3. Leave style preferences until the very end, and keep them to five items at most.

Please do not rewrite the whole document unless I specifically ask you to do that.

## Summaries

For meeting notes, I basically want three things: the decisions that were made, the actions with an owner and a
date, and the open questions. A summary should fit on one screen. For example, a summary of the planning meeting on
2026-09-11 would look like this:

@@KEEP_01@@

## Things I am working on

- [x] Finish the Q3 report for the board
- [ ] Rewrite the onboarding guide for new contractors
- [ ] Review the style guide draft that Tomasz shared in the team folder at docs/style/house-style-v3.md

Reply:

{
 "task": "compress",
 "task_note": "",
 "compressed": [
  "",
  "# Writing preferences",
  "",
  "## Tone",
  "",
  "Prefer plain, direct tone. Avoid marketing language; no words like \"seamless\", \"robust\" or \"leverage\" in drafts for me.",
  "When unsure, say so clearly; don't hedge every sentence.",
  "",
  "## Drafts",
  "",
  "- Draft request: give one version, not three. Want alternatives, I'll ask.",
  "- Short paragraphs. Rule of thumb: max four sentences.",
  "- British spelling (organise, colour, catalogue): most readers in UK.",
  "- No conclusion section repeating or restating earlier sections.",
  "",
  "## Reviews",
  "",
  "Reviewing my writing:",
  "",
  "1. Factual errors first, before anything else.",
  "2. Then hard-to-follow sentences, with shorter version for each.",
  "3. Style preferences last, five items max.",
  "",
  "Don't rewrite whole document unless I specifically ask.",
  "",
  "## Summaries",
  "",
  "Meeting notes: three things - decisions made, actions with owner and date, open questions. Summary fits on one screen.",
  "Example, planning meeting summary 2026-09-11:",
  "",
  "@@KEEP_01@@",
  "",
  "## Things I am working on",
  "",
  "- [x] Finish Q3 report for board",
  "- [ ] Rewrite onboarding guide for new contractors",
  "- [ ] Review style guide draft Tomasz shared in team folder at docs/style/house-style-v3.md"
 ],
 "changes": [
  {
   "kind": "merge",
   "before": "- Do not add a conclusion that restates the earlier sections in different words.",
   "after": "- No conclusion section repeating or restating earlier sections."
  },
  {
   "kind": "pleasantry",
   "before": "Sure! Here are my preferences. ",
   "after": ""
  },
  {
   "kind": "hedging",
   "before": "It would be good to avoid marketing language,",
   "after": "Avoid marketing language;"
  },
  {
   "kind": "fragment",
   "before": "1. Point out anything that is factually wrong first, before anything else.",
   "after": "1. Factual errors first, before anything else."
  },
  {
   "kind": "filler",
   "before": "For meeting notes, I basically want three things:",
   "after": "Meeting notes: three things -"
  },
  {
   "kind": "phrase",
   "before": "3. Leave style preferences until the very end, and keep them to five items at most.",
   "after": "3. Style preferences last, five items max."
  },
  {
   "kind": "article",
   "before": "- [x] Finish the Q3 report for the board",
   "after": "- [x] Finish Q3 report for board"
  }
 ],
 "at_risk": [
  {
   "before": "It would be good to avoid marketing language,",
   "note": "A soft preference now reads as a firm rule (\"Avoid\"); same for \"would really rather not\" becoming \"no\"."
  },
  {
   "before": "- Do not add a conclusion section that simply repeats what the document has already said.",
   "note": "Two conclusion bullets merged into one; the Drafts list now has one bullet fewer."
  }
 ],
 "headline": "Tone, Drafts, Reviews and Summaries cut to fragments; every heading, the code block marker, the date and docs/style/house-style-v3.md kept.",
 "summary": "Pleasantries, hedging, filler and articles were cut, and the two duplicate conclusion bullets were merged into one. Every heading, the @@KEEP_01@@ marker, the date 2026-09-11, the path docs/style/house-style-v3.md, the banned words and the todo checkboxes were kept exactly. Re-read the Tone section: soft wording such as \"would rather not\" now reads as a firm rule."
}

fix: a runbook compressed elsewhere that broke four things

Input (body and the compressed version shown separately):

{
 "task": "fix",
 "body": "(a string - shown below)",
 "compressed": "(a string - shown below)",
 "facts": "(a JSON string; decoded: 4 errors - E1 headings, E2 code, E3 urls, E4 inline; 2 markers)"
}

body:

# On-call notes for the payments API

## Before you change anything

You should always check the status page at https://status.payments.example first, because there is a good chance
that the incident has already been reported and that someone else is working on it. If there is already an open
incident, please join the channel `#inc-payments` instead of starting a new investigation.

## Restarting the API

In most cases, a restart of the API pods fixes the problem. The deployment is called `payments-api` and it runs in
the `prod` namespace. To restart it, run the following command:

@@KEEP_01@@

After the restart, it is really important to watch the error rate on the dashboard at
https://grafana.payments.example/d/payments-overview for at least ten minutes before you close the incident.

## Rolling back a release

If the problem started right after a release, it is generally better to roll back than to try to fix forward. The
release history is kept in `deploy/releases.yaml`, and each entry has a version number such as 3.18.2. To roll back,
run the rollback script with the previous version:

@@KEEP_02@@

The script will ask you to confirm. Make sure that you type the version number exactly, because the script does not
check it against the release history.

## Escalation

If the error rate is still above 2% after a restart and a rollback, page the payments lead through the escalation
policy called `payments-primary`. Write a short summary in the incident document that includes the time the problem
started, what you have tried, and what the error rate is now.

compressed:

# On-call notes for payments API

## Before you change anything

Check status page first - incident often already reported, someone already on it. Open incident: join `#inc-payments`,
don't start new investigation.

## Restart API

Restart of API pods fixes most problems. Deployment `payments-api`, runs in `prod` namespace. Restart:

    kubectl rollout restart deployment/payments-api -n dev

After restart, watch error rate on dashboard at https://grafana.payments.example/d/payments-overview for at least ten
minutes before closing incident.

## Rolling back a release

Problem started right after release: roll back, don't fix forward. Release history in `deploy/releases.yaml`, each
entry has version number like 3.18.2. Roll back with previous version:

@@KEEP_02@@

Script asks to confirm. Type version number exactly - script does not check it against release history.

## Escalation

Error rate still above 2% after restart and rollback: page payments lead via escalation policy. Write short summary in
incident document: time problem started, what tried, current error rate.

Reply:

{
 "task": "fix",
 "task_note": "",
 "fixed": [
  "# On-call notes for the payments API",
  "",
  "## Before you change anything",
  "",
  "Check status page at https://status.payments.example first - incident often already reported, someone already on it. Open incident: join `#inc-payments`,",
  "don't start new investigation.",
  "",
  "## Restarting the API",
  "",
  "Restart of API pods fixes most problems. Deployment `payments-api`, runs in `prod` namespace. Restart:",
  "",
  "@@KEEP_01@@",
  "",
  "After restart, watch error rate on dashboard at https://grafana.payments.example/d/payments-overview for at least ten",
  "minutes before closing incident.",
  "",
  "## Rolling back a release",
  "",
  "Problem started right after release: roll back, don't fix forward. Release history in `deploy/releases.yaml`, each",
  "entry has version number like 3.18.2. Roll back with previous version:",
  "",
  "@@KEEP_02@@",
  "",
  "Script asks to confirm. Type version number exactly - script does not check it against release history.",
  "",
  "## Escalation",
  "",
  "Error rate still above 2% after restart and rollback: page payments lead via escalation policy `payments-primary`. Write short summary in",
  "incident document: time problem started, what tried, current error rate."
 ],
 "repairs": [
  {
   "id": "E1",
   "action": "Restored heading lines \"# On-call notes for the payments API\" (line 1) and \"## Restarting the API\" (line 8) exactly as in body."
  },
  {
   "id": "E2",
   "action": "Replaced the raw indented kubectl block at line 12 with marker @@KEEP_01@@, which stands at that place in body; the page puts the original block back."
  },
  {
   "id": "E3",
   "action": "Put https://status.payments.example back into the first sentence under \"## Before you change anything\" (\"Check status page at ... first\")."
  },
  {
   "id": "E4",
   "action": "Restored inline code `payments-primary` as the escalation policy name in the first sentence under \"## Escalation\"."
  }
 ],
 "headline": "Restored 2 headings, the @@KEEP_01@@ marker in place of an altered kubectl block, the status-page URL and `payments-primary`.",
 "summary": "All four validator failures are repaired; every other line is unchanged. The raw kubectl block in the compressed version had been altered (it targets -n dev, while body says the deployment runs in `prod`), so it was swapped for @@KEEP_01@@ and the page will restore the original command."
}

The page re-checks the reply; the API does not

The page puts every reply back into the whole file and holds it to account (recon.js): every code block marker exactly once, or it refuses to guess; the skill's validator over the result - headings, code blocks, URLs, file paths, inline code; shorter than the original; environment names, versions and dates kept; every quoted rewrite present in the original and the result; at most five at-risk sentences quoted from the original; no savings figure claimed by the reply. The fix lane adds one repair per error in order and a cap on lines rewritten that no error touched. 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 file 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://rulecut.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"}
}

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":"rulecut"}' | jq -r '.data.token')

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

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": "compress", "body": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

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 rulecut:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="rulecut:fix:$(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}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events. fix.json is a run input like input.json.

# 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: rulecut:fix:$(shasum -a 256 fix.json | cut -c1-16):a1" --data @fix.json