TypeScript 및 Hono.js로 소셜 미디어용 AI 에이전트 구축

작성자

카테고리:

← 피드로
DEV Community · Mayuresh Smita Suresh · 2026-07-19 개발(SW)

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 a 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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Pick the cloudflare-workers template when prompted. Then add what we need:

npm install @anthropic-ai/sdk drizzle-orm @neondatabase/serverless

Enter fullscreen mode Exit fullscreen mode

Step 1: The cron trigger

In wrangler.toml, define when the agent wakes up:

[triggers]
crons = ["0 * * * 1-5"] # hourly, weekdays only

Enter fullscreen mode Exit fullscreen mode

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))
  },
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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 }[]
}

Enter fullscreen mode Exit fullscreen mode

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 }
}

Enter fullscreen mode Exit fullscreen mode

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())
  `
}

Enter fullscreen mode Exit fullscreen mode

Step 6: Publish

You have two real options here:

  1. 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.
  2. 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()
}

Enter fullscreen mode Exit fullscreen mode

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)
  }
}

Enter fullscreen mode Exit fullscreen mode

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.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다