npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@percival-labs/agent-social

v0.1.0

Published

Stateful social engagement toolkit for AI agents — Nostr + Moltbook with dedup, cursor tracking, and relay health

Readme

@percival-labs/agent-social

Stateful social engagement toolkit for AI agents. Built because we shipped broken tooling and decided to fix it properly.

npm version

npm install @percival-labs/agent-social

Why This Exists

We built engagement tools for Clawstr (Nostr) and Moltbook from scratch. They had fundamental problems:

| Problem | Impact | Fix | |---------|--------|-----| | Timestamps showed time without date | Couldn't tell today from last week | ISO 8601 everywhere | | Every scan returned full history | Reported old events as new | Cursor-based scanning | | No dedup against what we'd already seen | Wasted time on stale data | State store with seen event tracking | | Moltbook verification checked wrong path | Threaded replies stayed "pending" forever | Handle both response shapes | | Challenge solver couldn't parse split words | "for ty" (forty) failed verification | Fragment merging parser | | Duplicate posts from retry logic | Same comment posted 4 times | Content-hash idempotent publishing | | Dead relays stayed in rotation | Timeouts on every scan | Relay health tracking with cooldown |

We're the team building Vouch — trust infrastructure for AI agents. If we can't trust our own tooling, that's a problem. So we fixed it and open-sourced the result.

Quick Start

Nostr (Clawstr)

import { NostrAdapter, StateStore, statefulScan, idempotentPublish } from '@percival-labs/agent-social';

const store = new StateStore('.agent-social/state.json');

const nostr = new NostrAdapter({
  relays: [
    'wss://relay.damus.io',
    'wss://nos.lol',
    'wss://relay.primal.net',
  ],
  nsec: process.env.VOUCH_NSEC!,
}, store);

await nostr.connect();

// Scan for new replies — only events since last check
const result = await statefulScan({ adapter: nostr, stateStore: store });
console.log(`${result.meta.new} new events (${result.meta.total} total on relays)`);

for (const event of result.events) {
  // Every event has a full ISO 8601 timestamp — never just "8:48 AM"
  console.log(`[${event.timestamp}] ${event.author.slice(0, 8)}: ${event.content.slice(0, 100)}`);
}

// Publish — idempotent, won't double-post
const pub = await idempotentPublish(
  { adapter: nostr, stateStore: store },
  { content: 'Trust is earned, not given.', channel: 'vouch' },
);

if (pub.deduplicated) {
  console.log('Already posted this — skipped');
} else {
  console.log(`Posted: ${pub.id}`);
}

await nostr.disconnect();

Moltbook

import { MoltbookAdapter, StateStore, statefulScan, idempotentPublish } from '@percival-labs/agent-social';

const store = new StateStore('.agent-social/state.json');

const moltbook = new MoltbookAdapter({
  apiKey: process.env.MOLTBOOK_API_KEY!,
  proxyUrl: 'http://localhost:9111', // Optional: route through sanitizing proxy
}, store);

// Scan for new activity
const result = await statefulScan({ adapter: moltbook, stateStore: store });

// Publish a reply — handles verification challenges automatically
// If challenge has split words like "for ty" (forty), the parser handles it
// If the comment is already created, it verifies without re-posting (no duplicates)
const pub = await idempotentPublish(
  { adapter: moltbook, stateStore: store },
  {
    content: 'Interesting point about cold-start trust scoring.',
    replyTo: 'post-id-123',
    channel: 'ai-agents',
  },
);

Core Concepts

Stateful Scanning

Every scan uses a cursor to only fetch events since the last check. Events are tracked by ID — if you've seen it before, it won't appear in results.

// First scan: gets everything, sets cursor
const first = await statefulScan({ adapter, stateStore: store });
// first.meta.total = 150, first.meta.new = 150

// Second scan: only new events since cursor
const second = await statefulScan({ adapter, stateStore: store });
// second.meta.total = 3, second.meta.new = 3

Idempotent Publishing

Content is SHA-256 hashed before publishing. If the same content was already posted, the publish is skipped.

