# Computed block

Some endpoints add a `computed` object to every item they return: a few values derived from the item's own unified fields by fixed arithmetic. Nothing is fetched, estimated or modelled. When an input is missing the value is `null`, never a made-up `0`, so you can tell "unknown" from "zero".

```json
{
  "content": { "text": "If you're in line to vote for @bsky38.com, please stay in line! Polls close in just under 5 hours.", "createdAt": "2026-09-21T18:04:06Z", "language": "en" },
  "author": { "username": "bsky.app", "displayName": "Bluesky", "...": "..." },
  "engagement": { "likes": 768, "comments": 100, "shares": 96, "bookmarks": 40 },
  "computed": { "engagement_rate": null, "language": "en" }
}
```

This is `bluesky.post_thread` as recorded from the live API. The post has likes, comments and shares but neither a view count nor its author's follower count in its own record, so `engagement_rate` is `null`; the platform provides the language.

## The fields

| Field | Type | What it is |
| --- | --- | --- |
| `engagement_rate` | number or null | Interactions as a percentage of reach: `(likes + comments + shares) / divisor × 100`, rounded to 4 decimals. |
| `estimated_reach` | number or null | The observed view count, `engagement.views`, when it is a number of 0 or more. Nothing more: unique reach is not modelled. The name matches what the field is commonly called elsewhere. |
| `language` | string or null | A two-letter ISO 639-1 code: the one the platform provides in `content.language`, else a conservative guess from the item's text. |

An endpoint declares which of the three it adds; the block holds exactly those keys, present on every item even when every value is `null`.

## Exact rules

### engagement_rate

1. The three counts are `engagement.likes`, `engagement.comments` and `engagement.shares`. A `null` among them counts as 0 as long as at least one is a number. When all three are `null`, the rate is `null`.
2. The divisor is `engagement.views` when it is a number greater than 0; else `author.followers` when it is a number greater than 0; else there is no divisor and the rate is `null`.
3. `rate = (likes + comments + shares) / divisor × 100`, rounded to 4 decimals.
4. A result above 100 is clamped to `100`; a negative one (a negative count) is clamped to `0`. Either clamp adds a sentence to `data._warnings`.

Most single posts have no follower count in their own record and no view count, so their rate is `null` by design, as in the example above. Profile-like endpoints do not declare `engagement_rate` at all. Where a platform reports views on a post, views are the divisor; where it does not but the record carries the author's followers, followers are.

### estimated_reach

`engagement.views` when it is a number greater than or equal to 0, else `null`. It is only declared on endpoints whose platform reports views.

### language

1. If `content.language` is a string, its language part is normalised to two lower-case letters (`en-US` becomes `en`) and used as is.
2. Otherwise a deterministic guess is made over `content.text`, then `content.title`, then `author.bio`, taking the first that gives a clear answer. Text shorter than 8 characters is skipped. If at least 80% of the letters are in one script that implies a language, that language is returned: Japanese (kana), Korean (Hangul), Chinese (Han only, at least 4 characters), Thai, Hebrew, Greek, Hindi (Devanagari) and Arabic. For Latin and Cyrillic text, function words are counted for English, Spanish, French, German, Italian, Portuguese, Dutch, Turkish, Indonesian, and Russian or Ukrainian; the winner needs at least 3 matching words and twice the runner-up's count.
3. Anything else, including short, mixed-script or ambiguous text, is `null`. A null is preferred to a wrong code.

