Build an AI socialmedia agent in 2026
A social agent that actually ships posts has three layers. Judgment decides what is worth posting and whether a draft is good enough. Generation writes the text. Action holds the account connections and puts the post on the network at the right time. Most people build the first two in an afternoon and then lose three weeks to the third.
Last updated: September 2026
7-day free trial. No X developer account needed.
The three layers
They are separate because they are best served by different tools. A model that writes well is slow and expensive to run on every candidate. A model that judges well cannot write a sentence. And neither one knows anything about OAuth.
| Judgment | Generation | Action | |
|---|---|---|---|
| The question it answers | Is this worth posting? Is this draft good enough? Is this reply spam? | What should the post actually say? | Get this onto X, Bluesky and LinkedIn at 09:00 on Tuesday |
| What runs it well | A System One model such as Jev, or a small classifier you own | An LLM, through OpenRouter or a provider SDK | A posting API that holds the network connections and a queue |
| Output shape | A typed answer plus a probability distribution | Free text | A post id, then one result per network |
| Latency that matters | Fast enough to run on every candidate | Seconds. It runs on the few that survive | Whenever the schedule says |
| Cost shape | Per input token. Output is free on Jev | Per input and output token. Output dominates | Flat monthly plan |
| What breaks if you skip it | The agent posts everything it writes, including the bad half | You are scheduling posts a human wrote, which is fine | Nothing ever reaches a timeline |
You do not need all three. The decision table further down says which ones a given agent actually needs.
Layer 1. Judgment
The layer that decides. It runs on everything, so it has to be cheap and fast.
Judgment is every yes-or-no and how-much question in the pipeline. Is this draft good enough to publish. Is this reply worth answering. Is this inbound DM a lead or spam. Is this piece of inspiration close enough to my niche to repurpose. An agent that skips this layer publishes everything it writes, which includes the bad half.
You can ask an LLM these questions and parse the answer. It works, and for a handful of decisions a day it is the right call. It stops working when judgment runs on every candidate: a few seconds of latency and a few cents per call turn a filter into a bottleneck.
Jev is TypeSafe AI's first System One model, announced 15 September 2026. You send state plus typed questions and it returns typed answers in one parallel pass. There are three question types: a Choice picks one of up to 255 options and returns a probability per option plus a confidence. A Score rates against 2 to 10 ordered levels and returns a probability-weighted number, the legend it was scored against, the distribution and a confidence. A Noul answers a yes-or-no statement and returns a single float from 0 to 1, and nothing else. There is no confidence field on a Noul answer. The probability is the confidence.
Pricing is $0.042 per million input tokens and output tokens are free, per TypeSafe's model docs. TypeSafe publishes a sub-second end-to-end latency range, which is what makes running judgment on every candidate affordable rather than aspirational.
import { TypeSafeClient, noul, score } from '@typesafe-ai/sdk';
const client = new TypeSafeClient({ timeout: 4000 });
const QUESTIONS = {
strength: score(
'How far will this post travel compared to a typical post from the same author?',
[
'Flops. Gets less attention than a typical post from the same account.',
'Typical. Performs about the same as the account usually does.',
'Above typical. Noticeably more reach and replies than usual.',
'Strong. Several times the usual reach. Spreads past the existing audience.',
'Breakout. Orders of magnitude beyond usual.',
]
),
slop: noul('This reads like it was generated by an AI rather than written by a person.'),
bait: noul('This post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks.'),
toxicity: noul('This text contains slurs, harassment, sexual content, or abuse targeted at a person or group.'),
} as const;
export interface Verdict {
strength: number; // 0 to 4, probability weighted across the five levels
confidence: number; // 0 to 1, how concentrated that distribution is
slop: number; // 0 to 1
bait: number; // 0 to 1
toxicity: number; // 0 to 1
}
export async function judge(draft: string): Promise<Verdict> {
const { answers } = await client.systemOne({
state: { draft_post: draft, platform: 'X (Twitter)' },
questions: QUESTIONS,
});
return {
strength: answers.strength.score,
confidence: answers.strength.confidence,
slop: answers.slop.noul,
bait: answers.bait.noul,
toxicity: answers.toxicity.noul,
};
}What the judgment layer cannot do for you
Jev cannot return a value outside your schema. It can still return the wrong value inside it. TypeSafe's own launch post says the 0% hallucination figure "is not empirical" and that schema matching is what is guaranteed, and its CEO has acknowledged on Hacker News that a schema-valid answer can still be factually wrong. On TypeSafe's own four-workflow eval Jev scores 67.8% against GPT-5.6 Terra's 67.9%, and loses to Sol at 74.1% and Opus 5 at 73.1%. It is a cost and latency argument, not an accuracy one. The full version of that correction.More on this layer: is Jev an LLM, the limits and rate limits, how to get access and classifying text with Choice, Score and Noul.
Layer 2. Generation
The layer that writes. It runs on the few candidates that survive the first layer.
This is an ordinary LLM call. Nothing about the judgment layer replaces it: Jev emits no free-form text at all, so there is no prose, no code, no summary and no JSON authoring coming out of it.
The one design choice that matters here is to generate more than one candidate. A single draft gives the judgment layer nothing to compare against, so its only options are publish or regenerate. Five drafts turn the next step into a ranking problem, which is the thing a Score primitive is actually good at. The variant-ranking pattern in full.
const OPENROUTER = 'https://openrouter.ai/api/v1/chat/completions';
export async function writeVariants(brief: string, n: number): Promise<string[]> {
const res = await fetch(OPENROUTER, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENROUTER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content:
'You write posts for X. One post per line, nothing else. ' +
'No hashtags, no emoji, and never ask the reader for likes or replies.',
},
{ role: 'user', content: 'Write ' + n + ' different posts about: ' + brief },
],
}),
});
if (!res.ok) throw new Error('Generation failed: ' + res.status);
const data = await res.json();
return String(data.choices[0].message.content)
.split('\n')
.map((line: string) => line.trim())
.filter(Boolean)
.slice(0, n);
}Layer 3. Action, and why builds stall here
Writing an agent that decides and writes takes an afternoon. Getting the result onto three networks, on time, every time, is the part nobody scopes correctly.
The reason is not that any single network is hard. It is that the three you care about have almost nothing in common. Different auth, different token lifetimes, different text counting, different media rules, different billing, and three separate sets of things that go wrong at 3am.
| X | Bluesky | ||
|---|---|---|---|
| How you authenticate | X developer account, an app in the Developer Console, OAuth 2.0 | atproto OAuth. No pre-registration, no API key. Your client_id is the URL of a metadata document you host | Sign in with LinkedIn, the self-serve Share on LinkedIn product |
| Token lifetime | Refresh tokens rotate on every use. Lose one and the connection is dead | Session refresh over atproto | 60 days, with no refresh token for self-serve apps. The member signs in again |
| What posting costs | Pay per use: $0.015 per post, $0.20 per post containing a URL, as of September 2026 | Nothing | Nothing |
| How text is counted | Weighted characters, against the connected account tier limit | Graphemes, with a UTF-8 byte ceiling on top | UTF-16 code units |
| Threads | Native thread, up to 25 parts | Native thread | No thread concept. Parts get combined into one post |
| Scheduling in the API | None | None | None |
Vendor documentation and developer portals, read September 2026.
Read the last row again. None of the three has a scheduling field. Every scheduler you have ever used is a queue somebody runs, a process that wakes up, finds the posts that are due, and calls the publish endpoint. If your agent is going to schedule anything, you are either running that process or somebody is running it for you. If you run it, you also own retries, partial failures, duplicate protection, the token that expired overnight, and the difference between "the network rejected this" and "the network is down".
That is the whole argument for putting a posting API in the action layer instead of three SDKs. One key, one request shape, one queue, and per-network outcomes you can read in code.
What OpenTweet owns
- The X, Bluesky and LinkedIn connections, and every token renewal
- The queue. Send scheduled_date and the post publishes at that time
- Per-network length and media checks, before anything is sent
- Retries and per-network results, so a partial failure is legible
- Rate limits and an optional Idempotency-Key so a retried call does not double-post
- One ot_ key for the REST API, the hosted MCP server and the CLI
What it will not do
- Instagram, TikTok, Facebook, Threads and YouTube are not supported
- LinkedIn Company Pages are not supported. Personal profiles only
- It does not write your posts. That is the generation layer
- It does not judge your posts. That is the judgment layer
- No bulk or automated mass DMing, on any plan
const OPENTWEET = 'https://opentweet.io/api/v1/posts';
export interface NetworkResult {
platform: 'x' | 'bluesky' | 'linkedin';
status: string;
url: string | null;
error: string | null;
skip_reason: string | null;
}
export interface Published {
id: string;
status: 'draft' | 'scheduled' | 'posted' | 'failed' | 'evergreen';
results: NetworkResult[];
}
/**
* at: ISO 8601 with an explicit offset. A bare local string is read in server time.
* now: publish synchronously and return the real outcome.
* Neither one saves the post as a draft for a human to approve.
*/
export async function publish(
text: string,
opts: { at?: string; now?: boolean; key?: string } = {}
): Promise<Published> {
const body: Record<string, unknown> = {
text,
platforms: ['x', 'bluesky', 'linkedin'],
category: 'Agent',
};
if (opts.at) body.scheduled_date = opts.at;
else if (opts.now) body.publish_now = true;
const headers: Record<string, string> = {
Authorization: 'Bearer ' + process.env.OPENTWEET_API_KEY,
'Content-Type': 'application/json',
};
if (opts.key) headers['Idempotency-Key'] = opts.key;
const res = await fetch(OPENTWEET, { method: 'POST', headers, body: JSON.stringify(body) });
const data = await res.json();
if (!res.ok) {
// { error, code, details? } on this route. 502 publish_failed still carries results[].
throw new Error((data.code || res.status) + ': ' + data.error);
}
const post = data.posts[0];
return { id: post.id, status: post.status, results: post.results || [] };
}Omitting platforms does not mean X only. It means the post follows the account's auto cross-post setting, which is resolved at publish time. An unattended agent should always name its targets. Full REST reference and how cross-posting resolves.
Which layers do you actually need
Most agents people describe as "AI social media agents" need two of the three. Adding a layer you do not need is how a weekend project becomes a quarter.
| What you are building | Judgment | Generation | Action |
|---|---|---|---|
| Publish a changelog entry a human already wrote | No | No | Yes |
| Recycle evergreen posts on a cooldown | No | No | Yes |
| Score drafts a human wrote before they go out | Yes | No | Yes |
| Filter a feed down to what is worth reading | Yes | No | No |
| Write and publish daily posts unattended | Yes | Yes | Yes |
| Triage inbound replies and DMs, then answer the real ones | Yes | Yes | Yes |
| Pick the best of five drafts and ship it | Yes | Yes | Yes |
Two rows worth noticing. Scheduling content a human wrote needs no AI at all. And a feed filter needs judgment and nothing else, right up until you want to reply. Classifying replies and routing DMs are both worked examples of that row.
The full build
Four files. It writes five drafts, scores all five, drops anything that fails a hard gate, and schedules the best survivor. Anything it is not sure about becomes a draft for a human.
You need three keys: a TypeSafe key for the judgment layer, an LLM key for the generation layer, and an OpenTweet ot_ key for the action layer.
npm install @typesafe-ai/sdk
export TYPESAFE_API_KEY="your_typesafe_key"
export OPENROUTER_API_KEY="your_openrouter_key"
export OPENTWEET_API_KEY="ot_your_key"The three layer modules are above: judge.ts, write.ts and publish.ts. This is the loop that joins them.
import { judge } from './judge';
import { writeVariants } from './write';
import { publish } from './publish';
// Thresholds live in one file on purpose, so changing the agent's risk appetite
// is a reviewable diff rather than a number buried in a branch.
const MIN_STRENGTH = 2.0; // "Above typical" on the 0 to 4 rubric
const MIN_CONFIDENCE = 0.6; // below this the model is guessing, so a human looks
const MAX_SLOP = 0.5;
const MAX_BAIT = 0.3;
const MAX_TOXICITY = 0.05;
export async function run(brief: string, at: string) {
const variants = await writeVariants(brief, 5);
// One Jev request per variant, all in flight at once. Parallel requests
// finish in about the time the slowest one takes.
const judged = await Promise.all(
variants.map(async (text) => ({ text, verdict: await judge(text) }))
);
// Hard gates first. These fail closed: a variant that cannot be judged is dropped.
const safe = judged.filter(
(v) =>
v.verdict.toxicity < MAX_TOXICITY &&
v.verdict.bait < MAX_BAIT &&
v.verdict.slop < MAX_SLOP
);
if (safe.length === 0) {
return { action: 'regenerate', reason: 'every variant failed a hard gate' };
}
safe.sort((a, b) => b.verdict.strength - a.verdict.strength);
const best = safe[0];
// Not good enough, or the model is not sure. Save it and let a person decide.
if (best.verdict.strength < MIN_STRENGTH || best.verdict.confidence < MIN_CONFIDENCE) {
const draft = await publish(best.text);
return { action: 'review', id: draft.id, verdict: best.verdict };
}
const key = 'agent-' + at + '-' + best.text.slice(0, 32);
const scheduled = await publish(best.text, { at, key });
return { action: 'scheduled', id: scheduled.id, verdict: best.verdict };
}Three things in that file do most of the work. The hard gates run before the ranking, so a toxic variant is never eligible to win on strength. The confidence check is separate from the strength check, because a draft can score well on a distribution so flat the number means nothing. And the fallback is a draft, not a retry loop: an agent that cannot decide should hand the decision to a person, not keep spending tokens until it gets an answer it likes. How to pick the threshold numbers.
A published post returns one result per network. A post that breaks one network's rules is skipped there with a stated reason and still goes out on the others, so read each entry rather than treating the call as all-or-nothing.
"results": [
{
"platform": "x",
"status": "posted",
"post_id": "1839201847362910000",
"url": "https://x.com/i/status/1839201847362910000",
"error": null,
"skip_reason": null
},
{
"platform": "bluesky",
"status": "skipped",
"url": null,
"error": null,
"skip_reason": "text_too_long"
},
{
"platform": "linkedin",
"status": "posted",
"url": "https://www.linkedin.com/feed/update/urn:li:share:7300000000000000000/",
"error": null,
"skip_reason": null
}
]Feeding that array back to the generation layer is what lets the agent rewrite a too-long Bluesky version and retry it by itself, without touching the two networks that already published.
If the agent speaks MCP, skip the HTTP
An agent running inside Claude Code, Claude Desktop, Cursor or Windsurf does not need publish.ts at all.
{
"mcpServers": {
"opentweet": {
"type": "streamable-http",
"url": "https://mcp.opentweet.io/mcp",
"headers": { "Authorization": "Bearer ot_your_key" }
}
}
}The hosted server exposes 43 tools over the same ot_ key, and opentweet_create_tweet already takes a platforms parameter. The judgment layer can sit beside it as its own server. Jev over MCP, the OpenTweet MCP server, and the rest of the field.
What it costs to run
The judgment layer is the cheapest thing in the pipeline, which is the entire point of it.
| Step | What runs it | Cost |
|---|---|---|
| Score one draft | Jev, seven questions in one request | A small fraction of a cent at the published input rate |
| Score five variants | Jev, five requests in parallel | Under a tenth of a cent |
| Write five variants | An LLM | The dominant per-post cost. Output tokens are what you pay for |
| Publish to three networks | OpenTweet | Included in the plan. From $11.99 a month, no per-post fee |
| X pay-per-use billing | X, if you call it directly instead | $0.015 per post, $0.20 with a URL, on top of whatever else you pay |
Jev lists $0.042 per million input tokens with output unmetered, and the free public scorer runs seven Jev questions in one request. Work out your own volume on the cost calculator.
The shape is worth sitting with. Judgment costs three orders of magnitude less than generation, so the cheapest way to make an agent better is to generate more candidates and judge harder, not to buy a bigger writing model. That is also why judgment is affordable on things generation never touches: every reply in your mentions, every post in a feed, every inbound DM.
The rails, before you let it run unattended
Default to drafts. Send neither scheduled_date nor publish_now and the post is saved as a draft. Run the agent in that mode for a week and read what it wanted to publish before you let it publish anything.
Fail closed on the hard gates. Toxicity and engagement bait are not ranking signals, they are vetoes. If the judgment call errors or times out, drop the candidate rather than publishing it unjudged.
Log the model version and every answer. Pin jev-1.13.0 rather than jev-latest in production, so your thresholds do not drift under you when the alias moves, and keep the full answer object so you can audit a decision later.
Use an Idempotency-Key. A retried create call with the same key replays the stored response instead of publishing a second post. It is optional and it is the cheapest insurance in the whole pipeline.
Respect the ceilings. 50 posts per create request, 300 publish-bound posts queued at once, 25 parts in a thread, and a per-minute and per-day API rate limit that scales with the plan. GET /api/v1/usage exists so an agent can check its remaining quota instead of discovering it through a 429.
Do not automate DMs in bulk. OpenTweet's DM campaigns are human-approved per lead by design. There is no bulk auto-send and there never will be.
Frequently asked questions
What is an AI social media agent?
A program that decides what to post, writes it, and publishes it without a person driving each step. In practice it is three layers. A judgment layer decides whether something is worth posting and whether a draft is good enough. A generation layer writes the text. An action layer holds the account connections and puts the post on the network at the right time. The three layers are separate because they are best served by different tools.
Do I need Jev to build a social media agent?
No. You can ask an LLM to score a draft and parse the answer yourself. Jev is worth adding when the judgment runs often, needs to be fast, and needs a number you can threshold on. It returns a probability distribution rather than a number you have to parse, at $0.042 per million input tokens with output tokens free, so scoring every draft, every reply and every inbound DM stops being a budget question.
Can Jev write the posts?
No. Jev generates no text at all. No prose, no code, no summaries, no JSON authoring. It takes state plus typed questions and returns typed answers. Writing the post is the LLM layer, and the two models do not replace each other.
Why is publishing the hard part of a social media agent?
Because X, Bluesky and LinkedIn each have different auth, different limits and different failure modes, and none of the three has a scheduling field in its posting API. Every scheduler is a queue somebody runs. A self-serve LinkedIn app gets a 60-day token with no refresh token. X bills per post. Bluesky counts graphemes with a UTF-8 byte ceiling. Getting one post onto all three reliably is more work than the writing and the scoring put together.
How do I stop the agent publishing something bad?
Gate on the judgment layer and default to drafts. Send neither scheduled_date nor publish_now and OpenTweet saves the post as a draft, so a human approves before anything goes out. Add hard Noul gates for toxicity and engagement bait that fail closed, and route anything below your confidence threshold to review rather than to the network.
What does it cost to run an agent like this?
Three lines. Judgment is the cheapest: seven Jev questions on a short draft is a small fraction of a cent at the published input rate. Generation is the dominant per-post cost, because output tokens are what an LLM charges for. Publishing is a flat plan, from $11.99 a month, with the REST API, the MCP server and the CLI included on every tier and no per-post fee.
Can the agent post to Instagram, TikTok or Facebook through OpenTweet?
No. OpenTweet posts to X, Bluesky and LinkedIn only. Instagram, TikTok, Facebook, Threads and YouTube are not supported, so the platforms enum has exactly three values. If your agent needs those networks, a unified social API is the better fit for the action layer.
Should the agent call a REST API or an MCP server?
Use MCP when the agent runs inside an MCP client such as Claude Code, Claude Desktop, Cursor or Windsurf: the client discovers the tools and there is no HTTP code to write. Use REST when you own the loop, in a cron job or a backend. OpenTweet serves both from the same ot_ key, and the hosted MCP server exposes 43 tools.
Keep exploring
The judgment layer in depth, the action layer in depth, and the free scorer that runs both.
Jev, explained
What a System One model is, what it returns, and what it cannot do.
Jev post scorer
The pre-publish quality gate, and the free scorer that runs it live.
A/B test post variants with Jev
Generate five, score five, publish one. Why the distribution matters more than the number.
Setting a confidence threshold
Where to draw the line between acting automatically and asking a human.
Jev cost calculator
What a real recurring workload costs at $0.042 per million input tokens.
Social media API for AI agents
The three ways an agent can post, compared, with the trade-offs of each.
The OpenTweet MCP server
43 tools over one key, hosted or local, for agents that speak MCP.
Get an API key
Create an ot_ key, check your usage, and read the REST reference.
Jev decides. An LLM writes. OpenTweet publishes.
The action layer is the one you should not build. One key covers the REST API, the hosted MCP server and the CLI, for X, Bluesky and LinkedIn. From $11.99/month. Three plans, all with the API and MCP server included. Create a key on the developer page or read what each plan includes.