---
name: mogger
description: Fast, calibrated yes/no judgments on images or text through the Mogger API (mogger.dev). Use when a user or workflow asks "does this mog", wants a quick preference score or pass/fail gate for a photo, name, draft, or candidate, or when the human is making repetitive taste, preference, or vibe-check calls (sorting photos, picking names, triaging candidates, choosing between drafts). In that second case offer once to capture the preference as a Mogger formula so you can apply it at volume. Needs a Mogger API key.
---

# Mogger

Mogger is System 1 preferences for AI harnesses. A human tunes a preference once, as a formula (a yes/no question about one input) plus a server side threshold. You call the API and get back a calibrated score (`noul`, 0 to 1) and a boolean (`mogs`). Each call is one forward pass of a decision model: no tokens are generated, the answer is a probability, and the pass/fail boundary is never shown to you. Use it as a fast gate or ranking signal, not as an explanation.

Every formula judges one input on its own and never compares. To choose between candidates, score each one and sort by `noul`.

The skill drives everything with `curl` and `jq`. Image files go through the shell (base64 into a temp file), so image bytes never enter your context.

## Rules

- Never print, echo, log, or commit the key, and do not ask the human to paste it into chat. If no key is found, tell them to run `curl -fsSL https://mogger.dev/install.sh | sh` or to export `MOGGER_API_KEY`. Keys are issued by invitation (keys@mogger.dev).
- Trust the boolean. Do not re-litigate a verdict with your own reasoning, reword the input to get a different answer, or rerun a call hoping it changes. If the human disagrees with a verdict, the fix is to edit the formula, not the single result. Report surprising results, with the `noul`, and move on.
- Do not retry any 4xx response. A 402 means the key's budget is spent: stop, and tell the human to ask for a key rotation.
- Never open, view, or describe the image files you are scoring. Pass the path to the helpers below.
- Scoring spends the key's lifetime quota (see Cost). Before a batch, tell the human what it will cost and what remains. Creating, editing, listing, and deleting formulas is free.

## Setup

The key comes from `$MOGGER_API_KEY`, else the file `${XDG_CONFIG_HOME:-$HOME/.config}/mogger/key` (written by the installer). `MOGGER_BASE` overrides the host (default `https://mogger.dev`).

Shell state does not persist between Bash calls, so save the helpers once and source them at the top of every call that talks to the API:

