Monitor
evalship checks every AI call your app makes in production and emails you the moment one fails, promises something, shares private data or frustrates a user.
Connect production
- Open your repository's Monitor page on evalship and click Make a production key. evalship shows the key once.
- Send your AI calls one of three ways:
- Your coding agent. Paste the line the page gives you,
Send this app's AI calls to evalship: https://evalship.com/production.md?repo=OWNER/REPO. The agent finds your LLM calls, names each one, and adds a call after each with the HTTP client your app already has. - OpenRouter. In OpenRouter's Settings, under Broadcast, add a Webhook destination with the URL
https://evalship.com/api/v1/otlp/v1/tracesand the headerAuthorization: Bearer <your key>. Name each call withtrace: { call: "support-chat" }in its request. - OpenTelemetry. Point an OTLP/HTTP exporter (
http/protobuforhttp/json) at the same URL, with the same header. If the app already exports elsewhere, add a second exporter in code:OTEL_EXPORTER_OTLP_*variables would change both. Setevalship.callto the call's name on each LLM span. evalship keeps only the spans of AI calls, tools and agents, those withgen_ai.*,llm.*orevalship.*attributes, so the app's other spans cost nothing.
- Your coding agent. Paste the line the page gives you,
- Add the key to production's environment as
EVALSHIP_PRODUCTION_KEYand deploy. Calls show on Monitor within a minute of the first one.
Making a new key replaces the old one: calls with the old key stop arriving.
What it flags
| Issue | When |
|---|---|
| Failed | The call, or a step under it, ended in an error. |
| Promised something | The answer commits you to something a person would have to honor: a refund, a discount, a date, an exception to a policy. |
| Shared private data | The answer reveals someone else's personal data, a secret or the AI's own instructions. |
| Unsafe answer | The answer helps with something dangerous or gives health advice that could hurt. |
| Frustrated user | The user says an answer was wrong or useless, or asks for a person. |
| Refused to help | The AI declined to help. |
| No answer | The AI answered nothing. |
| Failed a check | The answer fails one of your checks for that call. |
Rules check every call. A model (Claude Haiku) checks what only reading can tell, and its flag stands only when it quotes the call word for word. When unsure, it flags nothing: a wrong alert costs more than a missed one.
Calls with the same flag in the same AI call make one issue. evalship emails your organization's owners when an issue opens, at most once every 10 minutes per repository. An issue you resolve opens again when a new call has it. An issue you ignore keeps counting, without emails.
Checks
An AI call whose answer reaches a user gets the reply checks above without any setup. For a call whose output feeds code, such as classifying or extracting, list what a good answer must be in .evalship.yml under monitor, by the call's name:
monitor:
classify-ticket:
- json
- matches: '"category": "(billing|bug|other)"'
support-chat:
- replies
- judge: "Does the answer stay on the user's question?"
| Check | Fails when the output |
|---|---|
not_empty |
is empty. |
json |
isn't valid JSON. |
contains: "text" |
doesn't contain the text. |
not_contains: "text" |
contains the text. |
matches: "regex" |
doesn't match the regular expression. |
max_chars: 500 |
is longer than that. |
judge: "question" |
gets a no from the model to your yes-or-no question. |
replies |
fails a reply check: keeps them for a call that lists its own checks. |
Naming a call's checks replaces the reply checks for that call, so list replies too to keep them. Each call takes up to 20 checks. evalship reads .evalship.yml from the default branch, within 10 minutes of a change.
Agents
An agent's run is a trace: each step a span under its parent, AI calls, tool calls and anything else. OpenTelemetry sends them as they are. From your own code, give every span of one run the same trace_id, its own span_id, and its parent's span_id as parent_span_id, and send tool calls as "kind": "tool". Monitor shows each trace as a tree, with the steps that failed or were flagged marked.
The API
POST https://evalship.com/api/v1/calls with Authorization: Bearer <your key> takes one call, or up to 100 as {"calls": [...]}, and answers 202 with {"received": n}.
| Field | |
|---|---|
call |
The AI call's name, the same each time. Required. |
input, output |
What the call got and what it answered, as strings. Required for an AI call; send "" and error when it failed. |
kind |
ai (the default), tool or step. |
session |
The id of the chat or job, so Monitor shows a call with the ones before it. |
user |
A hash of your own user id, never an email or a name. evalship hashes any other value before it stores it. |
model, duration_ms, input_tokens, output_tokens, cost |
When you have them. Cost is in dollars. |
error |
The error message, when the call failed. |
trace_id, span_id, parent_span_id |
For an agent's steps. |
at |
When the call started, in ISO 8601, within the last week. Defaults to when evalship got it. |
properties |
Anything else worth seeing next to a call, as an object of up to 4 KB. |
A call evalship can't keep answers 422 with what's wrong, and nothing of its list is kept. Sending the same span_id twice keeps it once. OpenTelemetry takes up to 1,000 spans a request: past that, the answer's partialSuccess says how many it left out. A repository locked by its plan answers 403.
Limits
| Plan | Calls and steps a month | AI calls the model checks |
|---|---|---|
| Free | 10,000 | 1,000 |
| Team | 100,000 | 10,000 |
| Pro | 500,000 | 50,000 |
| Business | 2,500,000 | 250,000 |
Calls count as they arrive. Past the first, the API answers 429 until the next month. Past the second, the rules still check every call. Input and output are kept up to 20,000 characters each, for as long as your plan keeps eval history.