---
title: Moderate product reviews with Jev, TanStack AI, and AI Gateway
description: Moderate product reviews for an e-commerce storefront with Jev from TypeSafe AI and TanStack AI's `decide()` function. The `@tanstack/ai-vercel-gateway` adapter returns typed choices, scores, and boolean probabilities through Vercel AI Gateway, so your code can publish clear reviews and hold flagged ones.
url: "https://vercel.com/kb/guide/moderate-product-reviews-jev-tanstack-ai"
published: 2026-09-22
last_updated: 2026-09-22
authors: Ben Sabic
related_resources:
  - title: "What is Jev, TypeSafe AI's System One model?"
    url: "https://vercel.com/i/what-is-jev"
    description: "Jev is TypeSafe AI's System One model, built to return typed decisions with probabilities from supplied state and questions."
  - title: "7 practical Jev use cases for AI applications"
    url: "https://vercel.com/i/jev-use-cases"
    description: "Explore seven Jev use cases, including form routing, ticket prioritization, tool approvals, document classification, and response evaluation."
  - title: "How to classify, route, and score with Jev and AI SDK"
    url: "https://vercel.com/kb/guide/typesafe-jev-and-ai-sdk"
    description: "Use Jev from TypeSafe AI with AI SDK's experimental evaluate API to classify, route, score, and verify inside your application. Jev returns typed choices, scores, and boolean probabilities through AI Gateway."
  - title: "Route form submissions with Jev and AI SDK"
    url: "https://vercel.com/kb/guide/jev-ai-sdk-form-router"
    description: "Route form submissions to the right team with the Jev x AI SDK Form Router template. Jev routes clear cases and a fallback model decides uncertain ones."
  - title: "How to automatically approve tool calls in eve with Jev"
    url: "https://vercel.com/kb/guide/auto-approve-tool-calls-eve-jev"
    description: "Use Jev to review tool calls in eve, allow routine actions, and request human approval when needed. Configure the policy and test its failure paths."
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Your storefront has a decision to make every time a customer submits a review. Before it appears under the product, something has to decide whether it's spam, whether it exposes personal information, and which section it belongs in. Keyword rules can't tell a shipping complaint from a product defect, or a discount code in spam from one a customer mentions in passing. Asking a language model means waiting on generated text and parsing it before your code can act.

