Building AI Agents for Social Media with TypeScript and Hono.js
Building AI Agents for Social Media with TypeScript and Hono.js
Everyone's talking about AI agents right now, but most tutorials stop at "call an LLM in a loop." If you actually want an agent that runs unattended - fetches data, thinks about it, writes content, and publishes it - you need a real backend, not just a prompt.
This post walks through the architecture I use for exactly that: a scheduled agent that finds fresh data, drafts social posts with Claude, and publishes them, built entirely on Hono.js running on Cloudflare Workers. I'll use "post finance news to LinkedIn/Reddit" as the running example, but the pattern generalizes to any "watch โ think โ act โ publish" agent.
Why Hono.js for agent backends
Hono is a small, fast web framework that runs on Cloudflare Workers, Deno, Bun, and Node. For agent workloads specifically, three things make it a good fit:
- Native Cloudflare Cron Triggers - agents that run on a schedule don't need a separate job scheduler or an always-on server.
- Edge runtime, near-zero cold start - your agent wakes up, does its work, and disappears. You pay for execution, not idle uptime.
- Middleware model - auth, logging, and rate-limiting for your own agent's admin routes come for free.
The architecture
Cron Trigger (Hono on Cloudflare Workers)
โ
โโโ 1. Fetch step โ pull raw data from an external API
โโโ 2. Reasoning step โ Claude API decides what's worth posting
โโโ 3. Generation step โ Claude API drafts platform-specific copy
โโโ 4. Dedup check โ Postgres/Neon, skip anything already posted
โโโ 5. Publish step โ social API (or a unified posting provider)
Each step is a plain async function. No agent framework, no hidden state machine - just a pipeline you can read top to bottom and unit test.
Setting up the project
npm create hono@latest social-agent
cd social-agent
npm install
Pick the cloudflare-workers template when prompted. Then add what we need:
npm install @anthropic-ai/sdk drizzle-orm @neondatabase/serverless
Step 1: The cron trigger
In wrangler.toml, define when the agent wakes up:
[triggers]
crons = ["0 * * * 1-5"] # hourly, weekdays only
In your Hono app, handle the scheduled event separately from HTTP routes:
import { Hono } from 'hono'
const app = new Hono()
export default {
fetch: app.fetch,
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(runAgentCycle(env))
},
}
ctx.waitUntil is important - it tells the Worker runtime to keep the instance alive until your async work finishes, even though there's no HTTP response to wait on.
Step 2: Fetch fresh data
Keep this step dumb. It should return structured data, not decide anything.
interface RawEvent {
id: string
title: string
payload: Record<string, unknown>
}
async function fetchLatestEvents(env: Env): Promise<RawEvent[]> {
const res = await fetch('https://api.example.com/events?window=today', {
headers: {
Authorization: `Bearer ${env.SOURCE_API_KEY}`,
},
})
if (!res.ok) throw new Error(`Source fetch failed: ${res.status}`)
const data = await res.json()
return data.events
}
Step 3: Let Claude decide what's worth posting
This is the part people skip and regret. Don't auto-draft a post for every single item - have the model triage first. It's cheaper, and it keeps your feed from looking like a bot dump.
import Anthropic from '@anthropic-ai/sdk'
async function triageEvents(events: RawEvent[], env: Env) {
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: 'You triage events for social media worthiness. Only flag items with a genuinely interesting angle - a surprise, a pattern, a number that stands out. Return strict JSON, no prose.',
messages: [
{
role: 'user',
content: JSON.stringify(events),
},
],
})
const text = message.content.find((c) => c.type === 'text')?.text ?? '[]'
return JSON.parse(text) as { id: string; angle: string }[]
}
Asking for "strict JSON, no prose" up front saves you a fragile regex-strip step later.
Step 4: Generate platform-specific copy
LinkedIn and Reddit have different norms - LinkedIn rewards a confident, analytical voice; Reddit punishes anything that reads like marketing copy. Generate both in one call, but prompt for the difference explicitly rather than reusing one draft everywhere.
async function draftPosts(item: RawEvent, angle: string, env: Env) {
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 800,
system: `Write two versions of a post about this event. LinkedIn: 80-150 words, analytical tone, one soft mention of relevant context, no hashtag spam. Reddit: framed as a discussion starter, no promotional language, ends with a genuine question. Return JSON: { "linkedin": "...", "reddit": "..." }`,
messages: [
{
role: 'user',
content: `Event: ${item.title}\nAngle: ${angle}\nData: ${JSON.stringify(item.payload)}`,
},
],
})
const text = message.content.find((c) => c.type === 'text')?.text ?? '{}'
return JSON.parse(text) as { linkedin: string; reddit: string }
}
Step 5: Dedup with Postgres
Nothing kills credibility faster than posting the same thing twice because a cron overlapped. Use Neon's serverless driver - it works over HTTP, which matters on Workers since you don't have a persistent TCP connection.
import { neon } from '@neondatabase/serverless'
async function alreadyPosted(env: Env, eventId: string) {
const sql = neon(env.DATABASE_URL)
const rows = await sql`SELECT 1 FROM posted_events WHERE event_id = ${eventId}`
return rows.length > 0
}
async function markPosted(env: Env, eventId: string, platform: string, postId: string) {
const sql = neon(env.DATABASE_URL)
await sql`
INSERT INTO posted_events (event_id, platform, post_id, posted_at)
VALUES (${eventId}, ${platform}, ${postId}, now())
`
}
Step 6: Publish
You have two real options here:
- Native platform APIs. LinkedIn requires an approved Company Page and
w_member_socialscope; Reddit requires its own OAuth app and respects strict rate limits. Both are doable but slow to set up the first time. - A unified posting provider (e.g. Ayrshare) that abstracts multiple platforms behind one API. Much faster to ship an MVP with - worth it if you're validating the idea before investing in native integrations.
async function publish(platform: 'linkedin' | 'reddit', content: string, env: Env) {
const res = await fetch('https://api.ayrshare.com/api/post', {
method: 'POST',
headers: {
Authorization: `Bearer ${env.AYRSHARE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
post: content,
platforms: [platform],
}),
})
if (!res.ok) throw new Error(`Publish failed on ${platform}: ${res.status}`)
return res.json()
}
Wiring it together
async function runAgentCycle(env: Env) {
const events = await fetchLatestEvents(env)
const flagged = await triageEvents(events, env)
for (const { id, angle } of flagged) {
if (await alreadyPosted(env, id)) continue
const source = events.find((e) => e.id === id)!
const { linkedin, reddit } = await draftPosts(source, angle, env)
const li = await publish('linkedin', linkedin, env)
await markPosted(env, id, 'linkedin', li.id)
// Reddit: hold for human review instead of auto-publishing - see note below
await queueForReview(env, id, reddit)
}
}
The guardrail that actually matters
Automating the fetch โ draft pipeline is safe. Automating the publish step to Reddit is not, at least not at first. Most active subreddits have strict self-promotion rules, and an account that posts on a predictable schedule with promotional undertones gets flagged as a bot fast - sometimes shadowbanned entirely.
Two things fix this:
- Human-in-the-loop for Reddit specifically. Queue the draft (Slack, email, a simple admin route in the same Hono app) and require a manual approve before it publishes.
- Keep the language descriptive, not advisory. For anything finance-adjacent, "revenue beat estimates by X%" is commentary; "you should buy this" edges into advice you don't want to be on the hook for.
LinkedIn is more forgiving of a consistent posting cadence, so it's the safer platform to fully automate first.
Where to go from here
This same skeleton - cron trigger, fetch, triage, generate, dedup, publish - works for far more than finance news. Swap the fetch step for GitHub releases, product reviews, conference CFPs, or your own product's usage metrics, and you have a different agent with the same reliability guarantees.
The part worth getting right early is the triage step. An agent that posts about everything is just noise with extra steps; an agent that only speaks up when there's a genuine angle is the one people actually follow.
If you're building something similar, I'd love to hear what you're automating - drop it in the comments.
Comments
No comments yet. Start the discussion.