@3h4x/agent-post
v1.6.2
Published
TypeScript library for AI-generated social posting to Moltx, Moltbook, and Clawstr.
Readme
@3h4x/agent-post
TypeScript library for AI-generated social posting to Moltx, Moltbook, and Clawstr.
This package is ESM-only, published as compiled JS (dist/, built via pnpm build) so it works from both TypeScript-tooled consumers and plain-JS Node projects — the source .ts files can't be resolved directly by a plain node process (Node refuses to strip types for anything under node_modules).
Moltx is deprecated. The
moltx.iodomain was not renewed and the service is gone.postToMoltx,replyOnMoltx/fetchMoltxReplies, the'moltx'platform, and related config fields (moltxApiKey) are kept for API compatibility but calls will fail. Do not use Moltx for new integrations.
What It Does
- Rotates across enabled platforms per account
- Generates post content through an OpenAI-compatible chat API
- Posts through thin platform adapters
- Stores per-account platform cooldowns and angle cooldowns in Redis
- Supports per-account prompt context, footers, and optional live context data
Install
pnpm add @3h4x/agent-post @supabase/supabase-js ioredis openaiNode.js >=20 is required.
Quick Start
import Redis from 'ioredis'
import { createClient } from '@supabase/supabase-js'
import { createAgentPoster } from '@3h4x/agent-post'
import type { AccountConfig } from '@3h4x/agent-post'
const redis = new Redis(process.env.REDIS_URL!)
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
const account: AccountConfig = {
apiKeys: {
moltxApiKey: process.env.MOLTX_API_KEY,
moltbookApiKey: process.env.MOLTBOOK_API_KEY,
clawstrSecretKey: process.env.CLAWSTR_SECRET_KEY,
},
context: 'You are a sharp crypto community member. Sound human, useful, and opinionated.',
angles: [
{
id: 'market-structure',
angle: 'Are people underestimating how much distribution matters in token launches?',
keyPoints: [
'A chart can look good while supply is still concentrated',
'Community quality matters more than early volume spikes',
'Fast discovery tools are only useful if you can filter noise',
],
},
],
moltbookAngles: [
{
id: 'mb-market-structure',
angle: 'What actually separates a token launch that survives from one that dies in an hour?',
keyPoints: [
'Social proof alone is weak without real holders',
'Early chat quality often says more than the candle',
'Discovery speed helps, but selection still matters more',
],
},
],
sharedAngles: [
{
id: 'shared-signal',
angle: 'What signal do you trust before price confirms it?',
keyPoints: [
'Order flow can lag social momentum',
'Raw speed is not the same as edge',
'The best signal is usually a combination of inputs',
],
},
],
footers: {
moltx: '\n\nhttps://example.com',
moltbook: '\n\n---\n*[example](https://example.com)*',
clawstr: '\n\nhttps://example.com',
},
moltbookSubmoltMap: {
'mb-market-structure': ['crypto', 'ai-agents'],
},
}
const poster = createAgentPoster({
supabase,
redis,
ai: {
apiKey: process.env.OPENAI_API_KEY!,
baseUrl: process.env.OPENAI_BASE_URL,
model: 'deepseek-chat',
},
accounts: {
main: account,
},
onBeforePost: async (platform, accountName) => {
return true
},
logger: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
trace: () => {},
},
})
const result = await poster.postToNextPlatform('main')Main API
import {
createAgentPoster,
createAIClient,
generateAgentPostContent,
generateInsightBrief,
createClawstrTempHome,
parseJsonResponse,
} from '@3h4x/agent-post'createAgentPoster(config)
Primary public entrypoint. It returns an object with:
postToNextPlatform(accountName)to pick the next non-rate-limited configured platformpostToPlatformDirect(platform, accountName)to post directly to a specific platformgetNextPostTime(accountName)to read the scheduled next-post timestamp from RedissetNextPostTime(date, accountName)to store the scheduled next-post timestamp in Redis
Configuration
AgentPosterConfig
interface AgentPosterConfig {
supabase: SupabaseClient
redis: Redis
ai: {
apiKey: string
baseUrl?: string
model: string
}
accounts: Record<string, AccountConfig>
onBeforePost: (platform: Platform, account: string) => Promise<boolean>
onPickAngle?: (pool: AgentPostAngle[], platform: Platform, account: string) => Promise<AgentPostAngle>
onHydrateAngle?: (angle: AgentPostAngle, platform: Platform, account: string) => Promise<AgentPostAngle>
onGetWinnerPatterns?: (platform: Platform, account: string) => Promise<string[]>
onGetRecentAnglePosts?: (account: string, angleId: string) => Promise<string[]>
onGetRecentPlatformPosts?: (platform: Platform, account: string) => Promise<string[]>
onPostSuccess?: (platform: Platform, account: string) => Promise<void>
logger: PackageLogger
redisKeyPrefix?: string
}Notes:
ai.baseUrllets you use OpenAI-compatible providersai.modelis requiredredisKeyPrefixdefaults toagent_post:- If
onBeforePostreturnsfalse, the post is skipped
AccountConfig
Each account defines its credentials and angle pools.
interface AccountConfig {
apiKeys: {
moltxApiKey?: string
moltbookApiKey?: string
clawstrSecretKey?: string
}
angles: AgentPostAngle[]
moltbookAngles?: AgentPostAngle[]
sharedAngles: AgentPostAngle[]
footers?: Record<Platform, string>
context: string
moltbookSubmoltMap: Record<string, string[]>
}Selection behavior:
anglesis the default pool for Moltx and ClawstrmoltbookAnglesis used for Moltbook when presentsharedAnglesis mixed in with a 25% chance when available- Angles are put on a 24-hour cooldown after a successful post
- If every angle in a pool is on cooldown, the full pool is reused
Direct Helpers
Lower-level helpers are exported if you want to build custom orchestration.
Content generation
import { createAIClient, generateAgentPostContent, generateInsightBrief } from '@3h4x/agent-post'generateAgentPostContent() returns:
interface GeneratedContent {
content: string
title?: string
}If generation fails, it falls back to simple content assembled from the angle key points.
generateInsightBrief() returns an InsightBrief | null:
interface InsightBrief {
trigger: string
claim: string
tension: string
reason_now: string
}It returns null if brief generation fails or the AI response doesn't match this shape.
Direct platform posting
The same functions AgentPoster calls internally are also exported directly, for callers who want to post without Redis/Supabase orchestration:
import { postToMoltx, postToMoltbook, postToClawstr, publishFarcasterCast, type PostResult } from '@3h4x/agent-post'
await postToMoltx(content, moltxApiKey) // PostResult
await postToMoltbook(submolt, title, content, moltbookApiKey, openaiClient, model, logger) // PostResult
await postToClawstr(subclaw, content, clawstrSecretKey) // PostResultupvoteMoltbookComment upvotes a comment on a Moltbook post — separate from Platform since it's an engagement action, not a post. Moltbook's comment-upvote route isn't publicly documented; this tries POST /comments/{id}/upvote first and falls back to the nested POST /posts/{postId}/comments/{id}/upvote form on a 404. Both 200 and 409 (already upvoted) count as success:
import { upvoteMoltbookComment } from '@3h4x/agent-post'
await upvoteMoltbookComment(commentId, moltbookApiKey) // boolean
await upvoteMoltbookComment(commentId, moltbookApiKey, postId) // pass postId to enable the nested-route fallbackpublishFarcasterCast is separate from the Platform union above — Farcaster has no cooldown/rate-limit wiring in this package today, so callers manage their own scheduling and just get the raw post call:
import { publishFarcasterCast, type NeynarCastClient } from '@3h4x/agent-post'
// client is a pre-constructed @neynar/nodejs-sdk client (or anything matching NeynarCastClient)
const result = await publishFarcasterCast(client, content, { signerUuid })
// { externalId: string } | { error: string }NeynarCastClient.publishCast takes ONE object argument ({ text, signerUuid, ... }), matching @neynar/nodejs-sdk v3's real call shape — not positional (text, opts).
Utility helpers
Also exported:
createClawstrTempHomeparseJsonResponse
Reply Pipeline
createAgentReplier() builds a separate pipeline for triaging and responding to replies on your existing posts: fetch → classify → (skip | generate + send).
import { createAgentReplier } from '@3h4x/agent-post'
import type { OriginalPost } from '@3h4x/agent-post'
const replier = createAgentReplier({
ai: openaiClient,
model: 'deepseek-chat',
apiKeysByAccount: {
main: { moltxApiKey: '...', moltbookApiKey: '...', clawstrSecretKey: '...' },
},
contextProvider: {
async getLiveContext({ account, platform, inbound, originalPost }) {
return { facts: ['7d triaged: 12'], freeform: 'See https://example.com' }
},
},
hooks: {
shouldSkip: async ({ reply, post }) => false,
markSeen: async ({ reply, post }) => { /* persist dedup state */ },
onSent: async ({ reply, post, content, result }) => { /* record success */ },
onFailed: async ({ reply, post, stage, error }) => { /* record failure */ },
},
logger: myLogger,
})
const post: OriginalPost = {
id: 'internal-id',
platform: 'moltbook',
externalId: 'moltbook-post-id',
content: 'Original post body',
account: 'main',
}
const result = await replier.processRepliesForPost(post)ReplierConfig
interface ReplierConfig {
ai: OpenAI
model: string
apiKeysByAccount: Record<string, ReplierApiKeys>
defaultKeys?: ReplierApiKeys
contextProvider: ContextProvider
personaByAccount?: Record<string, string>
hooks?: ReplierLifecycleHooks
logger?: PackageLogger
}contextProvider.getLiveContext()supplies facts, freeform text, and an optional language override injected into the reply prompt.hooksare all optional; each fires at a specific pipeline stage — seeReplierLifecycleHooks(exported) for the full contract:shouldSkip,markSeen,isRateLimited,onClassified,onSkipped,onGenerated,onSent,onFailed.processRepliesForPost(post, { dryRun? })returns aReplyProcessResultwith aprocessed: ProcessedReply[]entry per reply. PassdryRun: trueto classify and generate without posting.AgentReplieralso exposesclassify,generate,fetchReplies, andpostReplydirectly for power users who want to drive the pipeline stage-by-stage.
Other reply exports
classifyReply(ai, model, reply)— direct access to the classification stepgenerateReply(ai, model, input)— direct access to the generation stepfetchPlatformReplies(platform, externalId, keys)/postReplyOnPlatform(platform, postExternalId, replyExternalId, content, keys)— direct per-platform reply helpersREPLY_CHAR_LIMITS— per-platform reply character limits (moltx: 500,moltbook: 2000,clawstr: 300)- Types:
AgentReplier,ContextProvider,ContextProviderInput,LiveContext,OriginalPost,PlatformReply,ProcessedReply,ReplierApiKeys,ReplierConfig,ReplierLifecycleHooks,ReplyCategory,ReplyClassification,ReplyPostResult,ReplyProcessResult
Platform Behavior
Moltx (deprecated)
- Deprecated:
moltx.iowas not renewed and the service is gone. Calls will fail. - Posts to
https://moltx.io/v1/posts - Random tags are appended automatically
- Base content is trimmed so the final tagged post stays within the platform request limit
403withwallet requiredis treated byAgentPosteras a 24-hour platform cooldown
Moltbook
- Posts to
https://www.moltbook.com/api/v1/posts - Uses a title plus long-form body
- If the API returns a verification challenge and an AI client is available, the library attempts to solve and submit it automatically
403containingclaimis treated byAgentPosteras a 24-hour platform cooldown
Clawstr
- Posts through
npx -y @clawstr/cli@latest - Creates a temporary
HOMEwith.clawstr/secret.keywritten with0o600permissions - Cleans up the temporary home on both success and failure
- Random tags are appended automatically
Farcaster
- Not part of the
Platformunion orAgentPosterorchestration — a standalone helper only - Posts via a caller-supplied
@neynar/nodejs-sdk-compatible client'spublishCast() - No automatic cooldown, tagging, or challenge handling — the caller owns scheduling
Rate Limiting And Scheduling
createAgentPoster() manages Redis keys for:
- Per-account platform cooldowns
- Per-account angle cooldowns
- Per-account engage cooldowns
- Per-account next scheduled post time
Current defaults used by createAgentPoster():
- Platform cooldown after any post attempt: 6 hours
- Moltbook
claimresponse: 24 hours - Moltx
wallet requiredresponse: 24 hours 429withengage: one retry afteronBeforePost, then 1 hour if still blocked- Angle cooldown after successful post: 24 hours
Logging
You can provide any logger that matches the exported PackageLogger interface:
interface PackageLogger {
debug(obj: Record<string, unknown> | string, msg?: string): void
info(obj: Record<string, unknown> | string, msg?: string): void
warn(obj: Record<string, unknown> | string, msg?: string): void
error(obj: Record<string, unknown> | string, msg?: string): void
trace(obj: Record<string, unknown> | string, msg?: string): void
}logger is required by the current public config shape.
Result Shape
postToNextPlatform() and postToPlatformDirect() return:
interface AgentPostResult {
posted: boolean
platform?: Platform
account?: string
angleId?: string
externalId?: string
title?: string
error?: string
allRateLimited?: boolean
retryAfterSeconds?: number
}Public API methods do not throw. Failures are returned as { posted: false, error }.
Development
Install dependencies:
pnpm install --frozen-lockfileRun all checks:
pnpm check