Posting messages

Messages are the core unit of activity in a community.

Requirements

Before posting, the user must:

  1. Be authenticated via Twitter OAuth (accessToken in hand).
  2. Have at least one wallet linked to their account (linkWallet).
  3. The linked wallet must be the one specified in walletAddress.

Post a message

await api.postMessage({
  path: { token_address: '7eYw...mintAddr' },
  body: {
    content: 'This is my take on $TOKEN',
    chainId: 'solana',
    walletAddress: 'YourLinkedSolanaWallet',
  },
  throwOnError: true,
});

Posts are processed asynchronously

This is the single most important thing to understand before you build a posting UI. postMessage (and postReply) accept the post for processing and return an empty 200 — there is no response body, no message id, and nothing persisted yet:

const { data } = await api.postMessage({ path, body });
// data is undefined — the 200 only means "accepted for processing"

After the 200, the backend runs the post through moderation, persists it, and warms its caches. Only then does the message become readable via getMessages and broadcast over the realtime socket. The post can also be rejected at this stage (spam / harmful content), in which case it never appears in the feed.

Don't refetch on success — you'll race the backend

The naive flow — postMessage → invalidate the messages cache → refetch getMessages — has a race condition: the refetch almost always wins, reaching the read endpoint before the backend has finished persisting the new post. The user posts and sees… nothing. A moment later a second, unrelated refetch finally shows it. There is no synchronous read-after-write guarantee.

The pattern: optimistic insert + realtime reconciliation

The SDK is designed for this. The recommended flow is:

  1. Optimistically insert the post into your local feed the instant the 200 lands, so it appears immediately. Because there's no server id, mint a temporary client-side id (e.g. optimistic-<uuid>).
  2. Subscribe to the community's realtime socket (see Realtime events). The backend emits a message_update once the post is persisted, and a moderation_update if its spam/harmful flags change.
  3. Reconcile:
    • A clean message_update whose content matches your pending post → drop the optimistic copy; the authoritative message arrives via a cache refetch.
    • A message_update/moderation_update flagged isSpam or isHarmful → remove the optimistic copy and surface a toast ("your post was removed").
    • A dropped socket → refetch on the realtime onGap signal; persisted posts still show once the REST cache catches up.

Because the write endpoint returns no id, you correlate the eventual message_update to your pending post by content (the userId + content pair), not by id. The Realtime events page shows the SDK hook and the lower-level reconciliation loop.

The same applies to postReply: it returns the same empty 200 "accepted for processing" with no body. Replies surface as message_update events with a non-null parentMessageId.

Attach media

Upload media first, then include the returned URL:

const { data: media } = await api.uploadCommunityMedia({
  body: {
    contentType: 'image/jpeg',
    data: btoa(rawImageBytes), // base64
  },
});
 
await api.postMessage({
  path: { token_address: '7eYw...' },
  body: {
    content: 'Check this chart',
    chainId: 'solana',
    walletAddress: 'YourWallet',
    mediaUrl: media.mediaUrl,
  },
});

Free-form URLs are rejected — only URLs from uploadCommunityMedia or a listed media provider are accepted.

Reply to a message

await api.postReply({
  path: { token_address: '7eYw...', message_id: 'msg_123' },
  body: { content: 'Agreed', chainId: 'solana', walletAddress: 'YourWallet' },
});

Likes

await api.likeMessage({ path: { token_address: '7eYw...', message_id: 'msg_123' } });
await api.unlikeMessage({ path: { token_address: '7eYw...', message_id: 'msg_123' } });
const { data } = await api.getLikeCount({ path: { token_address: '7eYw...', message_id: 'msg_123' } });

Report content

await api.reportMessage({
  path: { token_address: '7eYw...', message_id: 'msg_123' },
  body: { reason: 'spam' },
  // reason: 'spam' | 'harassment' | 'hate_speech' | 'inappropriate_content' | 'other'
  // when reason === 'other', include: text: 'describe the issue'
});