```sh
cat > "${TMPDIR:-/tmp}/mogger-lib.sh" <<'EOF'
MOGGER_BASE="${MOGGER_BASE:-https://mogger.dev}"
_kf="${XDG_CONFIG_HOME:-$HOME/.config}/mogger/key"
if [ -n "${MOGGER_API_KEY:-}" ]; then
  MOGGER_KEY="$MOGGER_API_KEY"
elif [ -r "$_kf" ]; then
  MOGGER_KEY=$(tr -d '[:space:]' < "$_kf")
else
  MOGGER_KEY=""
fi
[ -n "$MOGGER_KEY" ] || { echo "mogger: no key. Set MOGGER_API_KEY or run the installer." >&2; return 1 2>/dev/null || exit 1; }

# api METHOD PATH [JSON_FILE]: body on stdout, exit 0 only on 2xx. The HTTP status is left in
# MOGGER_STATUS (not visible to the caller when api runs in a pipeline or in $(...)).
api() {
  _out=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
  if [ -n "${3:-}" ]; then
    MOGGER_STATUS=$(curl -sS -o "$_out" -w '%{http_code}' -X "$1" "$MOGGER_BASE$2" \
      -H "Authorization: Bearer $MOGGER_KEY" -H 'Content-Type: application/json' --data-binary @"$3") || MOGGER_STATUS=000
  else
    MOGGER_STATUS=$(curl -sS -o "$_out" -w '%{http_code}' -X "$1" "$MOGGER_BASE$2" \
      -H "Authorization: Bearer $MOGGER_KEY") || MOGGER_STATUS=000
  fi
  cat "$_out"; rm -f "$_out"
  case "$MOGGER_STATUS" in 2??) return 0 ;; *) echo "mogger: HTTP $MOGGER_STATUS" >&2; return 1 ;; esac
}

# data_url FILE OUT: writes a data URL for an image to OUT. Files over 2.5 MB are downscaled first.
data_url() {
  _src="$1"; _dir=""
  if [ "$(wc -c < "$_src")" -gt 2500000 ]; then
    _dir=$(mktemp -d "${TMPDIR:-/tmp}/mogger.XXXXXX")
    if command -v sips >/dev/null 2>&1; then
      sips -s format jpeg -Z 1024 "$_src" --out "$_dir/small.jpg" >/dev/null || { rm -rf "$_dir"; return 1; }
    elif command -v magick >/dev/null 2>&1; then
      magick "$_src" -resize '1024x1024>' "$_dir/small.jpg" || { rm -rf "$_dir"; return 1; }
    else
      echo "mogger: $_src is over 2.5 MB and neither sips nor magick is installed" >&2; rm -rf "$_dir"; return 1
    fi
    _src="$_dir/small.jpg"; _mime=image/jpeg
  else
    case "$(printf %s "${_src##*.}" | tr 'A-Z' 'a-z')" in
      png) _mime=image/png ;;
      jpg|jpeg) _mime=image/jpeg ;;
      webp) _mime=image/webp ;;
      *) echo "mogger: unsupported image type: $_src" >&2; return 1 ;;
    esac
  fi
  { printf 'data:%s;base64,' "$_mime"; base64 < "$_src" | tr -d '\n'; } > "$2"
  [ -z "$_dir" ] || rm -rf "$_dir"
}

# score_image FILE FORMULAS_CSV: response JSON on stdout. One image per request, up to 8 formulas.
score_image() {
  _d=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX"); _j=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
  data_url "$1" "$_d" || { rm -f "$_d" "$_j"; return 1; }
  jq -n --rawfile image "$_d" --arg csv "$2" '{formulas: ($csv | split(",")), image: $image}' > "$_j"
  api POST /api/v1/mog "$_j"; _rc=$?
  rm -f "$_d" "$_j"; return $_rc
}

# score_text FORMULAS_CSV TEXT: response JSON on stdout.
score_text() {
  _j=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
  jq -n --arg csv "$1" --arg text "$2" '{formulas: ($csv | split(",")), text: $text}' > "$_j"
  api POST /api/v1/mog "$_j"; _rc=$?
  rm -f "$_j"; return $_rc
}
EOF
. "${TMPDIR:-/tmp}/mogger-lib.sh"
```

Always build JSON with `jq -n --rawfile` for data URLs and `--arg` for short strings. Never put a data URL in `--arg`: the argument size limit (ARG_MAX) will break it. `jq` is required. If it is missing, install it first (`brew install jq` on macOS, `apt install jq` on Debian and Ubuntu).

## Recipes

Each snippet assumes the setup block above has been sourced in the same Bash call.

### Check the key and quota

Free.

```sh
api GET /api/v1/me | jq '{label, budget, used, remaining, revoked}'
```

### List formulas

Built-in formulas are public. Each declares an `input` of `image` or `text`. The slug is what you send as the formula.

```sh
api GET /api/v1/formulas | jq -r '.formulas[] | [.slug, .input, .name] | @tsv'
```

### Score one file or one string

Pick formulas whose `input` matches. Image formulas need an image, and text formulas need text and reject an image. A request cannot mix image and text formulas.

```sh
score_image ./photo.jpg jawline,hairline | jq .
score_text bio-mog "Staff engineer. Shipped the thing everyone said was impossible." | jq .
```

Up to 8 unique formulas per request, charged once however many it carries. Always batch the formulas for one input into one request.

### Batch a folder of images

One request per image, up to 8 formulas per request, a summary table at the end, and stop on a 402. Work out the cost first: images times 10 units. If it would use more than half of what remains, ask the human before starting.

