A verdict came back this morning with "score": 49 in it. Not moderately interesting, not probably one to watch. Forty-nine, on a scale that runs 0 to 100, against a gate at 60 that is the same 60 it was last quarter and will be the same 60 next year.
That sounds like a small thing to build a product around. It isn't. A person reading a release does several jobs at once without noticing: recognising the company, remembering that guidance was already given in January, weighing the adjectives against the numbers, deciding in about two seconds whether any of it is worth a second read. Software does none of that for free. Every one of those judgements an agent has to make from raw prose is an inference call you're paying for, a place the reasoning can go sideways, and a branch in your code that can't be tested with an assertion. The design goal for everything below is to move those judgements to our side of the wire and hand back something a program can compare.
Five integers instead of a paragraph
The actionability head returns five sub-scores, each an integer 0 to 10, and a compound score 0 to 100 built from them. novelty, materiality, surprise, specificity, directness. Same names every time, same ranges every time, present on every verdict whether the item is a merger or a car park announcement.
An agent branching on actionability.score >= 60 needs no language understanding whatsoever. It needs an integer comparison. The reasoning string is there for the moment a human asks why, and it's the field I'd read first if I were the human, but it is not the field your control flow should hang off. The distinction matters more than it sounds like it should: prose is a thing your agent has to be right about, and a number is a thing it merely has to read.
When the gate opens, the sentiment block adds short_direction and long_direction, both signed integers from −5 to +5, plus conviction on its own 0 to 100 scale and three sub-scores under it. Direction picks the side. Conviction sizes it. Keeping those in separate fields is the whole reason a systematic caller can put one into the sign of a position and the other into a multiplier, which is not something you can express when tone arrives as a single blended number.
Now the part that trips up every first integration, and it's deliberate. Under the default score_sentiment: "if_actionable", an item that scores below the gate comes back with no sentiment key at all. Not null. Absent. Write "sentiment" in verdict rather than reaching straight for verdict.sentiment.short_direction, and your agent gets to report below the actionability gate, which is a true statement about the world. A null invites a default, and the default people reach for is zero, which reads as a confident call of no movement. If you want tone on everything including the recaps, POST /v1/score takes score_sentiment: "always" and runs both heads regardless.
Narrow before you read
search_feeds filters on our side: symbol, actionability, sentiment_min and sentiment_max, exchange, aggregator, since, four tag dimensions, and a limit up to 100. An agent that asks for actionable US biotech since 06:00 and reads four items beats one that pulls two hundred and sorts them itself, and it wins on more than wall clock, because the ranking work already happened here on scores you didn't spend anything to compute.
Two things to get right on the first attempt rather than the fifth.
The sentiment filters speak the 0 to 10 scale with 5 as neutral, so sentiment_min=7 selects for bullish, not for high conviction. Conviction lives on the verdict object and never on the query string. And tag values are a closed vocabulary, so call list_tags once at the start of a session and cache what it gives you. The dimensions are corp_activity, market, market_country, economic_category. An agent left to guess will confidently invent earnings_report where the dimension takes earnings, and a wrong value doesn't raise anything. It returns zero rows, which looks precisely like a quiet morning.
A link is the load-bearing field
Every item carries id, title, source, published_at and link. The link is the one I'd defend hardest in a design review. A score is an opinion, and an opinion an agent can't attribute is worth roughly nothing to the person reading the agent's output at 07:15 with a position on. I have yet to meet a portfolio manager who accepted a number without asking where it came from. An agent that can't answer that question is a very fast intern.
So the shape that works is: search narrow, cite the link, and when the answer needs depth, call get_feed with the id. That returns the full object, per-symbol analysis and comments and tags included, which is where the reasoning behind a score lives. Two calls, both checkable.
This is also why the MCP server exists as a real integration rather than a suggestion that you point something at our website. @forecite/mcp runs locally over stdio with your key in the environment, and it registers eight tools: search_feeds, get_feed, score_artifact, list_symbols, get_symbol, list_tags, list_sources, account. Your agent gets typed parameters with descriptions on them and a validated response shape. It does not get an HTML page and a hope.
Budget the loop
Scoring your own text is the expensive call, and it's metered per day rather than per minute, which means an autonomous loop can be budgeted rather than throttled.
Feed reads run against a separate counter, 5,000 a day on Starter and uncapped from Pro upward, and they reset on the UTC day rather than a rolling window. So the cheap move is to read verdicts that already exist and reserve scoring for text that isn't in the feed: an internal note, a transcript, a broker PDF someone pasted into a channel.
The account tool reports tier, limits, and today's usage split into feed and score. Call it at the top of a run and again at the bottom. A loop that decides how much work to do needs to know what it has left before it starts spending, and the difference between an agent that checks and an agent that discovers is about forty minutes of somebody's afternoon.
None of this is generosity about limits. It's that the numbers are legible to a program at all, which is the same argument as the 0 to 100 scale and the stable field names. A 49 from March means what a 49 means today. Write your threshold once, and leave it alone.