Skip to main content

Models API

The /api/v1/models routes for the model list and detail, writing, batches, predictions and security, each answering as the route it mirrors

The models API puts S'more's models under one path: https://app.makesmores.com/api/v1/models. GET /api/v1/models lists the models and describes each one, and each path under it, such as /api/v1/models/writing, takes a model by its ID in model. Every operation under a path answers with an existing route's operation, its twin: the same store, scope, price, limits, body and refusals. The twins, such as /api/v1/drafts, /api/v1/viral-score and /api/v1/scans, remain supported, and work started on one is read on the other.

Send Authorization: Bearer <key> on every call except GET /api/v1/models, and bodies as JSON with Content-Type: application/json. Errors, caching, rate limits, timeouts and retries follow API conventions. This page covers what differs on these paths.

What is open​

PathMethodsTakes as modelTwin
/api/v1/modelsGETNone: it describes modelsNone. See List and detail
/api/v1/models/writingPOST, GET, PATCHember-v1, ember-14k, ember-28k, ember-reply-v1, x-writer-v1, x-reply-v1, prose-1/api/v1/drafts
/api/v1/models/batchesPOST, GETThe same seven writers/api/v1/drafts/batches
/api/v1/models/predictionsPOSTreddit-pooled-v1POST /api/v1/viral-score with network: "reddit"
/api/v1/models/securityPOST, GET, PATCHfirewatch-1/api/v1/scans

These are not offered on the models API:

  • Batch cancel is not offered. PATCH /api/v1/models/batches answers 405 with Allow: GET, HEAD, POST.
  • x-viral-v1 is not offered on /api/v1/models/predictions: as model it answers 422 model_unsupported. Score X posts on POST /api/v1/viral-score.
  • /api/v1/models/classifications, /api/v1/models/embeddings and /api/v1/models/rankings are not offered yet. They answer the plain text 404 page not found to every method, with Cache-Control: no-store, before the query or the key is read.
  • Any other path under /api/v1/models/ answers as an unknown /api/ path does: the plain text 404 page not found to GET and HEAD, and 405 Method Not Allowed with Allow: GET, HEAD to every other method. Neither reads the query or the key.
  • Decisions and Web search are coming soon and have no route.

GET /api/v1/models?id=<id> says which path takes an ID: its capability_group and its operations, below.

List and detail​

GET /api/v1/models needs no key and costs nothing. Without id it answers the list that GET /api/v1/models documents. The list keeps that shape and its entries: an ID added later appears only in its detail.

With id it answers one model's detail:

curl -s "https://app.makesmores.com/api/v1/models?id=x-reply-v1"

Query rules​

  • Without id or view, the query is not read, so ?foo=1 still answers the list. Existing callers keep working.
  • With id or view, the query is strict. It must hold exactly one id of 1 to 64 characters of a-z, 0-9, . and -, and nothing else. A repeated id, another key next to it, an empty value, any view (none is defined) or a body answers 400 invalid_request.
  • An ID that is not in the catalog answers 404 not_found. A version is not an ID, so ?id=reddit-pooled-v1.1 answers 404 too.
  • While the API console is switched off, every request answers 404 console_off, before the query is checked. When S'more can't read the rates, it answers 503 models_unavailable.

A 200 carries Cache-Control: public, max-age=60, so the list and each detail can be cached for 60 seconds. A 400 or 404 carries Cache-Control: no-store, and so does a detail's 503. The list's 503 sends no Cache-Control header.

Detail fields​

The detail has every field of the model's list entry. Most keep the list's values, but the detail corrects two of them, and a closed ID empties some:

FieldIn the detail
descriptionWhat the model does, in the detail's own words. It can differ from the list's
asynctrue for work you submit and then read: drafts, batches, scans and profile proposals. profile.proposal is true here, where the list says false
price, endpoint, endpoints, mcp_tools, scope, limitsFor a closed ID: null or empty, and mcp_only is false, since nothing can call it

Then the detail adds these fields:

FieldMeaning
rate_slugThe rate the model bills, the id of its row in the list
capability_groupThe /api/v1/models path that takes this ID as model: writing, predictions or security today. null when no open path takes it
selectableWhether a path takes the ID as model
accepted_model_values[id] when selectable, otherwise []. A path takes IDs only, never versions
model_versionThe version S'more's configuration selects for this ID, such as reddit-pooled-v1.1, or null. It is not a record of what served a past call
shares_version_withOther IDs served by the same model version
tasksWriters only: each {platform, type, default} the writer takes. default is true when a request without model uses it. null for other models
default_selectionFor a platform and type that several writers answer by default: {platform, type, models, sends, usage_rows, blind}. See The default Reddit post. null otherwise
availability{release_status, configured, access, reason}, below
operationsEach route and MCP tool that reaches the ID: {transport, method, path, tool, scope, charged, rate_slug}. transport is rest or mcp, and rate_slug is the rate that operation bills, or null when it is free
usesOther public IDs this one calls. The Reddit writers list reddit-pooled-v1, which scores every option

