# Recipe: read sentiment and stance

> Rate the sentiment of a review, pick the writer's stance on a price change and check for a competitor, then feed a dashboard and alert the pricing team when the text and the stars agree.

A subscriber writes a two-star review after a price rise. One request rates the sentiment on your five-level scale, reads the writer's stance on the price change and checks whether a competitor is named. A dashboard gets the rating, and the pricing team gets an alert when the words and the stars agree.

## The request

- **State.** The review with its product and star rating. The questions point at `{{review.text}}`, but the model reads the whole state, so the 2 stars sit next to the words as a fact.
- **`sentiment` is a `rate`** in beta on five levels, from `Very negative` to `Very positive`. On one model version the same text always gets the same rating, so a dashboard built on it is comparable over time.
- **`price_stance` is a `pick`** of four stances, with `not_mentioned` as the way out for reviews that say nothing about the price.
- **`mentions_competitor` is a `check`**: one yes or no fact about the text.
- **`min_confidence`** on every question your code acts on.

```json
{
  "state": {
    "review": {
      "product": "Monthly coffee subscription",
      "stars": 2,
      "text": "The beans are still good, but after the price went up from 18 to 24 euros a month I expected more than the same two bags. BrewBox gives you three bags for less. Cancelling at the end of the month unless something changes."
    }
  },
  "questions": {
    "sentiment": {
      "type": "rate",
      "instructions": "What is the overall sentiment of {{review.text}}?",
      "levels": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"],
      "min_confidence": 0.3
    },
    "price_stance": {
      "type": "pick",
      "instructions": "What is the writer's stance on the price change in {{review.text}}?",
      "options": {
        "for": "Accepts or supports the new price",
        "against": "Objects to the new price",
        "neutral": "Mentions the price without taking a side",
        "not_mentioned": "Does not mention the price change"
      },
      "min_confidence": 0.5
    },
    "mentions_competitor": {
      "type": "check",
      "instructions": "Does {{review.text}} mention a competing product or company?",
      "min_confidence": 0.6
    }
  }
}
```

## Run it