[Jev](https://vercel.com/i/what-is-jev), a System One model from TypeSafe AI, evaluates supplied state against typed questions and returns choices, scores, and boolean probabilities without generating prose. TanStack AI exposes those answers through its `decide()` function, and the `@tanstack/ai-vercel-gateway` adapter sends each call through Vercel [AI Gateway](https://vercel.com/ai-gateway), so evaluation requests share credentials, logs, budgets, and data controls with the rest of your Gateway traffic.

Your application decides what happens next, so a topic choice can place a review in the right section, a sentiment score can feed a rating summary, and a spam flag or uncertain answer can hold the review for a moderator. Keeping those rules in code lets you change how the application responds without redefining what you ask the model to assess.

## Overview

In this guide, you'll learn how to:

- Ask a single yes-or-no question about a piece of state
  
- Answer several typed questions in one request from a TanStack Start app
  
- Branch on probability and confidence so clear reviews publish automatically and flagged or uncertain ones go to a moderator
  
- Unit-test that branching without calling Jev
  
- Set Gateway data controls, cancel in-flight requests, and track token usage
  

## Prerequisites

Before you begin, make sure you have:

- A [Vercel account](https://vercel.com/signup)
  
- [Vercel CLI](https://vercel.com/docs/cli) installed (`npm i -g vercel`)
  
- Node.js 22+ and a package manager (e.g., [pnpm](https://pnpm.io/))
  
- An existing [TanStack Start](https://vercel.com/kb/guide/deploy-a-tanstack-start-app-to-vercel) project
  

> If you're new to Jev, read [how to classify, route, and score with Jev](https://vercel.com/kb/guide/typesafe-jev-and-ai-sdk) first. That guide covers how Jev differs from a language model in more depth. This guide focuses on the TanStack AI API.

## How TanStack AI evaluates with Jev

TanStack AI's `decide()` function takes an `adapter` that names the model and provider, a `state` to evaluate, and a map of `questions` built with the `choice()`, `score()`, and `boolean()` helpers. Unlike `chat()`, it doesn't stream. The call resolves to one object with a typed answer for every question key.

The `@tanstack/ai-vercel-gateway` adapter provides `vercelGatewayDecider`, which sends the request to AI Gateway's evaluation endpoint.

TanStack AI also ships an adapter that calls TypeSafe directly. Both implement the same evaluate activity, so switching means changing the adapter and nothing else.

### Three question helpers

Each TanStack AI helper corresponds to a Jev question type. Answers appear directly on the result under their question keys, so `questions.topic` becomes `result.topic`.

The `meta` key holds usage and model information and cannot be used as a question name.

| Helper      | What it does                               | Configuration                                                   | Answer fields                                                                    |
| ----------- | ------------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `choice()`  | Picks one option from a named set          | `options`: map of option keys to descriptions (`null` for none) | `type`, `value`, `probability`, `probabilities`, `confidence`                    |
| `score()`   | Grades the state against an ordered rubric | `levels`: array of at least two labels, lowest to highest       | `type`, `value`, `score`, `probability`, `probabilities`, `legend`, `confidence` |
| `boolean()` | Estimates whether a statement is true      | Optional `criteria: { true, false }` descriptions               | `type`, `value`, `probability`                                                   |

Reading the answer fields:

- `**type**` is `'choice'`, `'score'`, or `'boolean'`, so you can narrow on it when handling answers generically.
  
- `**value**` is the decision. For `choice()`, it's the selected option key, typed as a union of your keys. For `score()`, it's the label of the level nearest to `score`. For `boolean()`, it's `true` when `probability` is 0.5 or higher.
  
- `**probability**` describes the returned `value` for choice and score answers. For boolean answers, it estimates whether the statement is true, so `0.98` is a strong yes and `0.02` is a strong no.
  
- `**probabilities**` contains the full distribution, using option names for choice answers and level indices for score answers. The score answer's `legend` maps those indices to labels.
  
- `**score**` is a fractional position on the level scale, indexed from zero. `1.08` falls between the second and third levels, and `value` names the nearest.
  
- `**confidence**` is TypeSafe's statistic for how concentrated the distribution is, from `0` (spread evenly) to `1` (all on one answer). It's returned for `choice()` and `score()` answers, not for `boolean()`.
  

The AI SDK's `experimental_evaluate` returns answers under `result.answers` and TypeSafe's confidence statistics in `providerMetadata`. TanStack AI's `decide()` places answers directly on the result, with `confidence` included on choice and score answers.

You can adapt the support-ticket example in [the AI SDK guide](https://vercel.com/kb/guide/typesafe-jev-and-ai-sdk) to compare how the two APIs express the same routing decisions.

## Steps

### 1\. Install TanStack AI and the AI Gateway adapter

Install the core package and the adapter:

**pnpm**

```bash
pnpm i @tanstack/ai @tanstack/ai-vercel-gateway
```

**npm**

```bash
npm i @tanstack/ai @tanstack/ai-vercel-gateway
```

**yarn**

```bash
yarn add @tanstack/ai @tanstack/ai-vercel-gateway
```

**bun**

```bash
bun add @tanstack/ai @tanstack/ai-vercel-gateway
```

`@tanstack/ai` exports `decide()` and the question helpers.

`@tanstack/ai-vercel-gateway` exports `vercelGatewayDecider` along with the chat, embedding, image, and summarize adapters covered in [Using TanStack AI with Vercel AI Gateway](https://vercel.com/kb/guide/tanstack-ai-vercel-ai-gateway).

### 2\. Authenticate with AI Gateway

The adapter reads `AI_GATEWAY_API_KEY` from your environment. When that variable isn't set, it falls back to `VERCEL_OIDC_TOKEN`. Deployments on Vercel receive an OIDC token automatically, so no key is needed in production.

For local development, connect to AI Gateway using one of the following methods. You can pull an OIDC token from your linked Vercel project or add an AI Gateway API key to `.env.local`.

#### Option 1: Vercel OIDC

Link the directory to your Vercel project and pull its development environment variables. This writes a `VERCEL_OIDC_TOKEN` to `.env.local`:

```bash
vercel link
vercel env pull .env.local
```

The local token expires after 12 hours. Re-run `vercel env pull .env.local` when a request returns an unauthorized error.

#### Option 2: AI Gateway API key

API keys work anywhere, including CI and servers outside Vercel. Create a key in the [Vercel dashboard](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fai-gateway%2Fapi-keys) and set it as `AI_GATEWAY_API_KEY` in `.env.local`:

```plaintext
AI_GATEWAY_API_KEY=YOUR_KEY_HERE
```

To pass a key explicitly instead, use the `create*` factory. This is useful when the key comes from a service like [Vercel Connect](https://vercel.com/kb/guide/tanstack-ai-vercel-ai-gateway#2.-create-an-api-key) rather than an environment variable:

```typescript
import { createVercelGatewayDecider } from '@tanstack/ai-vercel-gateway'

const adapter = createVercelGatewayDecider('typesafe-ai/jev', process.env.AI_GATEWAY_API_KEY!)
```

Whichever option you choose, keep the credential on the server. `decide()` runs in server code and is called from the browser over `fetch`.

### 3\. Ask one boolean question

Start by asking whether a customer review contains promotional content. Pass the review text as `state` and name the question `isPromotional` to read its typed answer from `result.isPromotional`.

```typescript
import { decide, boolean } from '@tanstack/ai'
import { vercelGatewayDecider } from '@tanstack/ai-vercel-gateway'

export async function isPromotional(review: string) {
  const result = await decide({
    adapter: vercelGatewayDecider('typesafe-ai/jev'),
    state: review,
    questions: {
      isPromotional: boolean({
        instructions: 'Is this review promotional content rather than a customer opinion?',
        criteria: {
          true: 'The text advertises another product, store, or service, or includes links, discount codes, or contact details.',
          false: 'The text describes the reviewer\'s own experience with the product.',
        },
      }),
    },
  })

  return result.isPromotional.probability
}
```

For a review like `'Skip this one. Get the real thing 40% off at dealz-outlet dot com with code SAVE40.'`, expect `isPromotional()` to return a probability close to `1`, with `result.isPromotional.value` set to `true`.

The `criteria` on a boolean question are optional, but they sharpen the decision by telling the model exactly what counts as true and false.

### 4\. Answer several questions against structured state in one request

Jev evaluates the questions in a request independently and in parallel. You can combine `choice()`, `score()`, and `boolean()` questions against the same state.

Pass a string, object, or array as `state` without serializing it yourself. An array, such as a message history, provides one shared context for all questions; unrelated inputs need separate evaluations.

On a headless storefront, reviews can reach your server route from a custom form built with [Next.js Commerce](https://vercel.com/templates/next.js/nextjs-commerce) or [Hydrogen](https://vercel.com/blog/vercel-and-shopify-are-rebuilding-hydrogen), where Shopify or another commerce backend serves the product data, or through a webhook from a reviews app.

The following TanStack Start server route reuses an adapter created at module scope to evaluate each review's topic, sentiment, and two content flags in one call.

```typescript
import { decide, choice, score, boolean } from '@tanstack/ai'
import { vercelGatewayDecider } from '@tanstack/ai-vercel-gateway'
import { createFileRoute } from '@tanstack/react-router'

export type Review = {
  productName: string
  rating: number // 1 to 5 stars, chosen by the customer
  title: string
  body: string
}

function parseReview(body: unknown): Review | undefined {
  if (typeof body !== 'object' || body === null || !('review' in body)) return undefined
  const r = (body as { review: unknown }).review
  if (typeof r !== 'object' || r === null) return undefined
  const { productName, rating, title, body: text } = r as Record<string, unknown>
  if (typeof productName !== 'string' || typeof title !== 'string' || typeof text !== 'string') return undefined
  if (typeof rating !== 'number' || !Number.isInteger(rating) || rating < 1 || rating > 5) return undefined
  return { productName, rating, title, body: text }
}

const ADAPTER = vercelGatewayDecider('typesafe-ai/jev')

export function evaluateReview(review: Review) {
  return decide({
    adapter: ADAPTER,
    state: review,
    questions: {
      topic: choice({
        instructions: 'What is this review mainly about?',
        options: {
          productQuality: 'The product itself, including how it works, its build, fit, or durability',
          shipping: 'Delivery speed, packaging, or damage in transit',
          customerService: 'Support interactions, returns, or refunds',
          notAReview: 'A question, unrelated text, or content that is not about a purchase',
        },
      }),
      sentiment: score({
        instructions: 'How does the reviewer feel about their purchase, based on the text?',
        levels: [
          'Very negative, would not buy again',
          'Mostly negative with some positives',
          'Mixed or neutral',
          'Mostly positive with minor complaints',
          'Very positive, would recommend',
        ],
      }),
      isPromotional: boolean({
        instructions: 'Is this review promotional content rather than a customer opinion?',
      }),
      mentionsPersonalInfo: boolean({
        instructions: 'Does the review include personal information such as full names, phone numbers, email addresses, or order numbers?',
      }),
    },
  })
}

export type ReviewEvaluation = Awaited<ReturnType<typeof evaluateReview>>

export const Route = createFileRoute('/api/moderate')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        let body: unknown
        try {
          body = await request.json()
        } catch {
          return new Response('Request body must be JSON', { status: 400 })
        }
        const review = parseReview(body)
        if (!review) {
          return new Response('Invalid review', { status: 400 })
        }
        return Response.json(await evaluateReview(review))
      },
    },
  },
})
```

TanStack Start exposes `src/routes/api.moderate.ts` at `/api/moderate`.

`parseReview` checks the four fields Jev will see and rejects anything else with a `400`. For a production form, a schema library such as Zod does the same job with less code. Whether the reviewer is a verified purchaser belongs to your commerce backend, not the request body, so look it up there when your publish rules need it.

The supplied state includes the customer's star rating, while the sentiment question asks Jev to assess the review text. Comparing them can reveal mismatches, such as a five-star review describing a poor experience with the product.

Start the dev server and send a product review:

**pnpm**

```bash
pnpm dev
```

**npm**

```bash
npm run dev
```

**yarn**

```bash
yarn dev
```

**bun**

```bash
bun dev
```
```bash
curl -X POST http://localhost:3000/api/moderate \
  -H "Content-Type: application/json" \
  -d '{
    "review": {
      "productName": "Trailhead 40L Backpack",
      "rating": 2,
      "title": "Straps frayed within a month",
      "body": "The bag looks great, but the shoulder straps started fraying after three weeks of commuting. Shipping was fast, no complaints there."
    }
  }'
```

The response contains a typed answer under each question's key, with the resolved model and token usage in `meta`. Jev reports `completionTokens` but bills only for input tokens. The example below shows possible values for this review, which the policy in step five maps to a moderation decision using fixed thresholds:

```json
{
  "topic": {
    "type": "choice",
    "value": "productQuality",
    "probability": 0.89,
    "confidence": 0.84,
    "probabilities": { "productQuality": 0.89, "shipping": 0.09, "customerService": 0.01, "notAReview": 0.01 }
  },
  "sentiment": {
    "type": "score",
    "value": "Mostly negative with some positives",
    "score": 1.08,
    "probability": 0.71,
    "confidence": 0.66,
    "legend": {
      "0": "Very negative, would not buy again",
      "1": "Mostly negative with some positives",
      "2": "Mixed or neutral",
      "3": "Mostly positive with minor complaints",
      "4": "Very positive, would recommend"
    },
    "probabilities": { "0": 0.12, "1": 0.71, "2": 0.14, "3": 0.03, "4": 0 }
  },
  "isPromotional": { "type": "boolean", "value": false, "probability": 0.02 },
  "mentionsPersonalInfo": { "type": "boolean", "value": false, "probability": 0.01 },
  "meta": {
    "model": "typesafe-ai/jev",
    "usage": { "promptTokens": 312, "completionTokens": 21, "totalTokens": 333 }
  }
}
```

Reading the response:

- `topic.value` is typed from the declared option keys, so TypeScript flags comparisons with an unknown option, such as `value === 'quality'`.
  
- `topic.probabilities` covers every option, and the selected value always has the highest probability. Here the review mentions shipping, and the distribution reflects that without changing the answer.
  
- `sentiment.value` names the level nearest to `sentiment.score`, which isn't always the level with the highest probability. `sentiment.legend` maps the index keys in `sentiment.probabilities` back to their labels.
  
- `isPromotional.probability` is the estimated probability that the review is promotional, not a confidence in the answer.
  

### 5\. Branch on probability and confidence

Use the returned statistics to decide which reviews can publish automatically and which need human review. Set stricter thresholds where a wrong decision has greater consequences.

Publishing spam or someone's phone number is costly and holding a review for a moderator is cheap, so the two flags use a low floor while the topic gate is stricter.

The moderation policy below uses three paths:

| Condition                                                                                                       | What your code does                             |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `isPromotional` or `mentionsPersonalInfo` probability is ≥ 0.3                                                  | Hold the review and record which flag tripped   |
| Topic `confidence` is below 0.6, the selected option's `probability` is below 0.7, or the topic is `notAReview` | Hold the review as unclear or off-topic         |
| Otherwise                                                                                                       | Publish the review in the section for its topic |

Keep the moderation policy in a pure function that accepts the evaluation result. Separating it from `decide()` lets the tests in step six check policy decisions using fixed answer objects, without calling Jev.

```typescript
import type { ReviewEvaluation } from '../routes/api.moderate'

type Answers = Pick<ReviewEvaluation, 'topic' | 'sentiment' | 'isPromotional' | 'mentionsPersonalInfo'>

export function moderateReview({ topic, sentiment, isPromotional, mentionsPersonalInfo }: Answers) {
  // Publishing spam or personal data is costly. Holding a review is cheap.
  if (isPromotional.probability >= 0.3) {
    return { action: 'hold' as const, reason: 'possible promotional content' }
  }
  if (mentionsPersonalInfo.probability >= 0.3) {
    return { action: 'hold' as const, reason: 'possible personal information' }
  }

  // Below either floor, the model can't tell what the review is about. Don't guess.
  if (topic.confidence < 0.6 || topic.probability < 0.7 || topic.value === 'notAReview') {
    return { action: 'hold' as const, reason: 'unclear or off-topic review' }
  }

  return {
    action: 'publish' as const,
    section: topic.value, // narrowed to 'productQuality' | 'shipping' | 'customerService'
    sentiment: sentiment.value,
  }
}
```

Then update the `POST` handler from [step four](#4.-answer-several-questions-against-structured-state-in-one-request) to return the moderation decision instead of the raw answers:

```typescript
// src/routes/api.moderate.ts (handler only)
import { moderateReview } from '../lib/moderate-review'

// ...inside createFileRoute
POST: async ({ request }) => {
  let body: unknown
  try {
    body = await request.json()
  } catch {
    return new Response('Request body must be JSON', { status: 400 })
  }
  const review = parseReview(body)
  if (!review) {
    return new Response('Invalid review', { status: 400 })
  }
  const evaluation = await evaluateReview(review)
  return Response.json(moderateReview(evaluation))
},
```

Sending the same `curl` request from [step four](#4.-answer-several-questions-against-structured-state-in-one-request) now returns a decision:

```json
{
  "action": "publish",
  "section": "productQuality",
  "sentiment": "Mostly negative with some positives"
}
```

`action: 'publish'` means the evaluation passed the moderation thresholds above. Whether the review goes live still depends on rules Jev doesn't see, such as a verified-purchase requirement, per-account review limits, or a product that has been delisted, so keep those as a second check in your code.

Treat the example thresholds as starting points. Calibration describes how predicted probabilities match observed outcomes across many examples; it doesn't guarantee an individual answer is correct. Run labeled reviews through the same questions, then [choose cutoffs based on the errors your workflow can tolerate](https://vercel.com/i/jev-probabilities-and-thresholds).

### 6\. Test the moderation policy without calling Jev

Because `moderateReview` takes a result object rather than a review, you can test each branch with fixed answers and no network call.

Add Vitest as a development dependency:

**pnpm**

```bash
pnpm add -D vitest
```

**npm**

```bash
npm install -D vitest
```

**yarn**

```bash
yarn add -D vitest
```

**bun**

```bash
bun add -d vitest
```

The `clearAnswers` fixture is typed against `ReviewEvaluation`, so TypeScript catches an answer shape that drifts from the questions in [step four](#4.-answer-several-questions-against-structured-state-in-one-request). Each hold test isolates one condition, so a test fails if that condition is removed from the policy, and the boundary tests pin the `0.3`, `0.6`, and `0.7` cutoffs.

```typescript
import { describe, expect, it } from 'vitest'
import type { ReviewEvaluation } from '../routes/api.moderate'
import { moderateReview } from './moderate-review'

type Answers = Pick<ReviewEvaluation, 'topic' | 'sentiment' | 'isPromotional' | 'mentionsPersonalInfo'>

const legend = {
  0: 'Very negative, would not buy again',
  1: 'Mostly negative with some positives',
  2: 'Mixed or neutral',
  3: 'Mostly positive with minor complaints',
  4: 'Very positive, would recommend',
}

const clearAnswers: Answers = {
  topic: {
    type: 'choice',
    value: 'productQuality',
    probability: 0.89,
    confidence: 0.84,
    probabilities: { productQuality: 0.89, shipping: 0.09, customerService: 0.01, notAReview: 0.01 },
  },
  sentiment: {
    type: 'score',
    value: 'Mostly negative with some positives',
    score: 1.08,
    probability: 0.71,
    confidence: 0.66,
    legend,
    probabilities: { 0: 0.12, 1: 0.71, 2: 0.14, 3: 0.03, 4: 0 },
  },
  isPromotional: { type: 'boolean', value: false, probability: 0.02 },
  mentionsPersonalInfo: { type: 'boolean', value: false, probability: 0.01 },
}

describe('moderateReview', () => {
  it('publishes when the topic is clear and no flag trips', () => {
    expect(moderateReview(clearAnswers)).toEqual({
      action: 'publish',
      section: 'productQuality',
      sentiment: 'Mostly negative with some positives',
    })
  })

  it('publishes at exactly the confidence and probability floors', () => {
    const decision = moderateReview({
      ...clearAnswers,
      topic: { ...clearAnswers.topic, confidence: 0.6, probability: 0.7 },
    })

    expect(decision.action).toBe('publish')
  })

  it('holds when the promotional flag reaches its floor', () => {
    const decision = moderateReview({
      ...clearAnswers,
      isPromotional: { type: 'boolean', value: false, probability: 0.3 },
    })

    expect(decision).toEqual({ action: 'hold', reason: 'possible promotional content' })
  })

  it('holds when the personal information flag trips', () => {
    const decision = moderateReview({
      ...clearAnswers,
      mentionsPersonalInfo: { type: 'boolean', value: true, probability: 0.9 },
    })

    expect(decision).toEqual({ action: 'hold', reason: 'possible personal information' })
  })

  it('holds when topic confidence alone is too low', () => {
    const decision = moderateReview({
      ...clearAnswers,
      topic: { ...clearAnswers.topic, confidence: 0.59 },
    })

    expect(decision).toEqual({ action: 'hold', reason: 'unclear or off-topic review' })
  })

  it('holds when the selected topic probability alone is too low', () => {
    const decision = moderateReview({
      ...clearAnswers,
      topic: { ...clearAnswers.topic, probability: 0.69 },
    })

    expect(decision).toEqual({ action: 'hold', reason: 'unclear or off-topic review' })
  })

  it('holds when the text is not a review, even with high confidence', () => {
    const decision = moderateReview({
      ...clearAnswers,
      topic: {
        type: 'choice',
        value: 'notAReview',
        probability: 0.95,
        confidence: 0.93,
        probabilities: { productQuality: 0.02, shipping: 0.02, customerService: 0.01, notAReview: 0.95 },
      },
    })

    expect(decision).toEqual({ action: 'hold', reason: 'unclear or off-topic review' })
  })
})
```

Run the tests:

**pnpm**

```bash
pnpm vitest run src/lib/moderate-review.test.ts
```

**npm**

```bash
npx vitest run src/lib/moderate-review.test.ts
```

**yarn**

```bash
yarn vitest run src/lib/moderate-review.test.ts
```

**bun**

```bash
bunx vitest run src/lib/moderate-review.test.ts
```

**Expected output:**

```text
✓ src/lib/moderate-review.test.ts (7 tests)

 Test Files  1 passed (1)
      Tests  7 passed (7)
```

Testing the thresholds this way is separate from checking whether the thresholds are right for your data. For that, run real reviews with known moderation outcomes through Jev and compare its probabilities to what happened.

### 7\. Set Gateway options, cancel requests, and track usage

Alongside `adapter`, `state`, and `questions`, `decide()` accepts options for data controls, cancellation, and monitoring:

- Set `modelOptions.gateway` to apply controls such as [Zero Data Retention](https://vercel.com/docs/ai-gateway/security-and-compliance/zdr) and [No Training](https://vercel.com/docs/ai-gateway/security-and-compliance/disallow-prompt-training). The adapter forwards these settings as `providerOptions.gateway`.
  
- Pass an `abortSignal` to cancel an in-flight request when your application aborts it or a timeout expires.
  
- Add observe-only `middleware` to track completion, errors, and cancellation. Its `onUsage` hook receives the token counts for each call.
  

Evaluation requests also appear in AI Gateway [logs](https://vercel.com/docs/ai-gateway/observability-and-spend/logs) and count toward [budgets](https://vercel.com/docs/ai-gateway/observability-and-spend/budgets).

```typescript
import { decide, boolean } from '@tanstack/ai'
import { vercelGatewayDecider } from '@tanstack/ai-vercel-gateway'

const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)

const result = await decide({
  adapter: vercelGatewayDecider('typesafe-ai/jev'),
  state: 'Skip this one. Get the real thing 40% off at dealz-outlet dot com with code SAVE40.',
  questions: {
    isPromotional: boolean({ instructions: 'Is this review promotional content rather than a customer opinion?' }),
  },
  modelOptions: {
    gateway: {
      disallowPromptTraining: true,
      zeroDataRetention: true,
    },
  },
  abortSignal: controller.signal,
  middleware: [
    {
      name: 'usage-logger',
      onUsage: (_ctx, usage) => {
        console.log(`prompt tokens: ${usage.promptTokens}`)
      },
    },
  ],
})

console.log(result.isPromotional.value)
```

Jev charges for input tokens only, so `promptTokens` is what you're billed for. To emit OpenTelemetry spans for evaluate calls instead of logging, pass TanStack AI's `otelMiddleware()` in the same `middleware` array.

## Other storefront decisions that fit a typed question

Other e-commerce tasks also involve interpreting customer requests or product information to choose from a defined set of outcomes.

The examples below use Jev to assess the supplied record, while application code applies thresholds and business rules to decide what happens next.

| Decision                         | State                                       | Questions                                                                                                                        | Your code then                                                          |
| -------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Route a return request           | Order details and the customer's message    | `choice()` for reason (defective, wrong item, changed mind, not received); `boolean()` for whether the item is described as used | Auto-approve within policy, otherwise queue for an agent                |
| Triage a product question        | The question and product metadata           | `choice()` for intent (fit or sizing, compatibility, availability, shipping); `boolean()` for whether it's a support issue       | Answer from product data, or hand off to support                        |
| Classify a search query          | The raw query and recent catalog terms      | `choice()` for intent (browse a category, find a specific product, check an order); `score()` for how specific the query is      | Pick the results page layout, or redirect to order lookup               |
| Categorize a new catalog item    | Supplier title, description, and attributes | `choice()` over your leaf categories, or one call per level that passes the previous selection in `state`                        | Place the product, or hold it for a merchandiser when confidence is low |
| Flag a risky order note          | Order note and shipping details             | `boolean()` for a delivery instruction that conflicts with the address; `boolean()` for a request to alter the invoice           | Pause fulfillment for review                                            |
| Score a support chat for handoff | The conversation so far                     | `score()` for customer frustration; `boolean()` for a cancellation request                                                       | Escalate to a person before the customer asks                           |

Your application uses Jev's classifications and scores alongside inventory, account history, and business rules to decide whether to approve a return, change an order, or escalate a request. Explore [seven practical Jev use cases](https://vercel.com/i/jev-use-cases) for patterns outside commerce.

## Best practices

### Ask atomic questions and combine them in code

Jev works best when each question asks one well-scoped thing that a knowledgeable person could answer in a few seconds. If a question requires extended reasoning or weighs several independent factors, split it into one question per factor and combine the answers with your own logic. Each question is evaluated independently, so adding one doesn't change the answers to the others.

| Instead of                           | Ask                                                                                                    | Then                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| "Rate this pull request"             | 3 `score()` questions: test coverage, documentation, description clarity                               | Weight the three scores by importance in code                  |
| "Is this ticket a priority?"         | A `boolean()` for urgency, a `score()` for business impact, a `choice()` for customer tier             | Compute ticket priority from all three answers                 |
| "Should the agent run this command?" | A `boolean()` for destructive intent, a `boolean()` for touching production, a `choice()` for category | Require confirmation when any risk flag exceeds your threshold |

### Describe options and levels instead of labeling them

`choice()` options are a map of keys to descriptions, and `score()` levels are ordered descriptions from lowest to highest. Writing `'Mostly negative with some positives'` gives the model far more to match against than `'negative'`. When an option key needs no description, pass `null`.

### Keep state focused

Input tokens are the only thing you pay for, so pass the fields the decision depends on rather than an entire record. For a review, that's the product name, title, body, and rating, not the customer's account or order history. `state` accepts an object, so you can select fields without serializing them yourself.

### Set thresholds per action, not per model

Read-only actions like showing a screen can tolerate a wrong guess, so a probability of 0.7 might be enough. Destructive actions need a higher bar, closer to 0.9 or above. Set the numbers from reviews you've already moderated, weighing that publishing spam costs more than holding a good review, and send anything under the threshold to a moderator rather than rejecting it.

Keep [classification separate from authorization](https://vercel.com/i/jev-agent-control). Jev can tell you that a review looks promotional or that a command looks destructive. Whether to publish the review or run the command depends on rules Jev doesn't see, such as account status, policy, and permissions, and those checks apply whatever the probability.

### Create the adapter once

Construct `vercelGatewayDecider('typesafe-ai/jev')` at module scope rather than inside a request handler. The adapter holds no per-request state, and reusing it avoids repeating the environment lookup on every call.

## Troubleshooting

### Authentication errors (`401` or `403`)

Neither `AI_GATEWAY_API_KEY` nor `VERCEL_OIDC_TOKEN` is set, or the OIDC token has expired. Set an API key, or run `vercel env pull .env.local` to refresh the token, using `vercel link` first if the directory isn't linked to a project. If a `403` persists, check your access to the linked project and AI Gateway.

### `decide()` throws before any request is made

Make sure `questions` contains at least one entry. Add a question if the map is empty, and rename any entry using the reserved key `meta`.

### Evaluation is unavailable through an OpenAI-compatible client

For Jev evaluation in TanStack AI, use `vercelGatewayDecider` with `decide()`. The `vercelGatewayText` adapter only handles text generation.

AI Gateway also supports evaluation through AI SDK, the [native HTTP API](https://vercel.com/docs/ai-gateway/modalities/evaluation#http-api), and the [TypeSafe-compatible API](https://vercel.com/docs/ai-gateway/sdks-and-apis/typesafe). Its OpenAI-, Anthropic-, and Cohere-compatible endpoints don't support evaluation.

### The request never completes

Evaluation runs on the server and needs a credential, so a `decide()` call from browser code fails. Wrap the call in a server route and call it over `fetch`, as in [step four](#4.-answer-several-questions-against-structured-state-in-one-request). For long-running requests, pass an `abortSignal` so your handler fails fast instead of holding the connection open.

### Answers seem overconfident on your data

Run labeled examples through the same questions and compare predicted probabilities with observed outcomes. Inspect where errors cluster, such as reviews about damaged packaging landing in `productQuality` instead of `shipping`, revise unclear option and level descriptions, and evaluate the updated questions before choosing new thresholds.

## Next steps

- Read [How to classify, route, and score with Jev and AI SDK](https://vercel.com/kb/guide/typesafe-jev-and-ai-sdk) for the same patterns with `experimental_evaluate`, including how Jev differs from a language model
  
- Follow [Using TanStack AI with Vercel AI Gateway](https://vercel.com/kb/guide/tanstack-ai-vercel-ai-gateway) to stream chat, generate embeddings, and configure provider routing with the same adapter package
  
- See the [TanStack AI Vercel AI Gateway adapter reference](https://tanstack.com/ai/latest/docs/adapters/vercel-gateway) for `createVercelGatewayDecider` and Gateway routing options
  
- Read [What is Jev, TypeSafe AI's System One model?](https://vercel.com/i/what-is-jev) and [When should you use Jev?](https://vercel.com/i/when-to-use-jev) to find other decisions in your application that fit a typed question
  
- [Evaluate Jev's probabilities and choose a threshold](https://vercel.com/i/jev-probabilities-and-thresholds) using labeled examples from your own workflow
  
- See the [AI Gateway evaluation docs](https://vercel.com/docs/ai-gateway/modalities/evaluation) and the [Jev model page](https://vercel.com/ai-gateway/models/jev) for pricing, limits, and the HTTP API

## Related Resources

- [What is Jev, TypeSafe AI's System One model?](https://vercel.com/i/what-is-jev): Jev is TypeSafe AI's System One model, built to return typed decisions with probabilities from supplied state and questions.
- [7 practical Jev use cases for AI applications](https://vercel.com/i/jev-use-cases): Explore seven Jev use cases, including form routing, ticket prioritization, tool approvals, document classification, and response evaluation.
- [How to classify, route, and score with Jev and AI SDK](https://vercel.com/kb/guide/typesafe-jev-and-ai-sdk): Use Jev from TypeSafe AI with AI SDK's experimental evaluate API to classify, route, score, and verify inside your application. Jev returns typed choices, scores, and boolean probabilities through AI Gateway.
- [Route form submissions with Jev and AI SDK](https://vercel.com/kb/guide/jev-ai-sdk-form-router): Route form submissions to the right team with the Jev x AI SDK Form Router template. Jev routes clear cases and a fallback model decides uncertain ones.
- [How to automatically approve tool calls in eve with Jev](https://vercel.com/kb/guide/auto-approve-tool-calls-eve-jev): Use Jev to review tool calls in eve, allow routine actions, and request human approval when needed. Configure the policy and test its failure paths.