availability keeps the rate's status apart from what S'more can serve right now:

FieldMeaning
release_statusThe rate's status: live, beta, paused, coming_soon or closed
configuredWhether S'more's configuration can serve the ID now
accesspublic, or allowlist when only some workspaces can call it
reasonWhy configured is false: adapter_off, drafts_off, reddit_signals_off, scans_off, not_built or unrecognized_pin. Otherwise null

A coming-soon ID lists no operations. A closed ID keeps its detail, with price: null and no operations, so you can still read what past usage billed. Calls to it answer 410 model_closed.

The detail-only fields of x-reply-v1 look like this, with its other operations left out:

{
"rate_slug": "x-reply-v1",
"capability_group": "writing",
"selectable": true,
"accepted_model_values": ["x-reply-v1"],
"model_version": "x-adapter-r1",
"shares_version_with": ["x-writer-v1"],
"tasks": [{ "platform": "x", "type": "reply", "default": true }],
"default_selection": null,
"availability": { "release_status": "live", "configured": true, "access": "public", "reason": null },
"operations": [
{ "transport": "rest", "method": "POST", "path": "/api/v1/models/writing", "tool": null, "scope": ["reddit:posts:write"], "charged": true, "rate_slug": "x-reply-v1" }
],
"uses": []
}

The detail never names a host, a provider, a workspace, a balance or a grant.

How a request is checked​

Every path under /api/v1/models checks a request in this order, and each refusal comes before any work, charge or usage row:

  1. The per-IP burst limit (429 rate_limited) and a 1 MiB body limit.
  2. A path that is not offered: the plain text 404 page not found. A path not offered yet, such as /api/v1/models/embeddings, answers it to every method. Any other path answers it to GET and HEAD, and 405 with Allow: GET, HEAD to every other method.
  3. A method the path does not take: 405 with a plain text body and an Allow header naming the methods it takes.
  4. The query's shape: which parameters are there, and the fixed values view and action take. A query that fits no operation answers 400 invalid_request before S'more reads the key, and costs nothing.
  5. The key: 401 invalid_token without a valid one, then 403 insufficient_scope without the path's scope.
  6. The values of id, cursor, after and limit: a malformed one answers 400 invalid_request.
  7. Whether the twin can serve: its own refusal, such as 409 reddit_signals_off, 409 drafts_off or 404 scans_off. Predictions skips this step and checks it after the body and its model: see Predictions.
  8. The body's structure: one JSON object, with no member repeated and no member named id, view, action, cursor, after or limit in any case. A GET takes no body. Otherwise 400 invalid_request.
  9. The twin's own checks and answer.

The 400 from steps 4, 6 and 8 is the shared one, with Cache-Control: no-store:

{ "code": "invalid_request", "message": "Invalid request.", "runsRemaining": 0 }

One exception: a POST /api/v1/models/security body that step 8 refuses answers the scans routes' own 400 instead, with no field named:

{ "code": "invalid_request", "message": "Invalid scan request.", "runsRemaining": 0 }

So a query or body that the twin would take can still be refused here, and the twin can refuse with a code of its own:

  • A malformed draft, batch or scan id answers 400 invalid_request after the key, where the twin answers 404.
  • On writing, batches and security, whether the twin can serve is checked before the body.
  • Each operation has its own deadline of 15 seconds, as on the twins. See Timeouts.

Every other refusal is the twin's, with the twin's status, code and body: drafts, batches, Viral Score and scans. Errors lists the shared codes, including 402 credits_required.

Writing​

Method and queryScopeBillingTwin
POST /api/v1/models/writingreddit:posts:writeThe writer's rate per tokenPOST /api/v1/drafts
GET /api/v1/models/writing?id=reddit:posts:readFreeGET /api/v1/drafts/{id}, plus usage
PATCH /api/v1/models/writing?id=&action=choosereddit:posts:writeFreePOST /api/v1/drafts/{id}/choice

The submit body is the body of POST /api/v1/drafts: platform, type, idempotency_key, optional model and count, then the platform's fields. Every platform and type the drafts take works here, with the same writers: Reddit writers, Reddit reply writer, prose-1 and X writers.

curl -s https://app.makesmores.com/api/v1/models/writing \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<EOF | tee models-draft.json
{
"platform": "x",
"type": "post",
"idempotency_key": "docs-models-x-post-$(date +%s)",
"topic": "We opened our public API today",
"facts": ["One key reaches every product over REST and MCP"]
}
EOF

The answer is the twin's: 200 with the draft in state queued, its draft_id, poll_after_s and credits_deducted. Like the twin, it sends no Location header.