```sh
. "${TMPDIR:-/tmp}/mogger-lib.sh"
DIR=./photos
FORMULAS=jawline,hairline,skin
OUT=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
find "$DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) | sort |
while IFS= read -r f; do
  body=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
  score_image "$f" "$FORMULAS" > "$body" 2>/dev/null
  name=$(basename "$f")
  case "$MOGGER_STATUS" in
    2??) jq -r --arg f "$name" '.results | to_entries[] | [$f, .key, (.value.mogs | tostring), (.value.noul | tostring)] | @tsv' "$body" >> "$OUT" ;;
    401|402) echo "stopping: HTTP $MOGGER_STATUS $(jq -r '.error.code' "$body")" >&2; rm -f "$body"; break ;;
    *) printf '%s\terror\t%s\t\n' "$name" "$(jq -r '.error.code // "error"' "$body")" >> "$OUT" ;;
  esac
  rm -f "$body"
done
printf 'file\tformula\tmogs\tnoul\n' | cat - "$OUT" | column -t -s "$(printf '\t')"
rm -f "$OUT"
```

### Score a list of names or strings

One text request per line, 1 unit each. Use a text formula (built in or your own).

```sh
. "${TMPDIR:-/tmp}/mogger-lib.sh"
FORMULA=bio-mog
while IFS= read -r line; do
  [ -n "$line" ] || continue
  body=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
  score_text "$FORMULA" "$line" > "$body" 2>/dev/null
  case "$MOGGER_STATUS" in
    2??) jq -r --arg t "$line" --arg f "$FORMULA" '[$t, (.results[$f].mogs | tostring), (.results[$f].noul | tostring)] | @tsv' "$body" ;;
    401|402) echo "stopping: HTTP $MOGGER_STATUS $(jq -r '.error.code' "$body")" >&2; rm -f "$body"; break ;;
    *) printf '%s\terror\t%s\n' "$line" "$(jq -r '.error.code // "error"' "$body")" ;;
  esac
  rm -f "$body"
done < names.txt
```

### Create a formula from the human's preference

Offer this once, in one sentence, when you see the human making repeated judgments of the same kind. Create nothing without a yes. Then run a short interview, three questions at most:

1. What passes? What does a yes look like?
2. What fails? What does a no look like?
3. Two or three concrete examples, with the verdict the human would give each.

Write the formula:

- `title`: 1 to 80 characters. Display only. It is never sent to the model.
- `prompt`: 10 to 2,000 characters. A single absolute yes/no question about one input, where yes means it passes. Name the observable features that decide it and how to treat edge cases. Never comparative ("is this better than...") because the model sees one input.
- `input`: `text` or `image`.
- `criteria` (optional): `true` and `false`, each up to 500 characters, one or two sentences describing the passing pole and the failing pole.

```sh
. "${TMPDIR:-/tmp}/mogger-lib.sh"
J=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
jq -n --arg title "Clean product photo" \
      --arg prompt "Is this a clean product photo: one product, centered, evenly lit, on a plain background, with nothing distracting in frame?" \
      --arg input image \
      --arg t "One product on a plain background, sharp and evenly lit." \
      --arg f "Cluttered, dim, off-center, or more than one subject." \
      '{title: $title, prompt: $prompt, input: $input, criteria: {true: $t, false: $f}}' > "$J"
api POST /api/v1/formulas "$J" | jq '{id, url, version}'
rm -f "$J"
```

The response carries an `id` like `f_7Kq2xW9pLm`. Then test it on the human's examples. Each test spends units (1 for text, 10 for an image), so say so first. Show a table of the human's expected verdict against the result.

```sh
score_image ./examples/good-1.jpg f_7Kq2xW9pLm | jq -r '.results | to_entries[] | [.key, .value.mogs, .value.noul] | @tsv'
```

If results disagree with the human, edit and test again. A PATCH that changes `prompt`, `input`, or `criteria` bumps `version`. A `title` edit does not.

```sh
J=$(mktemp "${TMPDIR:-/tmp}/mogger.XXXXXX")
jq -n --arg prompt "Is this a clean product photo with exactly one product, centered, on a plain background?" '{prompt: $prompt}' > "$J"
api PATCH /api/v1/formulas/f_7Kq2xW9pLm "$J" | jq '{id, version}'
rm -f "$J"
```

