dopp

dopp API

dopp is a drop-in proxy for any Jev-compatible decision API (POST /v1/systemone: TypeSafe's Jev, Kev, GLiNER, or your own endpoint). Send the same request you send to Jev; get the same response shape back. Each key belongs to a route. The route's setup decides who answers: by default Clef-flash (Cloudflare's open-weights model that speaks Jev's API), or Jev when your app sends its own TypeSafe key, until the route's own model, trained on its requests, meets the route's goal and takes over (autopilot). Every request is recorded and can become training data.

Base URL: https://dopp.sh

Set up from an agent

One call creates an account with a working key; no sign-up form. The person attaches it to themselves later by opening the claim link.

POST https://dopp.sh/agent/start
{"name": "my-app", "code": "<invite code, optional>"}
→ {"key": "us_…", "endpoint": "https://dopp.sh/v1/systemone", "claim_url": "https://dopp.sh/claim/…", "spendable_usd": 1, "expires_at": "…"}

Until it is claimed the account is a guest. A guest gets 10 free answers paid by dopp (per account, and per IP per day across guest accounts); after that /v1/systemone answers 402 with a claim_url until the person opens it. Requests that carry a TypeSafe key (as the bearer or in x-jev-key) are billed by TypeSafe and never count. The claim link works for 7 days and keeps everything already recorded. GET /agent/status with the key reports requests seen, spend left and whether it is claimed. Agent instructions: https://dopp.sh/skill.md

Authentication

Every request carries a bearer token: a dopp key (us_…, from the route's page or POST /agent/start), or the TypeSafe key your app already sends. A TypeSafe key is forwarded to TypeSafe, which keeps billing you; dopp stores only its hash, and the first request with it makes your account (sign in at dopp.sh and paste the same key once to see it). Requests through a key are recorded on that key's route and follow that route's setup.

Authorization: Bearer us_...

You do not need a TypeSafe key: a new route answers with Clef-flash, metered from your credit. A route can answer with Jev instead (pick it on the route). Then calls to Jev go through dopp's account and are metered from your credit, unless you add your key in Settings (stored encrypted, used only upstream) or send it per request as x-jev-key: <your key> (forwarded, never stored), and TypeSafe bills you directly. Jev's answers are kept on every request for your records but are never used as labels: training a model only on Jev's answers may go against TypeSafe's terms.

Endpoint

POST /v1/systemone

Request body (identical to TypeSafe):

{
  "model": "jev-latest",
  "state": "Board arrived with a cracked deck. I want a replacement, not a refund.",
  "questions": {
    "issue":  { "type": "choice", "instructions": "What is the main issue?",
                "criteria": { "damaged": "arrived broken", "wrong item": null, "late": null, "billing": null, "question": null } },
    "wants":  { "type": "choice", "instructions": "What does the customer want?",
                "criteria": { "replacement": null, "refund": null, "repair": null, "information": null } },
    "angry":  { "type": "noul",   "instructions": "Is the customer angry?" },
    "urgency":{ "type": "score",  "instructions": "How urgent is this?", "criteria": ["can wait", "this week", "today"] }
  }
}

- choice: pick one of criteria (option name → description or null). - noul: yes/no. Returns a probability of yes. - score: an ordered scale; criteria is the list of levels, lowest first.

Response:

{
  "model": "jev-1.13.0",
  "answers": {
    "issue":   { "type": "choice", "choice": "damaged", "probabilities": { "damaged": 0.97, "wrong item": 0.01, "late": 0.01, "billing": 0.0, "question": 0.01 } },
    "wants":   { "type": "choice", "choice": "replacement", "probabilities": { "replacement": 0.95, "refund": 0.03, "repair": 0.02, "information": 0.0 } },
    "angry":   { "type": "noul",   "noul": 0.81 },
    "urgency": { "type": "score",  "score": 1.4, "probabilities": { "0": 0.1, "1": 0.4, "2": 0.5 } }
  },
  "usage": { "input_tokens": 118, "output_tokens": 0 }
}

dopp adds one field to the response and sets model to whoever answered:

{
  "model": "jev-1.13.0",
  "understudy": {
    "served": "jev",
    "path": { "steps": [
      { "ask": "model", "ok": false, "reason": "skipped", "why": "there's no trained version yet" },
      { "ask": "jev", "ok": true, "ms": 218 }
    ] },
    "fallback": { "from": "model", "reason": "there's no trained version yet" },
    "recorded": true
  }
}

Choosing a target per request

Send x-understudy-target: jev (or model, or endpoint:<id>) to pick who answers one request. It is honoured only while the route's setup allows it (off by default); otherwise it is ignored.

Errors: 401 with {"error": "..."} for a missing, invalid or revoked key. Upstream errors from Jev are passed through with their status code.

Examples

curl:

curl -s https://dopp.sh/v1/systemone \
  -H "Authorization: Bearer $DOPP_KEY" \
  -H "content-type: application/json" \
  -d '{"state":"Trucks are loose, can you send new ones?","questions":{"wants":{"type":"choice","instructions":"What does the customer want?","criteria":{"replacement":null,"refund":null,"repair":null,"information":null}}}}'

Python:

import os, requests

def decide(state, questions):
    r = requests.post("https://dopp.sh/v1/systemone",
                      headers={"Authorization": "Bearer " + os.environ["DOPP_KEY"]},
                      json={"model": "jev-latest", "state": state, "questions": questions}, timeout=30)
    r.raise_for_status()
    return r.json()["answers"]

Node:

const answers = await fetch("https://dopp.sh/v1/systemone", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.DOPP_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ model: "jev-latest", state, questions }),
}).then(r => r.json()).then(r => r.answers);

TypeSafe's own SDK works unchanged: set its base URL to https://dopp.sh and keep your TypeSafe key, or use a dopp key.

Routes, requests, labels, autopilot

These are set on the dashboard (https://dopp.sh/routes). Autopilot does the routine parts by itself.

Your model's files

GET /v1/models with the route's dopp key lists its ready versions, newest first, and where each one's files are, so your app can run the model itself:

GET https://dopp.sh/v1/models
Authorization: Bearer us_...
→ {"object": "list", "route": "home", "data": [{"id": "home v1", "object": "model", "version": 1, "base": "tiny", "holdout": {"agreement": 0.985, "answers": 204}, "files": "https://dopp.sh/api/p/…/web/v1/…/", "offline_zip": "https://dopp.sh/…"}]}

Keep questions and options stable. Changing an option's name makes a new question-set with no examples of its own.

For agents

If you are an AI agent integrating dopp on someone's behalf:

Feature requests and problems

POST /feedback takes a feature request, a wall you hit, or a bug, from a person or an agent. No account needed. With a bearer (your dopp key, or the key your app sends) or a signed-in session it is tied to that account; the bearer is only looked up, never stored.

POST https://dopp.sh/feedback
content-type: application/json

{"text": "What I needed, what I tried, what happened", "kind": "feature", "agent": "Claude Code", "page": "/v1/systemone", "contact": "you@example.com"}
→ 201 {"ok": true, "id": "3f9c0a1b2c4d", "status_url": "https://dopp.sh/feedback/3f9c0a1b2c4d", "message": "…", "github": "https://github.com/doppsh/dopp/issues/new"}

Unknown paths under /v1/ and /agent/ answer 404 with {"error": "...", "feedback": "..."}, never a web page.

Limits

Raw markdown for agents: /docs.md · openapi.json · skill.md