Some facts in a picture are hard for a general model: how old a person looks, or whether a photo was made by an image generator. A booster is a model trained for exactly one such reading. It reads the request's images first, and its reading is added to what DecisionNode reads. DecisionNode still answers every question itself, in the shapes you asked for.
- Human Age (
age). Estimates a person's age from a face in the picture, using the agemin.com age estimation model, ranked #1 by NIST for Child Online Safety (ages 13 to 16) on true positive rate. View the NIST report card · NIST results. - AI-generated (
ai_generated). Whether a picture was made by an image generator, and which kind, using Trinity, the state-of-the-art AI image detection model from detector24.ai.
| Booster | Reads | Adds to the decision | Per picture | Time budget |
|---|---|---|---|---|
age | A picture of a person | How old the person is, with a range and a confidence. The largest face in the picture is read. | $0.024 | 800 ms |
ai_generated | Any picture | How likely the picture was made by an image generator, which generator most likely when it reads as generated, and whether the file carries trusted Content Credentials (C2PA) and who signed them. | $0.006 | 800 ms |
Add a booster to a request#
List the boosters by name in a top-level boosters array, beside images and questions: "boosters": ["age", "ai_generated"]. Each one reads every image of the request.
curl https://api.decisionnode.com/v1/decide \
-H "Authorization: Bearer $DECISIONNODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "decisionnode-latest",
"state": {
"account": {
"id": "acc_8812",
"product": "wine delivery",
"country": "SE"
}
},
"images": [
{
"id": "selfie",
"url": "https://files.example.com/signups/8812/selfie.jpg"
}
],
"boosters": ["age", "ai_generated"],
"questions": {
"adult": {
"type": "truth",
"instructions": "Is the person in `selfie` 18 or older?"
},
"real_photo": {
"type": "truth",
"instructions": "Is `selfie` a real photograph, not an AI-generated picture?"
}
}
}'- Up to 4 boosters a request, each named once.
nulland[]mean no boosters. - Boosters belong to the request, not to a question. Every question sees every reading, so say in each question which image it is about, by its
id. - Every image form works:
data,urlandupload_id. A booster reads exactly the bytes the decision reads. - A request without
boostersis answered exactly as before: nothing is added to it and nothing extra is billed.
Options#
Write a booster as an object instead of its name when it should read only some images, or when the request must not run without it. Below, age reads only the selfie and ai_generated reads both pictures and must answer: 3 booster calls, $0.036.
{
"images": [
{
"id": "selfie",
"url": "https://files.example.com/signups/8812/selfie.jpg"
},
{
"id": "document",
"url": "https://files.example.com/signups/8812/document.jpg"
}
],
"boosters": [
{
"name": "age",
"images": ["selfie"]
},
{ "name": "ai_generated", "require": true }
]
}A booster object
namestringrequired- The booster's name:
ageorai_generated. A bare string such as"age"is the same as{"name": "age"}. imagesarray of image idsdefaultevery image- The
ids of the request's images this booster reads. Leave it out to read them all. Naming fewer images costs fewer booster calls. requirebooleandefaultfalsetrue: if this booster fails or runs out of time, the request fails with529booster_unavailable, which you retry.false: the decision runs without that reading, andevidencesays why.optionsobject- Settings of a booster. Neither booster takes a setting today, so a key in it is a
400.
What comes back#
The answers keep their shapes. The body gains evidence, one entry per booster you named, in your order, and usage gains two counts. The response also carries the x-decisionnode-boosters header.
{
"model": "decisionnode-1.0",
"answers": {
"adult": { "type": "truth", "truth": 0.97 },
"real_photo": { "type": "truth", "truth": 0.96 }
},
"evidence": {
"age": {
"version": "age-v3",
"status": "ok",
"latency_ms": 188,
"billed_calls": 1,
"results": [
{
"image": "selfie",
"age_years": 34,
"low": 29,
"high": 39,
"confidence": 0.88,
"faces": 1
}
]
},
"ai_generated": {
"version": "ai_generated-v2",
"status": "ok",
"latency_ms": 342,
"billed_calls": 1,
"results": [
{
"image": "selfie",
"generated": 0.03,
"confidence": 0.94,
"method": "inference",
"top_generator": null,
"c2pa_signer": null
}
]
}
},
"usage": {
"input_tokens": 1121,
"output_tokens": 0,
"booster_tokens": 46,
"boosters": 2
}
}evidence.<booster>
versionstring- The version of the booster that read, such as
age-v3. Log it with the decision. statusstringok,timeout,error,skipped. See When a booster cannot read.latency_msinteger- How long the booster took.
billed_callsinteger- The calls of this booster billed on this request: one per image it read with status
ok, every time a request uses it; 0 only for a booster that timed out, failed or was skipped, a picture it could not decode, and a refused request. Times the booster's price, it is what the booster cost. resultsarray- One entry per picture, named by
image. Onok, each carries the booster's fields, or"readable": falsefor a picture it could not decode. Ontimeoutorerror, one bare{"image": id}per picture it was meant to read. Onskipped, empty.
body = response.json()
for name, entry in body.get("evidence", {}).items():
if entry["status"] != "ok":
log.warning("booster %s: %s", name, entry["status"]) # decided without it
for reading in entry["results"]:
save_reading(decision_id, name, entry["version"], reading)| Booster | Fields |
|---|---|
age | age_years, low, high, confidence, faces |
ai_generated | generated, confidence, method, top_generator, c2pa_signer, c2pa |
age reads the largest face in a picture: age_years with a likely range from low to high, a confidence, and faces, how many faces it found. A picture with no face comes back with faces 0 and no age. ai_generated gives generated, the probability that an image generator made the picture, and method: inference when it read the pixels, c2pa when the file's trusted Content Credentials declare it generated or edited by AI, which overrides the pixels and names the signer in c2pa_signer. top_generator names the likeliest generator only when a reading from the pixels says likely generated; a photograph like the selfie above reads null. Credentials that validate but are not trusted add "c2pa": "untrusted" and change nothing else. Every field and value is on the API reference.
| Field | What it counts |
|---|---|
usage.boosters | The booster calls billed for this request: one per booster and image read |
usage.booster_tokens | The part of input_tokens the readings take, counted once per request |
How the model uses a reading#
The readings are written after your state as short, fixed sentences, one per booster and image, each naming the picture by its number and id. The model reads them with the state, the images and the questions, as evidence. They are counted once per request, however many questions you ask, and their tokens are in usage.input_tokens and usage.booster_tokens.
- A reading is evidence, not an answer. DecisionNode answers every question itself and may weigh a reading against what the picture and the state show.
- Ask the question the reading informs. An age reading helps "Is the person in
selfie18 or older?" It does not answer it for you: your threshold ontruthdoes. - Keep the reading for your records.
evidencenames the booster version that read, so a decision can be explained later.
When a booster cannot read#
| Status | What happened | The decision | Billed |
|---|---|---|---|
ok | The booster read every picture; results has one reading per picture | Runs with the readings | Per picture read; not a picture it could not decode |
timeout | No answer within the booster's 800 ms; results lists one {"image": id} per picture it was meant to read | Runs without it; with require: true the request is a 529 | No: billed_calls 0 |
error | The booster failed; results lists the pictures as for a timeout | Runs without it; with require: true the request is a 529 | No: billed_calls 0 |
skipped | None of the request's images is one this booster reads; results is empty | Runs without it | No |
A booster that did not run still tells the model so, in its own line, so what the model read and what evidence says always agree. Within an ok booster, two readings say a picture gave nothing:
{ "image": "selfie", "faces": 0 }- No face (Human Age):
faces0 and no age. It is billed like any reading: the booster looked. - Not decodable:
"readable": false, a picture the booster could not decode. It is not billed. - Timed out or failed: the booster's
statussays so,resultsnames each picture it was meant to read with nothing else, andbilled_callsis 0.
Time#
Each booster has a time budget of 800 ms. The boosters of a request run side by side, before the model, so a boosted request takes at most the slowest booster's budget longer than the same request without boosters. latency_ms and the x-decisionnode-boosters header say what each one took.
x-decisionnode-boosters: age=ok:212,ai_generated=timeout:800One entry per booster, in your order: its name, its status and its time in milliseconds. The header is on every response of a request that named boosters, errors included, so a 529 tells you which booster failed.
What a booster costs#
cost = input tokens × model price
+ sum of billed_calls × booster price
- input tokens
usage.input_tokens, the readings included- billed_calls
evidence.<name>.billed_calls: one per image that booster read with statusok- booster price
- that booster's price per call:
age(Human Age) $0.024 andai_generated(AI-generated) $0.006
- Priced per call, by booster:
age(Human Age) $0.024 andai_generated(AI-generated) $0.006, drawn from your prepaid balance with the request's tokens. The first sample makes 2 calls, one of each booster on the one picture: $0.030. - A booster that timed out or failed is free, and so is a picture a booster could not decode (
"readable": false). A Human Age reading of a picture with no face is billed. - Billed every time a request uses a reading, the same picture in a later request included: there is no discount for a picture read before.
- A request the safety check refuses bills no booster call and carries no
evidence. - A
529after boosters ran bills the calls that read, since they happened, and your retry's readings are billed again. A402comes before any booster runs.
Booster spend shows under its own heading on the console's Usage page. Every price is on Pricing and billing.
Turning a booster off#
Every booster is on for a new workspace. An Owner or Admin can turn one off, or back on, on the console's Boosters page; other roles see the switches. A request that names a booster that is off gets 403 booster_not_enabled. A booster DecisionNode has turned off for your workspace shows as locked; write to us to change it.
Where boosters work#
| Where | Boosters |
|---|---|
POST /v1/decide | Yes |
| Batch jobs | Yes: each line takes boosters as a live request does, billed the same way |
| Fine-tuned models | Yes, as on their base model |
| Sessions | No: boosters on a session's open is 400 api_usage_error "boosters are not available on sessions" |
Errors#
A boosted request has five errors of its own: booster_unavailable (529, retry it), booster_not_enabled (403), and booster_unknown, booster_input_mismatch and a malformed array's api_usage_error (400, fix the request).
| Status | Type | When | What to do |
|---|---|---|---|
400 | api_usage_error | boosters is not an array, has more than 4 entries, names a booster twice, holds an entry that is neither a name nor an object with name, an object with a key other than name, images, require and options, or has a key in options. x-decisionnode-error says which | Fix the array |
400 | booster_unknown | A name that is not a booster | Use a name from the table above |
400 | booster_input_mismatch | A booster that reads images on a request without any, or an images entry that is not an image of the request | Add the image, or fix the id |
402 | insufficient_credit | The balance is empty; no booster ran | Add credit, then retry |
403 | booster_not_enabled | The booster is off for your workspace | Turn it on under Boosters, or leave it out |
529 | booster_unavailable | A booster with require: true failed or ran out of time | Wait Retry-After, then retry; the SDKs do |
{
"detail": {
"error_type": "booster_not_enabled",
"message": "The booster 'age' is not enabled for this workspace."
}
}What a booster never does#
- It never answers a question, however sure it is, and never adds a question or an answer.
- It never changes a request without boosters: the same request gets the same answer it got before boosters existed.
- It never decides what the safety check refuses. The safety check judges the request as you sent it, never a reading.
- It never keeps your images. The bytes are read for the call and not stored or logged; the request log records which boosters ran, their status and their time, never their readings.
- It never reads more than the request's images: the same images, in the same workspace, under the same retention, at most 16 a request.