<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Naim Hossain</title>
    <description>The latest articles on DEV Community by Naim Hossain (@naim_hossain_43eadf058df2).</description>
    <link>https://dev.to/naim_hossain_43eadf058df2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4042303%2F048a44b2-20bd-48e4-a4da-b9ecceb9f5de.jpg</url>
      <title>DEV Community: Naim Hossain</title>
      <link>https://dev.to/naim_hossain_43eadf058df2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naim_hossain_43eadf058df2"/>
    <language>en</language>
    <item>
      <title>Building AI Agents for Social Media with TypeScript and Hono.js</title>
      <dc:creator>Naim Hossain</dc:creator>
      <pubDate>Fri, 24 Jul 2026 05:54:58 +0000</pubDate>
      <link>https://dev.to/naim_hossain_43eadf058df2/building-ai-agents-for-social-media-with-typescript-and-honojs-4ca8</link>
      <guid>https://dev.to/naim_hossain_43eadf058df2/building-ai-agents-for-social-media-with-typescript-and-honojs-4ca8</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I'll use "post finance news to LinkedIn/Reddit" as the running example, but the pattern generalizes to any "watch → think → act → publish" agent.&lt;/p&gt;

&lt;p&gt;Why Hono.js for agent backends&lt;br&gt;
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:&lt;/p&gt;

&lt;p&gt;Native Cloudflare Cron Triggers — agents that run on a schedule don't need a separate job scheduler or a always-on server.&lt;br&gt;
Edge runtime, near-zero cold start — your agent wakes up, does its work, and disappears. You pay for execution, not idle uptime.&lt;br&gt;
Middleware model — auth, logging, and rate-limiting for your own agent's admin routes come for free.  &lt;/p&gt;

&lt;p&gt;The architecture&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Cron Trigger (Hono on Cloudflare Workers)&lt;br&gt;
   │&lt;br&gt;
   ├── 1. Fetch step   → pull raw data from an external API&lt;br&gt;
   ├── 2. Reasoning step → Claude API decides what's worth posting&lt;br&gt;
   ├── 3. Generation step → Claude API drafts platform-specific copy&lt;br&gt;
   ├── 4. Dedup check   → Postgres/Neon, skip anything already posted&lt;br&gt;
   └── 5. Publish step  → social API (or a unified posting provide&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Setting up the project&lt;br&gt;
npm create hono@latest social-agent&lt;br&gt;
cd social-agent&lt;br&gt;
npm install&lt;br&gt;
Pick the cloudflare-workers template when prompted. Then add what we need:&lt;/p&gt;

&lt;p&gt;npm install @anthropic-ai/sdk drizzle-orm @neondatabase/serverless&lt;br&gt;
Step 1: The cron trigger&lt;br&gt;
In wrangler.toml, define when the agent wakes up:&lt;/p&gt;

&lt;p&gt;[triggers]&lt;br&gt;
crons = ["0 * * * 1-5"] # hourly, weekdays only&lt;br&gt;
In your Hono app, handle the scheduled event separately from HTTP routes:&lt;/p&gt;

&lt;p&gt;import { Hono } from 'hono'&lt;/p&gt;

&lt;p&gt;const app = new Hono()&lt;/p&gt;

&lt;p&gt;export default {&lt;br&gt;
  fetch: app.fetch,&lt;br&gt;
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {&lt;br&gt;
    ctx.waitUntil(runAgentCycle(env))&lt;br&gt;
  },&lt;br&gt;
}&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Step 2: Fetch fresh data&lt;br&gt;
Keep this step dumb. It should return structured data, not decide anything.&lt;/p&gt;

&lt;p&gt;interface RawEvent {&lt;br&gt;
  id: string&lt;br&gt;
  title: string&lt;br&gt;
  payload: Record&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;async function fetchLatestEvents(env: Env): Promise {&lt;br&gt;
  const res = await fetch('&lt;a href="https://api.example.com/events?window=today" rel="noopener noreferrer"&gt;https://api.example.com/events?window=today&lt;/a&gt;', {&lt;br&gt;
    headers: { Authorization: &lt;code&gt;Bearer ${env.SOURCE_API_KEY}&lt;/code&gt; },&lt;br&gt;
  })&lt;br&gt;
  if (!res.ok) throw new Error(&lt;code&gt;Source fetch failed: ${res.status}&lt;/code&gt;)&lt;br&gt;
  const data = await res.json()&lt;br&gt;
  return data.events&lt;br&gt;
}&lt;br&gt;
Step 3: Let Claude decide what's worth posting&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;import Anthropic from '@anthropic-ai/sdk'&lt;/p&gt;

&lt;p&gt;async function triageEvents(events: RawEvent[], env: Env) {&lt;br&gt;
  const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })&lt;/p&gt;

&lt;p&gt;const message = await anthropic.messages.create({&lt;br&gt;
    model: 'claude-sonnet-4-6',&lt;br&gt;
    max_tokens: 1024,&lt;br&gt;
    system:&lt;br&gt;
      '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.',&lt;br&gt;
    messages: [&lt;br&gt;
      {&lt;br&gt;
        role: 'user',&lt;br&gt;
        content: JSON.stringify(events),&lt;br&gt;
      },&lt;br&gt;
    ],&lt;br&gt;
  })&lt;/p&gt;

&lt;p&gt;const text = message.content.find((c) =&amp;gt; c.type === 'text')?.text ?? '[]'&lt;br&gt;
  return JSON.parse(text) as { id: string; angle: string }[]&lt;br&gt;
}&lt;br&gt;
Asking for "strict JSON, no prose" up front saves you a fragile regex-strip step later.&lt;/p&gt;