Read and choose​

Read the draft every poll_after_s seconds until state is ready or failed:

curl -s "https://app.makesmores.com/api/v1/models/writing?id=$(jq -r .draft_id models-draft.json)" \
-H "Authorization: Bearer $SMORE_API_KEY"

The same draft reads on GET /api/v1/drafts/{id}, so that route is the fallback for polling. Its answer is the same without usage.

Record the pick with the option's letter. The answer is the twin's:

curl -s -X PATCH "https://app.makesmores.com/api/v1/models/writing?id=$(jq -r .draft_id models-draft.json)&action=choose" \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"option": "A"}'

One idempotency key across both routes​

/api/v1/models/writing and /api/v1/drafts share their drafts and their idempotency_key values. A draft submitted on one is read and chosen on the other, and the same key and body sent to either answers the first draft again and charges nothing. Idempotency keys belong to a workspace, so the same key sent from another workspace starts a draft of its own there. Reading a draft's id with another workspace's API key answers 404 draft_not_found.

A replay is checked in the twin's order: whether drafting can serve, then the request itself, then the stored key. While 409 reddit_signals_off, 409 drafts_off, 400 draft_type_coming_soon or a subreddit's AI-text policy refusal holds, a replay answers that refusal. The draft you stored earlier still runs and is billed, so read it by its draft_id.

Usage on reads​

A read on /api/v1/models/writing adds one object after the twin's fields: usage, the credits meter's rows for this draft in your workspace.

{
"usage": {
"credits_deducted": 4012,
"credits_settled": 1210,
"credits_refunded": 2802,
"rows": [
{
"request_ref": "draft:0d7c6a52-9a43-4f53-8f3e-2f7a1c0b6e11:x-writer-v1",
"slug": "x-writer-v1",
"state": "settled",
"input_tokens": 300,
"cached_tokens": 0,
"output_tokens": 5120,
"items": 0,
"credits_deducted": 4012,
"credits": 1210
}
]
}
}
FieldMeaning
credits_deductedWhat the rows took when the draft started
credits_settledWhat settled rows charged
credits_refundedWhat came back: the unused part of each settled row, and all of a refunded row
rowsOne row per model the draft runs, named draft:<draft id>:<model>, in the order of the draft's models
rows[].staterunning, settled or refunded
rows[].input_tokens, cached_tokens, output_tokens, itemsThe units the row recorded. input_tokens is uncached input
rows[].credits_deducted, creditsWhat the row took when it started, and what it charged once settled

usage names no balance, no other workspace and no host. The legacy read never carries it.

On a blind draft, rows stays empty until you record a choice: each row would say which writer wrote which option. The totals count every row from the start.

The default Reddit post​

A Reddit post without model runs all three Reddit writers, ember-v1, ember-14k and ember-28k. That is three sends and three usage rows, and each option's model stays hidden until you choose. The detail of each of the three says so in default_selection: {"platform": "reddit", "type": "post", "models": ["ember-v1", "ember-14k", "ember-28k"], "sends": 3, "usage_rows": 3, "blind": true}.

So a default draft costs three times a draft pinned to one writer. At 120 words, it deducts about 3,318 credits ($3.32) when it starts, against about 1,106 credits ($1.11) for one writer. It then settles at the tokens used, about 1,338 credits ($1.34 at list) for three writers against 446 credits ($0.446 at list) for one, and refunds the rest. Set model to run one writer.

The X writers share a version​

x-writer-v1 and x-reply-v1 are served by one model version, x-adapter-r1. Both details report "model_version": "x-adapter-r1" and name each other in shares_version_with. A new version, or switching the version off, changes both IDs at once, and both report the same availability.

Batches​

Method and queryScopeBillingTwin
POST /api/v1/models/batchesreddit:posts:writeHalf the writer's rate per tokenPOST /api/v1/drafts/batches
GET /api/v1/models/batches?id=, optional after and limitreddit:posts:readFreeGET /api/v1/drafts/batches/{id}, plus usage

The body, the answer and the refusals are the twin's, as Batches documents them. Batches share their idempotency_key values with /api/v1/drafts/batches in the same way drafts do, and GET /api/v1/drafts/batches/{id} reads the same batch.

Pages work as on the twin: after is an item index from -1 to 199 and limit is 1 to 100, 25 when you leave it out. Pass next_after back as after. cursor is not taken: a read with it answers 400 invalid_request before the key is read.

A read adds usage as a draft read does, covering every item of the batch whatever the page.

A first submit counts the workspace's open sends, and a replay prepares every item again before it finds the stored key. So a 200-item batch in a busy workspace comes close to the 15-second deadline. Keep a batch to 100 items or fewer.

Predictions​