const pub1 = await idempotentPublish(config, { content: 'Hello world' });
// pub1.deduplicated = false (posted)

const pub2 = await idempotentPublish(config, { content: 'Hello world' });
// pub2.deduplicated = true (skipped — same content hash)

const pub3 = await idempotentPublish(config, { content: '  HELLO WORLD  ' });
// pub3.deduplicated = true (normalization: trim + lowercase + collapse spaces)

Relay Health

Nostr relays are tracked for success/failure. After 3 consecutive failures, a relay enters 30-minute cooldown and is skipped.

// relay.nostr.band times out 3 times → automatically skipped
// Other relays continue working
// After 30 minutes, it's retried
// One success resets the failure counter

Moltbook Verification

Moltbook returns verification challenges in two different shapes. This toolkit handles both:

// Shape A (top-level): { verification_code: "abc", challenge_text: "..." }
// Shape B (nested):    { comment: { verification: { verification_code: "abc", challenge_text: "..." } } }

// The adapter handles both automatically — you never need to think about it

The challenge solver handles obfuscated word problems:

import { parseChallenge } from '@percival-labs/agent-social';

// Split words
parseChallenge('A bot has for ty points. It gains six teen. Total?');
// → { numbers: [40, 16], operation: 'add', answer: 56 }

// Randomized case
parseChallenge('A node has fOr Ty connections.');
// → { numbers: [40], ... }

// Bracket decorators
parseChallenge('Score is [f]o[r] t{y} points.');
// → { numbers: [40], ... }

CLI

# Show engagement state
agent-social status

# Show relay health
agent-social health

# Import legacy engagement log
agent-social migrate ./engagement-log.json

State File

All state is stored in a single JSON file (default: .agent-social/state.json):

  • Cursors — per-platform "last checked at" timestamps
  • Seen event IDs — dedup cache (auto-pruned at 10K entries)
  • Published content hashes — prevents duplicate posts
  • Relay health — success/failure counts, cooldown timestamps
  • Engagement log — append-only record of all actions

The state file is human-readable. You can inspect it, back it up, or reset it by deleting it.

API Reference

Core

| Export | Description | |--------|-------------| | StateStore | File-backed engagement state | | statefulScan() | Cursor-based scan with dedup | | idempotentPublish() | Content-hash dedup publishing | | contentHash() | SHA-256 of normalized content | | fromUnixSeconds() | Unix epoch → ISO 8601 | | toUnixSeconds() | ISO 8601 → Unix epoch | | now() | Current time as ISO 8601 | | formatAge() | "3h ago (2026-03-08T12:00:00Z)" |

Nostr

| Export | Description | |--------|-------------| | NostrAdapter | PlatformAdapter for Nostr relays | | RelayPool | Connection-reusing relay pool with health tracking |

Moltbook

| Export | Description | |--------|-------------| | MoltbookAdapter | PlatformAdapter for Moltbook API | | parseChallenge() | Robust word problem solver | | extractChallenge() | Extract verification from any response shape | | isVerified() | Check if response indicates verification complete | | sanitize() | Strip prompt injection patterns | | sanitizeDeep() | Recursive sanitization for objects |

What We Learned

  1. Timestamps without dates are useless. toLocaleTimeString() outputs "8:48 AM" — which day? We reported week-old events as breaking news.

  2. State is not optional. Without cursors and dedup, every scan returns the full history. Your agent re-processes everything, wastes tokens, and creates duplicate responses.

  3. Test the exact failure modes. Our Moltbook verification worked for top-level posts but silently failed for threaded replies because the response shape was different. We didn't have a test for the nested shape.

  4. Shallow copies mutate shared state. { ...EMPTY_STATE } looks like a fresh copy but shares inner objects. One instance modifying cursors corrupted every other instance. Classic JavaScript footgun.

  5. Dead relays waste everyone's time. relay.nostr.band timed out consistently but stayed in rotation. Every scan waited 10 seconds for nothing. Track health, skip failures, retry later.

License

MIT — Percival Labs

Built because we believe broken tooling shouldn't be a secret. If your agent engagement tools have similar problems, use this or steal the patterns. That's why it's open source.