&lt;p&gt;Step 4: Generate platform-specific copy&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;async function draftPosts(item: RawEvent, angle: string, env: Env) {&lt;br&gt;
  const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })&lt;/p&gt;

&lt;p&gt;const message = await anthropic.messages.create({&lt;br&gt;
    model: 'claude-sonnet-4-6',&lt;br&gt;
    max_tokens: 800,&lt;br&gt;
    system: &lt;code&gt;Write two versions of a post about this event.&lt;br&gt;
LinkedIn: 80-150 words, analytical tone, one soft mention of relevant context, no hashtag spam.&lt;br&gt;
Reddit: framed as a discussion starter, no promotional language, ends with a genuine question.&lt;br&gt;
Return JSON: { "linkedin": "...", "reddit": "..." }&lt;/code&gt;,&lt;br&gt;
    messages: [&lt;br&gt;
      { role: 'user', content: &lt;code&gt;Event: ${item.title}\nAngle: ${angle}\nData: ${JSON.stringify(item.payload)}&lt;/code&gt; },&lt;br&gt;
    ],&lt;br&gt;
  })&lt;/p&gt;

&lt;p&gt;const text = message.content.find((c) =&amp;gt; c.type === 'text')?.text ?? '{}'&lt;br&gt;
  return JSON.parse(text) as { linkedin: string; reddit: string }&lt;br&gt;
}&lt;br&gt;
Step 5: Dedup with Postgres&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;import { neon } from '@neondatabase/serverless'&lt;/p&gt;

&lt;p&gt;async function alreadyPosted(env: Env, eventId: string) {&lt;br&gt;
  const sql = neon(env.DATABASE_URL)&lt;br&gt;
  const rows = await sql&lt;code&gt;SELECT 1 FROM posted_events WHERE event_id = ${eventId}&lt;/code&gt;&lt;br&gt;
  return rows.length &amp;gt; 0&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;async function markPosted(env: Env, eventId: string, platform: string, postId: string) {&lt;br&gt;
  const sql = neon(env.DATABASE_URL)&lt;br&gt;
  await sql&lt;code&gt;&lt;br&gt;
    INSERT INTO posted_events (event_id, platform, post_id, posted_at)&lt;br&gt;
    VALUES (${eventId}, ${platform}, ${postId}, now())&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
}&lt;br&gt;
Step 6: Publish&lt;br&gt;
You have two real options here:&lt;/p&gt;

&lt;p&gt;Native platform APIs. LinkedIn requires an approved Company Page and w_member_social scope; Reddit requires its own OAuth app and respects strict rate limits. Both are doable but slow to set up the first time.&lt;br&gt;
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.&lt;br&gt;
async function publish(platform: 'linkedin' | 'reddit', content: string, env: Env) {&lt;br&gt;
  const res = await fetch('&lt;a href="https://api.ayrshare.com/api/post" rel="noopener noreferrer"&gt;https://api.ayrshare.com/api/post&lt;/a&gt;', {&lt;br&gt;
    method: 'POST',&lt;br&gt;
    headers: {&lt;br&gt;
      Authorization: &lt;code&gt;Bearer ${env.AYRSHARE_API_KEY}&lt;/code&gt;,&lt;br&gt;
      'Content-Type': 'application/json',&lt;br&gt;
    },&lt;br&gt;
    body: JSON.stringify({ post: content, platforms: [platform] }),&lt;br&gt;
  })&lt;br&gt;
  if (!res.ok) throw new Error(&lt;code&gt;Publish failed on ${platform}: ${res.status}&lt;/code&gt;)&lt;br&gt;
  return res.json()&lt;br&gt;
}&lt;br&gt;
Wiring it together&lt;br&gt;
async function runAgentCycle(env: Env) {&lt;br&gt;
  const events = await fetchLatestEvents(env)&lt;br&gt;
  const flagged = await triageEvents(events, env)&lt;/p&gt;

&lt;p&gt;for (const { id, angle } of flagged) {&lt;br&gt;
    if (await alreadyPosted(env, id)) continue&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const source = events.find((e) =&amp;gt; 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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
}&lt;br&gt;
The guardrail that actually matters&lt;br&gt;
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:&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
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.&lt;br&gt;
LinkedIn is more forgiving of a consistent posting cadence, so it's the safer platform to fully automate first.&lt;/p&gt;

&lt;p&gt;Where to go from here&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd love to hear what you're automating — drop it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>honojs</category>
    </item>
  </channel>
</rss>