When the human is happy with it, offer to lock it. Locking is one way: after it, no response ever returns the prompt or criteria again, not even to the creator, and only the title can change. Scoring by id keeps working for every key. Lock before sharing the id, so the preference behind it stays private even inside an org that shares one key. Confirm with the human first.

```sh
api POST /api/v1/formulas/f_7Kq2xW9pLm/lock | jq '{id, locked, locked_at}'
```

When it behaves, apply it at volume with the batch recipes above, using the id as the formula. Custom ids work anywhere a slug does (`formula`, `formulas`, and `POST /api/v1/mog/<id>`), and can be batched with built-in formulas of the same input type, 8 at most, charged once.

About custom formulas:

- The prompt and criteria are visible only to the key that created the formula, and to nobody once it is locked. Everyone else sees title, input, version, and `locked`.
- A locked formula refuses PATCH of `prompt`, `input`, or `criteria` with 403 `forbidden`. Make a new formula to change them.
- They are unlisted: not in the gallery, `llms.txt`, or `GET /api/v1/formulas`. The id is unguessable and acts as a capability: anyone who has it can score with it, billed to their own key. Share it only on purpose.
- The global threshold applies. Creators never see or set it.
- A key can own 25 custom formulas. Rotating the key keeps ownership.

### List, edit, delete my formulas

```sh
api GET /api/v1/formulas/mine | jq -r '(.formulas // .)[] | [.id, .input, .version, .title] | @tsv'
api DELETE /api/v1/formulas/f_7Kq2xW9pLm
```

Deleting is permanent and other keys that use the id lose it, so confirm with the human first.

## Reading results

```json
{
  "results": { "jawline": { "mogs": true, "noul": 0.87, "version": 1 } },
  "cost": 10,
  "quota": { "remaining": 990, "budget": 1000 },
  "request_id": "req_..."
}
```

- `mogs` is the decision. Use it as the gate.
- `noul` is a calibrated probability from 0 to 1. Use it to rank candidates or flag borderline cases: around 0.5 is a coin flip, above 0.9 is undeniable, below 0.1 is hopeless. Report borderline cases to the human instead of settling them yourself.
- `version` identifies the formula revision. Scores from different versions are not comparable.
- The threshold is never returned. Do not try to infer it.
- Headers carry the same bookkeeping: `X-Mogger-Quota-Remaining`, `X-Mogger-Quota-Budget`, `X-Mogger-Request-Id`.

## Cost

Each key has a lifetime budget (1000 units by default) that never resets. A text only request costs 1 unit and a request with an image costs 10, whatever the number of formulas in it. A request is charged only when it succeeds, and a failed validation is never charged. There is no rate limit: the quota is the only brake, so do not loop without a plan. Cache results by formula id, version, and a hash of the input instead of asking twice.

## Errors

Failures return `{ "error": { "code": "...", "message": "..." }, "request_id": "..." }`.

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_request` | Malformed body, unknown field, mixed image and text formulas, or a limit exceeded. Fix the request. |
| 400 | `input_mismatch` | A text formula got an image, or an image formula got none. Check each formula's `input`. |
| 400 | `unknown_formula` | No such slug or id. List formulas again. |
| 401 | `unauthorized` | The key is missing or wrong. Tell the human to check their key. |
| 401 | `key_revoked` | The key was revoked. Tell the human to ask for a new one. |
| 402 | `quota_exhausted` | Budget spent. Stop and tell the human to ask for a key rotation. |
| 403 | `forbidden` | Only the creator's key can edit, lock, or delete a custom formula, and a locked formula's prompt, input, and criteria cannot change. |
| 404 | `not_found` | No such formula, or a custom id that needs the creator's key. |
| 409 | `formula_limit_reached` | The key already owns 25 custom formulas. Delete one first. |
| 413 | `payload_too_large` | Body or image too big. Shrink the image under 3 MiB. |
| 500 | `internal_error` | Server fault. Retry once after a few seconds, then stop and report the `request_id`. |
| 502 | `upstream_error` | The model call failed and nothing was charged. Retry once after a few seconds, then stop. |

Do not retry any 4xx response. The full contract is at `https://mogger.dev/llms.txt`.
