API reference
One endpoint scores one image or one text against a formula and returns a calibrated score and a boolean. This page covers every endpoint, limit, and error, plus setup for agent harnesses.
https://mogger.dev/api/v1Introduction
Mogger is System 1 preferences for AI harnesses. A human tunes a preference once: a formula, which is one yes or no question about one input, and a pass/fail boundary kept on the server. An agent harness (Claude Code, a Grok bot, any workflow) then calls it at high volume, low cost, and low latency, and gets back a calibrated score and a boolean, with no pushback from a sassy System 2 model.
System 1 is the fast, intuitive mode of thought and System 2 the slow, deliberate one. Mogger hands an agent the first kind, tuned by a person.
Each call is a single forward pass of a decision model. No tokens are generated, so the answer is a probability, not prose. A response carries noul, the probability from 0 to 1 that the answer is yes, and mogs, the verdict. The boundary between the two verdicts is set on the server for each formula and may change. It is never returned, so an agent has nothing to argue with.
Every formula judges one input on its own. None compares two inputs: to choose between candidates, score each one and sort by noul.
The API speaks JSON over HTTPS. CORS is open on /api/v1, every response carries a request id, and every authenticated response reports what is left of the key's quota.

curl https://mogger.dev/api/v1/mog \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"formula": "bio-mog",
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined."
}'const res = await fetch("https://mogger.dev/api/v1/mog", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
formula: "bio-mog",
text: "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
const { mogs, noul } = data.results["bio-mog"];
console.log(mogs, noul);import os
import requests
res = requests.post(
"https://mogger.dev/api/v1/mog",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={
"formula": "bio-mog",
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
result = data["results"]["bio-mog"]
print(result["mogs"], result["noul"])# With the mogger skill installed, ask in plain words, for example:
# Score this with mogger (bio-mog): I restore 1970s synthesizers for a living. 312 repaired,...
# The skill sources its helpers and runs:
score_text bio-mog 'I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.' | jq '.results'Authentication
Scoring calls and custom formula calls need an API key, sent as a bearer token in the Authorization header. Listing and reading built-in formulas needs none.
Keys are invite only. Email [email protected] to ask for one. Check a key and its quota at any time with GET /api/v1/me, which is free.
A key is mog_ followed by 32 letters and digits. Mogger stores only its SHA-256 hash, so the key is shown once, when it is created, and a lost key cannot be recovered. Ask for a rotation instead: it revokes the old key and issues a new one with the same label.
Keep keys off the client
MOGGER_API_KEY.Authorization: Bearer mog_...{
"error": {
"code": "unauthorized",
"message": "Missing API key. Send Authorization: Bearer mog_..."
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}Quotas and cost
Each key has a lifetime budget of units, 1,000 unless it was issued with another. The budget never resets.
| Request | Units |
|---|---|
| Score text | 1 |
| Score an image (with or without text) | 10 |
| Create, edit, list, or delete a custom formula | 0 |
| Read formulas, check a key | 0 |
- A request is charged once, however many formulas it names. Batch up to 8 formulas for one input into one call.
- Only success is charged. A request that fails validation costs nothing, and if the model call fails the units are refunded (502
upstream_error). - When the remaining budget cannot cover a request, the API answers 402
quota_exhaustedand charges nothing. The body carries the quota, so an image request can fail while a text request still fits. - A spent key stays spent until it is rotated. Rotation issues a new key with a fresh budget and the same label, and your custom formulas move with it.
- There is no rate limit. The quota is the only brake, so plan batches and cache results by formula, version, and a hash of the input.
HTTP/1.1 402 Payment Required
X-Mogger-Request-Id: req_...
X-Mogger-Quota-Remaining: 4
X-Mogger-Quota-Budget: 1000{
"quota": {
"remaining": 4,
"budget": 1000
},
"error": {
"code": "quota_exhausted",
"message": "This key has 4 of 1000 units left and this request costs 10. Ask for a rotated key"
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}Score an input
/api/v1/mogKey required1 or 10 unitsScores one input against 1 to 8 formulas in a single forward pass and returns one result per formula. The request is charged once.
Body parameters
formulastringOne of formula or formulas- One formula: a built-in slug such as
bio-mog, or a custom formula id such asf_7Kq2xW9pLm. formulasstring[]One of formula or formulas- 1 to 8 unique slugs or ids that share one input type. Charged once, like a single formula.
textstringRequired by text formulas- Up to 10,000 characters, counted as Unicode code points after leading and trailing whitespace is trimmed. With image formulas it is optional context sent along with the image.
imagestringRequired by image formulas- A base64 data URL such as
data:image/png;base64,.... PNG, JPEG, or WebP, at most 3 MiB (3,145,728 bytes) decoded, and the bytes must match the declared type. URLs are not fetched. Text formulas reject it.
Input rules
- Every formula in a request shares one input type. Mixing image and text formulas returns 400
invalid_request. - An image formula without
image, or a text formula withimageor withouttext, returns 400input_mismatch. Text formulas refuse images so a text judgment is never billed at the image rate. - Unknown fields are rejected, and a field set to
nullcounts as absent. - A body over 4,400,000 bytes is rejected with 413
payload_too_largebefore it is read. - A request that fails any of these checks is never charged.
curl https://mogger.dev/api/v1/mog \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"formula": "bio-mog",
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined."
}'const res = await fetch("https://mogger.dev/api/v1/mog", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
formula: "bio-mog",
text: "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
const { mogs, noul } = data.results["bio-mog"];
console.log(mogs, noul);import os
import requests
res = requests.post(
"https://mogger.dev/api/v1/mog",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={
"formula": "bio-mog",
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
result = data["results"]["bio-mog"]
print(result["mogs"], result["noul"])# With the mogger skill installed, ask in plain words, for example:
# Score this with mogger (bio-mog): I restore 1970s synthesizers for a living. 312 repaired,...
# The skill sources its helpers and runs:
score_text bio-mog 'I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.' | jq '.results'{
"results": {
"bio-mog": {
"mogs": true,
"noul": 0.94,
"version": 1
}
},
"cost": 1,
"quota": {
"remaining": 999,
"budget": 1000
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}type MogResponse = {
/** One entry per formula, keyed by the slug or id you sent. */
results: Record<string, MogResult>;
/** Units charged: 1 for text, 10 with an image. */
cost: number;
quota: { remaining: number; budget: number };
request_id: string;
};
type MogResult = {
/** The verdict. */
mogs: boolean;
/** Calibrated probability that the answer is yes, 0 to 1. */
noul: number;
/** Formula revision. Scores from different versions are not comparable. */
version: number;
};Score an image with several formulas
Name every formula you want for one image in formulas. The image is sent to the model once, and the request costs 10 units whether it names one formula or 8. Results come back keyed by slug.
Send the image inline as a base64 data URL. Downscale large photos to about 1024 px on the long side before encoding: it keeps the body small and well under the 3 MiB (3,145,728 bytes) cap. In a shell, build the body in a file, since a base64 image is too long for a command line argument.
To score many images, send one request per image. To score many strings, send one request per string at 1 unit each.
# Build the body in a file: a base64 image is too long for a shell argument.
# For a PNG or WebP file, change image/jpeg to image/png or image/webp.
{
printf '{"formulas":["jawline","canthal-tilt"],"image":"data:image/jpeg;base64,'
base64 < photo.jpg | tr -d '\n'
printf '"}'
} > body.json
curl https://mogger.dev/api/v1/mog \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @body.json// Node 18 or later, as an ES module (mog.mjs). Use image/png or image/webp for other file types.
import { readFile } from "node:fs/promises";
const image = "data:image/jpeg;base64," + (await readFile("photo.jpg")).toString("base64");
const res = await fetch("https://mogger.dev/api/v1/mog", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ formulas: ["jawline", "canthal-tilt"], image }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
for (const [slug, r] of Object.entries(data.results)) {
console.log(slug, r.mogs, r.noul);
}import base64
import os
import requests
with open("photo.jpg", "rb") as f:
image = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode("ascii")
res = requests.post(
"https://mogger.dev/api/v1/mog",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={"formulas": ["jawline", "canthal-tilt"], "image": image},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
for slug, r in data["results"].items():
print(slug, r["mogs"], r["noul"])# With the mogger skill installed, ask in plain words, for example:
# Does ./photo.jpg mog on jawline and canthal-tilt?
# The skill base64-encodes the file in the shell, so the image
# never enters the model context, and runs:
score_image ./photo.jpg jawline,canthal-tilt | jq '.results'{
"results": {
"jawline": {
"mogs": true,
"noul": 0.91,
"version": 1
},
"canthal-tilt": {
"mogs": false,
"noul": 0.08,
"version": 1
}
},
"cost": 10,
"quota": {
"remaining": 989,
"budget": 1000
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}The response
resultsobject- One entry per formula, keyed by the slug or id you sent.
mogsboolean- The verdict.
truemeans the input mogs. noulnumber- Calibrated probability, from 0 to 1, that the answer to the formula's question is yes. Use it to rank candidates or to flag close calls for a human.
versioninteger- The formula revision that produced the score. It changes when the formula's question changes, and scores from different versions are not comparable.
costinteger- Units charged: 1 without an image, 10 with one.
quotaobjectremainingandbudgetfor the key, after this request.request_idstringreq_plus 24 characters. Quote it when reporting a problem.
The boundary stays on the server
mogs compares noul with a pass/fail boundary set on the server for each formula. The boundary is never returned and may change, so the same score can carry a different verdict later. Treat mogs as the decision.Response headers
X-Mogger-Request-Id: The request id, also in the body as request_id. Quote it when reporting a problem.X-Mogger-Quota-Remaining: Units left on the key after this request.X-Mogger-Quota-Budget: The key's lifetime budget in units.Cache-Control: no-store. Scoring responses are never cached.
type MogRequest = {
/** One slug or custom id. Send this or formulas. */
formula?: string;
/** 1 to 8 unique slugs or ids, all with the same input type. */
formulas?: string[];
/** Up to 10,000 characters. */
text?: string;
/** "data:image/png;base64,...", PNG, JPEG, or WebP, 3 MiB decoded at most. */
image?: string;
};
type MogResponse = {
/** One entry per formula, keyed by the slug or id you sent. */
results: Record<string, MogResult>;
/** Units charged: 1 for text, 10 with an image. */
cost: number;
quota: { remaining: number; budget: number };
request_id: string;
};
type MogResult = {
/** The verdict. */
mogs: boolean;
/** Calibrated probability that the answer is yes, 0 to 1. */
noul: number;
/** Formula revision. Scores from different versions are not comparable. */
version: number;
};{
"error": {
"code": "input_mismatch",
"message": "bio-mog is a text formula and cannot take an image. Remove image"
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}Score one formula by path
/api/v1/mog/{idOrSlug}Key required1 or 10 unitsScores one formula named in the path: a custom formula id from POST /api/v1/formulas, or a built-in slug such as bio-mog. The url field of a custom formula points here.
textstringRequired by text formulas- Same rules as
POST /api/v1/mog. imagestringRequired by image formulas- Same rules as
POST /api/v1/mog.
A formula or formulas field in this body is 400 invalid_request: the path already names the formula. The response has the same shape and headers as POST /api/v1/mog, with results keyed by the id or slug. To batch a custom formula with others, put its id in formulas instead. See the formula gallery for built-in slugs.
curl https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined."
}'const res = await fetch("https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.results["f_7Kq2xW9pLm"]);import os
import requests
res = requests.post(
"https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={
"text": "I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.",
},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["results"]["f_7Kq2xW9pLm"])# With the mogger skill installed, a custom id works anywhere a slug does:
score_text f_7Kq2xW9pLm 'I restore 1970s synthesizers for a living. 312 repaired, 4 ruined.' | jq '.results'List formulas
/api/v1/formulasNo keyFreeReturns every built-in formula in display order as { "formulas": [...] }. Custom formulas are never listed here. Responses may be cached for a few minutes.
The formula object
idstring- Same as
slugfor built-in formulas. Custom formulas use anf_id. kindstringbuiltinhere. Custom formulas arecustom.slugstring- What you send as
formula. Stable. name, tagline, descriptionstring- What the formula judges, in plain words.
categorystringFace,Physique,Style, orText.inputstringimageortext: which input the formula requires.lookForstring[]- The features the formula weighs, 3 to 5 short lines.
versioninteger- Current revision. Matches
versionin scoring results. examplesobject[]- Demo inputs. Image examples carry
src(a path on this site),alt,credit,license, andsourceUrl. Text examples carrytext.bakedis a score computed ahead of time,{ noul, mogs }, ornullwhen the example is not scored yet.
Built-in formulas
| Slug | Name | Input |
|---|---|---|
| jawline | Jawline | Image input |
| canthal-tilt | Canthal Tilt | Image input |
| hunter-eyes | Hunter Eyes | Image input |
| hairline | Hairline | Image input |
| skin | Skin | Image input |
| overall-face | Overall Face | Image input |
| fit-check | Fit Check | Image input |
| physique | Physique | Image input |
| bio-mog | Bio Mog | Text input |
| tweet-mog | Tweet Mog | Text input |
| resume-mog | Resume Mog | Text input |
| codebase-mog | Codebase Mog | Text input |
Each formula has a page with its examples, for instance Jawline.
curl https://mogger.dev/api/v1/formulas \
| jq -r '.formulas[] | [.slug, .input, .name] | @tsv'const res = await fetch("https://mogger.dev/api/v1/formulas");
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
for (const f of data.formulas) console.log(f.slug, f.input, f.name);import requests
res = requests.get(
"https://mogger.dev/api/v1/formulas",
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
for f in data["formulas"]:
print(f["slug"], f["input"], f["name"]){
"formulas": [
{
"id": "jawline",
"kind": "builtin",
"slug": "jawline",
"name": "Jawline",
"tagline": "Does the jaw draw one clean, defined line from ear to chin?",
"description": "Scores the lower third of a face: how sharply the jaw is defined, how cleanly it separates from the neck, and how well the chin projects. A beard or camera angle counts only for what it lets the model see.",
"category": "Face",
"input": "image",
"lookFor": [
"A visible jaw edge running from ear to chin",
"A defined angle at the back of the jaw",
"Forward chin projection that balances the face",
"A clean break between the jaw and the neck"
],
"version": 1,
"examples": [
{
"kind": "image",
"id": "david",
"label": "Michelangelo's David, plaster cast",
"src": "/examples/david-head-cast.jpg",
"alt": "Plaster cast of the head of Michelangelo's David, three-quarter view facing right, on a black background",
"credit": "Michelangelo (sculptor). Plaster cast, Statens Museum for Kunst",
"license": "Public domain",
"sourceUrl": "https://commons.wikimedia.org/wiki/File:Michelangelo_Buonarroti,_Hoved_fra_statuen_af_David,_,_KAS2232,_Statens_Museum_for_Kunst.jpg",
"baked": {
"noul": 0.8927,
"mogs": true
}
}
]
}
]
}type ListFormulasResponse = { formulas: Formula[] };
type Formula = {
/** Same as slug for built-in formulas. */
id: string;
kind: "builtin";
slug: string;
name: string;
tagline: string;
description: string;
category: "Face" | "Physique" | "Style" | "Text";
input: "image" | "text";
lookFor: string[];
version: number;
examples: Example[];
};
type Example =
| { kind: "text"; id: string; label: string; text: string; baked: Baked }
| {
kind: "image";
id: string;
label: string;
/** Path on the site, such as "/examples/david-head-cast.jpg". */
src: string;
alt: string;
credit: string;
license: string;
sourceUrl: string;
baked: Baked;
};
/** A score computed ahead of time, or null when not scored yet. */
type Baked = { noul: number; mogs: boolean } | null;Retrieve a formula
/api/v1/formulas/{idOrSlug}No key for built-insFreeReturns one built-in formula as { "formula": {...} }, the same formula object that List formulas returns. An unknown slug returns 404 not_found.
A custom formula id needs a key, and comes back in the same { "formula": {...} } wrapper. The creator's key gets the full creator view, with prompt and criteria. Any other key gets the public view without them. Without a key, a custom id returns 404 not_found, as if it did not exist. See Custom formulas.
curl https://mogger.dev/api/v1/formulas/jawlineconst res = await fetch("https://mogger.dev/api/v1/formulas/jawline");
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.formula.input, data.formula.lookFor);import requests
res = requests.get(
"https://mogger.dev/api/v1/formulas/jawline",
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["formula"]["input"], data["formula"]["lookFor"]){
"error": {
"code": "not_found",
"message": "No formula with slug \"jawlin\". GET /api/v1/formulas lists valid slugs"
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}Custom formulas
Any valid key can capture a preference as its own formula and call it by id. Write the question once, then let an agent apply it at volume. The create page does the same in the browser: write, test, edit, and delete your formulas with your key.
- The id is
f_plus 10 random letters and digits, such asf_7Kq2xW9pLm. It works anywhere a slug does: informula, informulasnext to built-in formulas of the same input type (8 at most, charged once), and in POST /api/v1/mog/{id}. - Custom formulas are unlisted: never in the gallery,
llms.txt, the sitemap, orGET /api/v1/formulas. The id acts as a capability. Anyone who holds it can score with it, billed to their own key, so share it on purpose. - The
promptandcriteriaare returned only to the key that created the formula. Everyone else sees the title, input, and version. Once the creator locks the formula, no key sees them again, the creator included. - The global pass/fail boundary applies. Creators never see or set it.
- Creating, editing, listing, and deleting cost 0 units. Scoring costs the same as a built-in formula.
- A key can own 25. Rotating the key keeps ownership, and a formula keeps working for other keys if its creator's key is revoked.
Writing a prompt
{
"id": "f_7Kq2xW9pLm",
"object": "formula",
"kind": "custom",
"title": "Clean product photo",
"input": "image",
"prompt": "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?",
"criteria": {
"true": "One product on a plain background, sharp and evenly lit.",
"false": "Cluttered, dim, off-center, or more than one subject."
},
"locked": false,
"locked_at": null,
"version": 1,
"url": "https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
"created_at": "2026-11-07T18:20:00.000Z",
"updated_at": "2026-11-07T18:20:00.000Z"
}type CustomFormula = {
/** "f_" plus 10 letters and digits. */
id: string;
object: "formula";
kind: "custom";
/** Display only. Never sent to the model. */
title: string;
input: "image" | "text";
/** Creator only, and absent for every key once locked. */
prompt?: string;
/** Creator only, and absent for every key once locked. */
criteria?: { true: string; false: string } | null;
/** True once the creator locks it. Locking is one way. */
locked: boolean;
/** When it was locked, or null. */
locked_at: string | null;
version: number;
/** POST { text } or { image } here to score. */
url: string;
created_at: string;
updated_at: string;
};{
"id": "f_7Kq2xW9pLm",
"object": "formula",
"kind": "custom",
"title": "Clean product photo",
"input": "image",
"locked": false,
"locked_at": null,
"version": 1,
"url": "https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
"created_at": "2026-11-07T18:20:00.000Z",
"updated_at": "2026-11-07T18:20:00.000Z"
}Create a custom formula
/api/v1/formulasKey requiredFreetitlestringRequired- 1 to 80 characters. Display only: never sent to the model.
promptstringRequired- 10 to 2,000 characters. The yes or no question the model answers about each input.
inputstringRequiredtextorimage.criteriaobject- Optional. One or two sentences for each pole sharpen the judgment.
truestringRequired- What a passing input looks like, up to 500 characters.
falsestringRequired- What a failing input looks like, up to 500 characters.
Returns 201 with the creator view, including id and url. A key that already owns 25 custom formulas gets 409 formula_limit_reached. Test the new formula on a few examples you already have a verdict for before using it at volume.
curl https://mogger.dev/api/v1/formulas \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Clean product photo",
"prompt": "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?",
"input": "image",
"criteria": {
"true": "One product on a plain background, sharp and evenly lit.",
"false": "Cluttered, dim, off-center, or more than one subject."
}
}'const res = await fetch("https://mogger.dev/api/v1/formulas", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Clean product photo",
prompt: "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?",
input: "image",
criteria: {
true: "One product on a plain background, sharp and evenly lit.",
false: "Cluttered, dim, off-center, or more than one subject.",
},
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.id, data.url);import os
import requests
res = requests.post(
"https://mogger.dev/api/v1/formulas",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={
"title": "Clean product photo",
"prompt": "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?",
"input": "image",
"criteria": {
"true": "One product on a plain background, sharp and evenly lit.",
"false": "Cluttered, dim, off-center, or more than one subject.",
},
},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["id"], data["url"]){
"id": "f_7Kq2xW9pLm",
"object": "formula",
"kind": "custom",
"title": "Clean product photo",
"input": "image",
"prompt": "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?",
"criteria": {
"true": "One product on a plain background, sharp and evenly lit.",
"false": "Cluttered, dim, off-center, or more than one subject."
},
"locked": false,
"locked_at": null,
"version": 1,
"url": "https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
"created_at": "2026-11-07T18:20:00.000Z",
"updated_at": "2026-11-07T18:20:00.000Z"
}List your custom formulas
/api/v1/formulas/mineKey requiredFreeReturns the custom formulas your key created, newest first, as { "formulas": [...] }. Each entry is the full creator view shown in Custom formulas, except that a locked entry has no prompt and no criteria. Formulas created before a key rotation are included.
curl https://mogger.dev/api/v1/formulas/mine \
-H "Authorization: Bearer $MOGGER_API_KEY"const res = await fetch("https://mogger.dev/api/v1/formulas/mine", {
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
for (const f of data.formulas) console.log(f.id, f.input, f.title);import os
import requests
res = requests.get(
"https://mogger.dev/api/v1/formulas/mine",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
for f in data["formulas"]:
print(f["id"], f["input"], f["title"])Update a custom formula
/api/v1/formulas/{id}Creator keyFreeSend only the fields to change: title, prompt, input, or criteria, with the same limits as on create. Set criteria to null to remove it. Returns the updated creator view.
Changing prompt, input, or criteria bumps version, because scores from the old question are no longer comparable. A title edit does not. Another key gets 403 forbidden, and an unknown id 404 not_found.
On a locked formula only title can change. A body with prompt, input, or criteria gets 403 forbidden.
curl -X PATCH https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm \
-H "Authorization: Bearer $MOGGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Is this a clean product photo with exactly one product, centered, on a plain background?"
}'const res = await fetch("https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "Is this a clean product photo with exactly one product, centered, on a plain background?",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.version);import os
import requests
res = requests.patch(
"https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
json={
"prompt": "Is this a clean product photo with exactly one product, centered, on a plain background?",
},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["version"]){
"id": "f_7Kq2xW9pLm",
"object": "formula",
"kind": "custom",
"title": "Clean product photo",
"input": "image",
"prompt": "Is this a clean product photo with exactly one product, centered, on a plain background?",
"criteria": {
"true": "One product on a plain background, sharp and evenly lit.",
"false": "Cluttered, dim, off-center, or more than one subject."
},
"locked": false,
"locked_at": null,
"version": 2,
"url": "https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
"created_at": "2026-11-07T18:20:00.000Z",
"updated_at": "2026-11-07T18:41:00.000Z"
}Lock a custom formula
/api/v1/formulas/{id}/lockCreator keyFreeOne wayLocks the formula for good. No body. Returns 200 with the creator view, now with locked set to true, locked_at set to the time of the lock, and no prompt or criteria keys. Locking a formula that is already locked returns the same view.
- From then on no response carries the
promptorcriteria, to any key, the creator included. Use it before you share an id, even inside a team that shares one key. - Scoring by id keeps working for every key, with the same
version, so scores stay comparable. - The
titlecan still change and the formula can still be deleted. Theprompt,input, andcriteriacan never change again: create a new formula to change the question. - There is no unlock. Keep your own copy of the prompt first, because it cannot be read back.
Only the creator's key can lock: another key or a built-in slug gets 403 forbidden, a missing key 401 unauthorized, and an unknown id 404 not_found.
curl -X POST https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm/lock \
-H "Authorization: Bearer $MOGGER_API_KEY"const res = await fetch("https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm/lock", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.locked, data.locked_at);import os
import requests
res = requests.post(
"https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm/lock",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["locked"], data["locked_at"]){
"id": "f_7Kq2xW9pLm",
"object": "formula",
"kind": "custom",
"title": "Clean product photo",
"input": "image",
"locked": true,
"locked_at": "2026-11-07T19:05:00.000Z",
"version": 1,
"url": "https://mogger.dev/api/v1/mog/f_7Kq2xW9pLm",
"created_at": "2026-11-07T18:20:00.000Z",
"updated_at": "2026-11-07T19:05:00.000Z"
}Delete a custom formula
/api/v1/formulas/{id}Creator keyFreeDeletes the formula for good and returns { "id": "...", "deleted": true }. Every key that used the id loses it: later calls return 400 unknown_formula. Only the creator's key can delete (403 forbidden otherwise).
curl -X DELETE https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm \
-H "Authorization: Bearer $MOGGER_API_KEY"const res = await fetch("https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.deleted);import os
import requests
res = requests.delete(
"https://mogger.dev/api/v1/formulas/f_7Kq2xW9pLm",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["deleted"]){
"id": "f_7Kq2xW9pLm",
"deleted": true
}Check a key
/api/v1/meKey requiredFreeReturns the key's label and quota. It is never charged, so check it before a large batch. A revoked key still gets an answer, with revoked: true, so its holder can see why calls fail.
labelstring- The name the key was issued under.
prefixstring- The first 8 characters of the key, safe to log.
budget, used, remaininginteger- Lifetime units: the total, what has been spent, and what is left.
revokedbooleantrueonce the key is revoked or replaced by a rotation.created_at, last_used_atstring- ISO 8601 timestamps.
last_used_atisnulluntil the first successful call.
curl https://mogger.dev/api/v1/me \
-H "Authorization: Bearer $MOGGER_API_KEY"const res = await fetch("https://mogger.dev/api/v1/me", {
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(`${data.remaining} of ${data.budget} units left`);import os
import requests
res = requests.get(
"https://mogger.dev/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['MOGGER_API_KEY']}"},
timeout=60,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(f"{data['remaining']} of {data['budget']} units left")# The mogger skill checks the key and quota with:
api GET /api/v1/me | jq '{label, budget, used, remaining, revoked}'{
"label": "build-bot",
"prefix": "mog_a1B2",
"budget": 1000,
"used": 37,
"remaining": 963,
"revoked": false,
"created_at": "2026-11-07T18:20:00.000Z",
"last_used_at": "2026-11-07T19:02:11.000Z"
}type KeyStatus = {
label: string;
/** First 8 characters of the key, such as "mog_a1B2". */
prefix: string;
budget: number;
used: number;
remaining: number;
revoked: boolean;
/** ISO 8601. */
created_at: string;
/** null until the first successful call. */
last_used_at: string | null;
};Errors
Errors use HTTP status codes and a JSON body with a stable code and a readable message. Branch on code: messages may change. Every error body carries request_id, and a 402 also carries quota.
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | invalid_request | The body is not a JSON object, has an unknown field, names no formula or more than 8, repeats a formula, mixes image and text formulas, or breaks a limit. Also sent with status 405 when the method is wrong for the path. | Fix the request. Sending it again unchanged returns the same error. |
| 400 | input_mismatch | An image formula got no image, or a text formula got an image or no text. | Match the input to each formula's input field. |
| 400 | unknown_formula | A slug or custom id in formula or formulas does not exist. | List valid slugs with GET /api/v1/formulas. |
| 401 | unauthorized | The Authorization header is missing, malformed, or names an unknown key. | Send Authorization: Bearer mog_... |
| 401 | key_revoked | The key was revoked, or replaced by a rotation. | Ask for a new key. |
| 402 | quota_exhausted | The key's lifetime budget cannot cover this request. The body also carries quota. | Stop. Ask for a rotated key. A cheaper text request may still fit. |
| 403 | forbidden | A key other than the creator's tried to edit or delete a custom formula. | Use the key that created the formula. |
| 404 | not_found | No endpoint at that path, no formula with that slug, or a custom id requested without a key. | Check the path. Send a key to read a custom formula. |
| 409 | formula_limit_reached | The key already owns 25 custom formulas. | Delete one before creating another. |
| 413 | payload_too_large | The body is over 4,400,000 bytes, or the decoded image is over 3 MiB (3,145,728 bytes). | Downscale the image. 1024 px on the long side is plenty. |
| 500 | internal_error | An unexpected fault on the server. | Retry once after a short wait, then report the request_id. |
| 502 | upstream_error | The scoring model call failed. Nothing is charged: the units are refunded. | Retry with backoff. |
Retrying
- Never retry a 4xx response unchanged. It will fail the same way. A 402 means stop and ask for a rotated key.
- Retry 502
upstream_errorwith backoff. It was not charged. - Retry 500
internal_erroronce, then report therequest_id. - A method the path does not support returns 405 with code
invalid_requestand anAllowheader.
{
"quota": {
"remaining": 4,
"budget": 1000
},
"error": {
"code": "quota_exhausted",
"message": "This key has 4 of 1000 units left and this request costs 10. Ask for a rotated key"
},
"request_id": "req_0MHmB7xKQ2c9tVwLp4ZsYd3R"
}type MoggerError = {
error: { code: ErrorCode; message: string };
request_id: string;
/** Present on 402 quota_exhausted. */
quota?: { remaining: number; budget: number };
};
type ErrorCode =
| "invalid_request"
| "input_mismatch"
| "unknown_formula"
| "unauthorized"
| "key_revoked"
| "quota_exhausted"
| "forbidden"
| "not_found"
| "formula_limit_reached"
| "payload_too_large"
| "internal_error"
| "upstream_error";Install the skill
One file, SKILL.md, teaches Claude Code and Codex to call Mogger. It drives every endpoint with curl and jq, and encodes images in the shell, so image bytes never enter the model's context. There is no MCP server and no npm package to install.
The installer is one command. It does three things:
- It installs the skill to
~/.claude/skills/mogger/SKILL.mdand~/.codex/skills/mogger/SKILL.md. When a different file is already there, it is kept asSKILL.md.bakand the installer says so. - It reads the key from
MOGGER_API_KEY, or asks for it without echoing, and saves it with owner only permissions to${XDG_CONFIG_HOME:-$HOME/.config}/mogger/key. - It checks the key with
GET /api/v1/meand prints the label and units left. It warns whenjqis missing and never edits your shell startup files.
To install by hand, download the one file into either skills folder and set MOGGER_API_KEY. The skill looks for the key in that variable first, then in the key file.
Read before you pipe
sh runs it as you. The installer is plain POSIX sh: open mogger.dev/install.sh to read it first.curl -fsSL https://mogger.dev/install.sh | sh# Claude Code
mkdir -p ~/.claude/skills/mogger
curl -fsSL https://mogger.dev/skills/mogger/SKILL.md -o ~/.claude/skills/mogger/SKILL.md
# Codex
mkdir -p ~/.codex/skills/mogger
curl -fsSL https://mogger.dev/skills/mogger/SKILL.md -o ~/.codex/skills/mogger/SKILL.md
# The key: an environment variable, or the file the installer writes
export MOGGER_API_KEY=mog_...curl -fsSL https://mogger.dev/install.sh | sh -s -- --uninstallUsing Mogger from Claude Code
With the skill installed, ask in plain words: does this headshot mog on jawline, which of these bios mog, sort this folder of photos by fit check. The skill picks formulas whose input matches, batches up to 8 per request, prints a summary table, and stops on a 402. It treats the boolean as final instead of reasoning its way around it.
To make Mogger a project habit, add the block on the right to the project's CLAUDE.md. Codex reads the same text from AGENTS.md.
When a person keeps making the same kind of call by hand (sorting photos, picking names, triaging candidates), the skill offers once to capture that preference as a custom formula, then tests it on examples the person already judged.
## Mogger
For fast yes or no judgments of an image or a piece of text (does this photo, bio, or draft pass?), use the mogger skill instead of judging it yourself.
- Pick formulas whose input matches: image formulas take an image, text formulas take text. The list is at https://mogger.dev/llms.txt.
- Batch up to 8 formulas for one input into one request. Text costs 1 unit, an image 10.
- Use `mogs` as the decision and `noul` (0 to 1) to rank. Do not argue with a verdict or rerun a call to change it.
- Never print the key. Never retry a 4xx. On a 402, stop and tell me the key needs a rotation.Other harnesses
Any harness with function calling can use Mogger as a tool: a Grok bot, an OpenAI or Anthropic agent loop, a workflow engine. Give the model a tool definition, and have your code make the HTTP call.
- Let the model name a file path, not paste image data. Your handler reads and encodes the file, which keeps the model's context small and the image out of it.
- Return only
resultsto the model. The verdict is the answer; there is nothing to deliberate. - The definition uses JSON Schema. Anthropic's Messages API calls the schema field
input_schema. OpenAI style APIs nest it asfunction.parameters. - To offer a custom formula, add its id to the
enum.
{
"name": "mogger_score",
"description": "Fast, calibrated yes or no judgment of one image or one text against Mogger formulas. Returns mogs (the verdict) and noul (0 to 1) for each formula. Use mogs as the decision.",
"parameters": {
"type": "object",
"properties": {
"formulas": {
"type": "array",
"description": "1 to 8 formulas that share one input type. Image formulas: jawline, canthal-tilt, hunter-eyes, hairline, skin, overall-face, fit-check, physique. Text formulas: bio-mog, tweet-mog, resume-mog, codebase-mog.",
"items": {
"type": "string",
"enum": ["jawline", "canthal-tilt", "hunter-eyes", "hairline", "skin", "overall-face", "fit-check", "physique", "bio-mog", "tweet-mog", "resume-mog", "codebase-mog"]
},
"minItems": 1,
"maxItems": 8,
"uniqueItems": true
},
"text": {
"type": "string",
"maxLength": 10000,
"description": "The text to judge. Required for text formulas."
},
"image_path": {
"type": "string",
"description": "Path to a PNG, JPEG, or WebP file to judge. Required for image formulas."
}
},
"required": ["formulas"]
}
}import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
// Runs when the model calls mogger_score. The model names a file; this code
// reads and encodes it, so image bytes never enter the model's context.
export async function moggerScore({ formulas, text, image_path }) {
const body = { formulas };
if (text) body.text = text;
if (image_path) {
const mime = MIME[extname(image_path).toLowerCase()];
if (!mime) return { error: "unsupported_image_type" };
body.image = `data:${mime};base64,${(await readFile(image_path)).toString("base64")}`;
}
const res = await fetch("https://mogger.dev/api/v1/mog", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOGGER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) return { error: data.error.code, message: data.error.message };
return data.results;
}Machine-readable docs
/llms.txt, /ai.txt, and /si.txt serve the same plain text, byte for byte: the contract, costs, limits, error codes, the custom formula endpoints, and every built-in formula with its slug and input type. Point an agent at any of them.
/api/v1/formulas returns the formula list as JSON, and /skills/mogger/SKILL.md is the agent skill.
curl https://mogger.dev/llms.txt