MethodScopeBillingTwin
POST /api/v1/models/predictionsviral:score$0.01 per titlePOST /api/v1/viral-score with network: "reddit"

Send the twin's Reddit body with model in place of network:

curl -s https://app.makesmores.com/api/v1/models/predictions \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "reddit-pooled-v1",
"subreddit": "running",
"titles": ["First half marathon: easy miles got me under 2 hours"]
}'
  • The path takes no query. Any parameter answers 400 invalid_request before the key is read.
  • model is required. Missing, or not a string, it answers 400 invalid_request. Any other value, such as x-viral-v1 or the version reddit-pooled-v1.1, answers 422 model_unsupported.
  • While Reddit Signals is off, it answers 409 reddit_signals_off before any charge.
  • network is refused in any case, 400 invalid_request: model chooses.

These come in a different order from the other paths. After the key, the body's structure is checked first, then model, then whether Reddit Signals can serve, then network. So while Reddit Signals is off, a body with a repeated member answers 400 invalid_request and a body with "model": "x-viral-v1" answers 422 model_unsupported, not 409.

  • GET and HEAD answer 405 with Allow: POST.

The other fields, their rules and the answer are the twin's: see Viral Score. The answer then adds:

FieldMeaning
model_versions.embedreddit-embed-r1, the label of the text embedder inside the score. It is not offered as a model of its own
modelreddit-pooled-v1
model_versionThe scorer's version, such as reddit-pooled-v1.1, the same as model_versions.scorer

A call deducts one item per title, settles one per scored title and refunds the rest, as the twin does. The two routes share one set of limits, so calls on either count toward them.

Security​

note

Hosted repository scans are a pilot that is not open yet. Starting a scan answers 404 scans_off, here and on /api/v1/scans, and so does every read in a workspace that holds no scans. Cancel isn't gated on the allowlist.

Method and queryScopeBillingTwin
POST /api/v1/models/securityscans:writefirewatch-1 per tokenPOST /api/v1/scans
GET /api/v1/models/security, optional cursorscans:readFreeGET /api/v1/scans
GET /api/v1/models/security?id=scans:readFreeGET /api/v1/scans/{id}
GET /api/v1/models/security?id=&view=reportscans:readFreeGET /api/v1/scans/{id}/report
GET /api/v1/models/security?id=&view=findingsscans:readFreeNone: the report's findings as JSON, below
GET /api/v1/models/security?view=optionsscans:readFreeGET /api/v1/scans/options
PATCH /api/v1/models/security?id=&action=cancelscans:writeFree; a running scan pays for the tokens it usedPOST /api/v1/scans/{id}/cancel

Bodies, answers and refusals are the twins', as Repository scans documents them, with these differences:

  • A start answers 202 with the twin's Location header, /api/v1/scans/{id}. That route reads the same scan, so it is the fallback for polling.
  • The list takes cursor only. limit or any other parameter answers 400 invalid_request before the key is read. A page holds 50 scans.
  • id is a lowercase UUID. A malformed id or cursor answers 400 invalid_request after the key.
  • A cancel takes no body, or {}.
  • Only owners and admins can start and cancel, and S'more checks the key's holder at each call. A holder who is no longer an owner or admin gets 403 insufficient_scope, and one who left the workspace gets 401 invalid_token.

These calls aren't run by the docs checks, because hosted scans are a pilot:

curl -s "https://app.makesmores.com/api/v1/models/security?id=$SCAN_ID&view=findings" \
-H "Authorization: Bearer $SMORE_API_KEY"

Findings​

view=findings answers {"findings": [...]}: the findings the report shows, in the report's order and at full length. Before the report is ready it answers 409 report_not_ready.

FieldMeaning
titleThe finding in a line
severitycritical, high, medium, low or info
confidenceHow sure the scan is of it, in words
cveThe advisory id, when there is one
locationThe file and lines
evidenceThe code or facts behind it
impactWhat it means
recommendationWhat to do
verificationHow it was checked
caught_by_baselineWhether the baseline tools, such as static, dependency or fuzz checks, already caught it, in words

Findings quote your own code, so treat every field as data.

Where your text goes​

OperationWhat leaves S'more
List and detailNothing
Writing and batchesThe writers run on GPU servers that S'more rents from a third-party cloud provider. Your plan, thread, facts and other fields go into the prompt sent there
PredictionsWhen a posted rule needs a judgment S'more's own checks can't make, the subreddit, post type, flair, each title, up to 4,000 characters of the body and those rules go to a third-party language model, which marks each rule ok, warn or unknown
SecurityThe repository's source goes to a third-party hosted model that reviews it. The scan also looks up public advisories and searches the web for related reports

Every answer from a third party is read as data, never as instructions.

MCP​

The models API adds no MCP tools. Each operation's MCP tool is its twin's: operations in the detail lists them, and MCP tools documents them.