Put a test key in `DEX_API_KEY` (see the [Quickstart](/docs/quickstart/#get-a-key-in-60-seconds)). Save the Python code as `sentiment.py` and run `python sentiment.py`. Save the TypeScript code as `sentiment.mts` and run `npx tsx sentiment.mts`: the code uses `await` at the top level, and the `.mts` ending makes the file an ES module. Install the SDKs from [Downloads](/docs/reference/sdks/#downloads).

```bash tab="curl"
curl https://api.thinqit.ai/v1/decide \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data-binary @- <<'DEX_REQUEST'
{
  "state": {
    "review": {
      "product": "Monthly coffee subscription",
      "stars": 2,
      "text": "The beans are still good, but after the price went up from 18 to 24 euros a month I expected more than the same two bags. BrewBox gives you three bags for less. Cancelling at the end of the month unless something changes."
    }
  },
  "questions": {
    "sentiment": {
      "type": "rate",
      "instructions": "What is the overall sentiment of {{review.text}}?",
      "levels": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"],
      "min_confidence": 0.3
    },
    "price_stance": {
      "type": "pick",
      "instructions": "What is the writer's stance on the price change in {{review.text}}?",
      "options": {
        "for": "Accepts or supports the new price",
        "against": "Objects to the new price",
        "neutral": "Mentions the price without taking a side",
        "not_mentioned": "Does not mention the price change"
      },
      "min_confidence": 0.5
    },
    "mentions_competitor": {
      "type": "check",
      "instructions": "Does {{review.text}} mention a competing product or company?",
      "min_confidence": 0.6
    }
  }
}
DEX_REQUEST
```

```python tab="Python"
# Save as dex_request.py, then run: python dex_request.py
import json

from thinqit_dex import Client

client = Client()  # reads DEX_API_KEY from the environment

request = json.loads(r'''
{
  "state": {
    "review": {
      "product": "Monthly coffee subscription",
      "stars": 2,
      "text": "The beans are still good, but after the price went up from 18 to 24 euros a month I expected more than the same two bags. BrewBox gives you three bags for less. Cancelling at the end of the month unless something changes."
    }
  },
  "questions": {
    "sentiment": {
      "type": "rate",
      "instructions": "What is the overall sentiment of {{review.text}}?",
      "levels": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"],
      "min_confidence": 0.3
    },
    "price_stance": {
      "type": "pick",
      "instructions": "What is the writer's stance on the price change in {{review.text}}?",
      "options": {
        "for": "Accepts or supports the new price",
        "against": "Objects to the new price",
        "neutral": "Mentions the price without taking a side",
        "not_mentioned": "Does not mention the price change"
      },
      "min_confidence": 0.5
    },
    "mentions_competitor": {
      "type": "check",
      "instructions": "Does {{review.text}} mention a competing product or company?",
      "min_confidence": 0.6
    }
  }
}
''')

decision = client.decide(
    request["state"],
    request["questions"],
)
for question_id, answer in decision.answers.items():
    print(question_id, answer)
```

```ts tab="TypeScript"
// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";

const client = new Client(); // reads DEX_API_KEY from the environment

// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
  "state": {
    "review": {
      "product": "Monthly coffee subscription",
      "stars": 2,
      "text": "The beans are still good, but after the price went up from 18 to 24 euros a month I expected more than the same two bags. BrewBox gives you three bags for less. Cancelling at the end of the month unless something changes."
    }
  },
  "questions": {
    "sentiment": {
      "type": "rate",
      "instructions": "What is the overall sentiment of {{review.text}}?",
      "levels": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"],
      "min_confidence": 0.3
    },
    "price_stance": {
      "type": "pick",
      "instructions": "What is the writer's stance on the price change in {{review.text}}?",
      "options": {
        "for": "Accepts or supports the new price",
        "against": "Objects to the new price",
        "neutral": "Mentions the price without taking a side",
        "not_mentioned": "Does not mention the price change"
      },
      "min_confidence": 0.5
    },
    "mentions_competitor": {
      "type": "check",
      "instructions": "Does {{review.text}} mention a competing product or company?",
      "min_confidence": 0.6
    }
  }
}`);

const decision = await client.decide(request);
console.log(decision.answers);
```

## Expected output

```json
{
  "id": "req_01M3JE28W1X5MHKHQB8HH39P96",
  "object": "decision",
  "created": 1790546355,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "sentiment": {
      "type": "rate",
      "rating": 1.1423,
      "levels": ["Very negative", "Negative", "Neutral", "Positive", "Very positive"],
      "probabilities": [0.0494, 0.8349, 0.0586, 0.0384, 0.0187],
      "confidence": 0.6798,
      "abstained": false
    },
    "price_stance": {
      "type": "pick",
      "choice": "against",
      "probabilities": {
        "for": 0.0334,
        "against": 0.9066,
        "neutral": 0.0357,
        "not_mentioned": 0.0243
      },
      "confidence": 0.8709,
      "abstained": false
    },
    "mentions_competitor": {
      "type": "check",
      "probability": 0.903,
      "confidence": 0.806,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 172,
    "state_tokens": 72,
    "question_tokens": 100,
    "allowance_tokens": 0,
    "paid_tokens": 0,
    "charge_micro_cents": 0,
    "unit_price_micro_cents": 0,
    "tier": "test"
  }
}
```

Captured from the live API on 2026-09-27 with a test key: model `dex-1.0.1`, calibration `cal-20260926-1`, `served_by: gpu`, 172 input tokens (72 for the state, 100 for the questions). A test key is charged nothing, so `tier` is `test` and the charge is 0. On this exact version the same request always returns these answers.

| Question | Type | Answer | Confidence | `min_confidence` | Abstained |
| --- | --- | --- | --- | --- | --- |
| `sentiment` | rate | rating 1.1423, most likely `Negative` (0.8349) | 0.6798 | 0.3 | no |
| `price_stance` | pick | `against` (0.9066) | 0.8709 | 0.5 | no |
| `mentions_competitor` | check | yes with probability 0.903 | 0.806 | 0.6 | no |

The review is `Negative` (0.8349), with a rating of 1.1423 on a scale from 0 to 4. The writer objects to the new price (0.9066) and names a competitor (0.903). No answer abstained.

## Act on it

`review_id`, `stars` and the functions `record_sentiment`, `alert_pricing_team` and `add_tag` stand for your own code; `stars` is the star rating from your own record. The alert needs two signals that agree: the stance read from the words and the stars the customer gave. Here both hold, so the rating goes to the dashboard, the pricing team gets an alert and the review is tagged `competitor`.

```python tab="Python"
sentiment = decision.rate("sentiment")
if not sentiment.abstained:
    record_sentiment(review_id, sentiment.rating)  # 0 is very negative, 4 very positive

stance = decision.pick("price_stance")
if not stance.abstained and stance.choice == "against" and stars <= 2:
    alert_pricing_team(review_id)  # the words and the stars agree

competitor = decision.check("mentions_competitor")
if not competitor.abstained and competitor.probability >= 0.5:
    add_tag(review_id, "competitor")
```

```ts tab="TypeScript"
const { sentiment, price_stance: stance, mentions_competitor: competitor } = decision.answers;

if (sentiment?.type === "rate" && !sentiment.abstained) {
  recordSentiment(reviewId, sentiment.rating); // 0 is very negative, 4 very positive
}
if (stance?.type === "pick" && !stance.abstained && stance.choice === "against" && stars <= 2) {
  alertPricingTeam(reviewId); // the words and the stars agree
}
if (competitor?.type === "check" && !competitor.abstained && competitor.probability >= 0.5) addTag(reviewId, "competitor");
```

As in the [support recipe](/docs/cookbook/support-routing/#act-on-it), the TypeScript answers have the general `Answer` type, so the code narrows each one on `type`.

## Adapt it

- **Do not act on sentiment alone.** Sarcasm and irony are often read literally, especially in Dutch. Combine the answer with facts such as the stars, a refund request or a cancellation, or send strong answers from irony-prone channels to a person. See [Known limits](/docs/concepts/known-limits/#sarcasm-and-irony-are-often-read-literally).
- **Pin a version for trends.** When the alias moves to a new version, ratings can shift a little. Pin the version your dashboard started on and move the pin on purpose: see [Pin versions in CI](/docs/cookbook/pin-versions-in-ci/).
- **Your own topic.** Point the stance pick at the change you track, such as a new delivery policy, and keep a way out for texts that do not mention it.
- **Which competitor.** When you need the name, ask a `pick` with your competitors as options, plus `none` and `other`.