No network and no model are involved, so the guess is the same every time for the same text. In the recorded `hackernews.search` rows on the [search page](https://onekey.dev/docs/search-multi.md), the story titled "Why Discord is switching from Go to Rust" gets `"language": "en"` this way (Hacker News does not report a language; the title has three English function words), while a title with fewer than three gets `null`.

## Warnings

`data._warnings` is a list of strings that is present only when there is something to say, next to `computed`. Today it carries the clamps:

- `engagement_rate clamped to 100 (computed 300: interactions exceed the divisor)`, when a post gathers more interactions than its author has followers.
- `engagement_rate clamped to 0 (computed -12.5: a count is negative)`.

The number in the sentence is the unclamped value. Treat the list as informational; its wording may change.

## Which endpoints have it

30 endpoints today (`engagement_rate` on 19, `estimated_reach` on 5, `language` on 30). This table is generated from the endpoint declarations; `features.computed` in `GET /v1/platforms` and `x-computed` in [`/openapi.json`](https://onekey.dev/openapi.json) are the live list, and each endpoint page shows an **Also works with** line.

| Endpoint | Result | Computed fields |
| --- | --- | --- |
| [`bluesky.post_thread`](https://onekey.dev/docs/api/bluesky/post_thread.md) | item | `engagement_rate`, `language` |
| [`bluesky.posts`](https://onekey.dev/docs/api/bluesky/posts.md) | list | `engagement_rate`, `language` |
| [`bluesky.posts_by_uri`](https://onekey.dev/docs/api/bluesky/posts_by_uri.md) | item | `engagement_rate`, `language` |
| [`bluesky.profile`](https://onekey.dev/docs/api/bluesky/profile.md) | item | `language` |
| [`devto.article`](https://onekey.dev/docs/api/devto/article.md) | item | `engagement_rate`, `language` |
| [`devto.article_by_path`](https://onekey.dev/docs/api/devto/article_by_path.md) | item | `engagement_rate`, `language` |
| [`devto.profile`](https://onekey.dev/docs/api/devto/profile.md) | item | `language` |
| [`devto.search`](https://onekey.dev/docs/api/devto/search.md) | list | `engagement_rate`, `language` |
| [`github.issue`](https://onekey.dev/docs/api/github/issue.md) | item | `engagement_rate`, `language` |
| [`github.profile`](https://onekey.dev/docs/api/github/profile.md) | item | `language` |
| [`gitlab.user`](https://onekey.dev/docs/api/gitlab/user.md) | item | `language` |
| [`hackernews.item`](https://onekey.dev/docs/api/hackernews/item.md) | item | `engagement_rate`, `language` |
| [`hackernews.search`](https://onekey.dev/docs/api/hackernews/search.md) | list | `engagement_rate`, `language` |
| [`hackernews.user`](https://onekey.dev/docs/api/hackernews/user.md) | item | `language` |
| [`instagram.post`](https://onekey.dev/docs/api/instagram/post.md) | item | `engagement_rate`, `estimated_reach`, `language` |
| [`instagram.profile`](https://onekey.dev/docs/api/instagram/profile.md) | item | `language` |
| [`lemmy.post`](https://onekey.dev/docs/api/lemmy/post.md) | item | `engagement_rate`, `language` |
| [`mastodon.status`](https://onekey.dev/docs/api/mastodon/status.md) | item | `engagement_rate`, `language` |
| [`reddit.post`](https://onekey.dev/docs/api/reddit/post.md) | item | `engagement_rate`, `language` |
| [`reddit.search_posts`](https://onekey.dev/docs/api/reddit/search_posts.md) | list | `engagement_rate`, `language` |
| [`reddit.user_about`](https://onekey.dev/docs/api/reddit/user_about.md) | item | `language` |
| [`stackexchange.question`](https://onekey.dev/docs/api/stackexchange/question.md) | item | `engagement_rate`, `language` |
| [`tiktok.profile`](https://onekey.dev/docs/api/tiktok/profile.md) | item | `language` |
| [`tiktok.video`](https://onekey.dev/docs/api/tiktok/video.md) | item | `engagement_rate`, `estimated_reach`, `language` |
| [`x.search`](https://onekey.dev/docs/api/x/search.md) | list | `engagement_rate`, `language` |
| [`x.tweet`](https://onekey.dev/docs/api/x/tweet.md) | item | `engagement_rate`, `language` |
| [`x.user`](https://onekey.dev/docs/api/x/user.md) | item | `language` |
| [`youtube.channel`](https://onekey.dev/docs/api/youtube/channel.md) | item | `estimated_reach`, `language` |
| [`youtube.channel_by_handle`](https://onekey.dev/docs/api/youtube/channel_by_handle.md) | item | `estimated_reach`, `language` |
| [`youtube.video_details`](https://onekey.dev/docs/api/youtube/video_details.md) | item | `engagement_rate`, `estimated_reach`, `language` |

On a list endpoint the block is on every element of `data`; on an item endpoint it is on the item. It is also present on these endpoints' rows in a [batch](https://onekey.dev/docs/batch.md), a [multi-platform search](https://onekey.dev/docs/search-multi.md) and a [URL lookup](https://onekey.dev/docs/lookup.md), since those run the same endpoints.

## Edge cases

- **It is never cached and never stale.** The block is computed when the response is served, from the data being served; a cached copy gets a fresh block over its (possibly stale) fields.
- **It does not change the fields it reads.** `engagement` and `content.language` are untouched; `computed` is added beside them.
- **`null` means unknown.** A missing view count does not become a reach of 0, and an unreadable text does not become `"en"`.
- **Rates can be above what you expect.** With followers as the divisor, a post that went further than its author's audience is clamped to 100 and warned about, rather than reported as 300%.

## What it costs

Nothing. The block adds no credits to any endpoint, batch or search, and it does not affect caching.
