Access Paragraph.com Web3-native blogging to create, fetch, and manage onchain posts, publications, subscribers, and tokenized content via API.
---
name: paragraph
description: OpenClaw skill for Paragraph.com — Web3-native blogging with tokenization, onchain storage, and community features
version: 1.2.0
author: Phil (OpenClaw)
license: ISC
homepage: https://github.com/ClaireAICodes/openclaw-skill-paragraph
metadata: { "openclaw": { "emoji": "📝", "requires": { "env": ["PARAGRAPH_API_KEY", "PARAGRAPH_PUBLICATION_SLUG"] } } }
# Skill type
type: tool
# Main entry point
main: skill.js
# Environment variables required
env:
- name: PARAGRAPH_API_KEY
description: Paragraph API authentication key
required: true
- name: PARAGRAPH_PUBLICATION_SLUG
description: Publication slug for URL building (required). Example: "myblog" or "jonathancolton.eth"
required: true
- name: PARAGRAPH_PUBLICATION_ID
description: Default publication ID (optional, not needed if slug is set)
required: false
- name: PARAGRAPH_API_BASE_URL
description: Custom API base URL (for testing)
required: false
# Tools provided
tools:
- paragraph_testConnection
- paragraph_createPost
- paragraph_getPost
- paragraph_getPostBySlug
- paragraph_listPosts
- paragraph_getPublication
- paragraph_getPublicationByDomain
- paragraph_getMyPublication
- paragraph_addSubscriber
- paragraph_listSubscribers
- paragraph_importSubscribers
- paragraph_getFeed
- paragraph_getPostsByTag
- paragraph_getCoin
- paragraph_getCoinByContract
- paragraph_getPopularCoins
- paragraph_listCoinHolders
- paragraph_getUser
- paragraph_getUserByWallet
- paragraph_getSubscriberCount
# Dependencies
dependencies: [] # Uses native fetch, no external deps
# Tags for discovery
tags:
- blogging
- web3
- nft
- tokens
- publishing
- content
- openclaw
- openclaw-skill
- paragraph
- decentralized
- onchain
- social
- creator-economy
- content-automation
# Documentation
documentation: README.md
# Setup instructions
setup:
- name: Get API key
description: Go to Paragraph account settings → Integrations → Generate API key
- name: Get publication slug
description: Find your publication slug in your Paragraph dashboard (e.g., "myblog" or "jonathancolton.eth")
- name: Set environment variables
description: Add PARAGRAPH_API_KEY and PARAGRAPH_PUBLICATION_SLUG to OpenClaw environment
- name: (Optional) Set default publication ID
description: Set PARAGRAPH_PUBLICATION_ID if you prefer using ID over slug (slug is recommended)
# Example usage
examples:
- description: Test Paragraph connection
call: paragraph_testConnection
- description: Get current publication (auto-discovers ID and slug)
call: paragraph_getMyPublication
- description: Create a blog post (fast response by default)
call: paragraph_createPost
params:
title: "My Web3 Blog Post"
markdown: "# Hello\n\nThis is my first post on Paragraph."
sendNewsletter: false
categories: ["web3", "blockchain"]
# waitForProcessing defaults to false – returns immediately; set true to wait for slug/url
- description: List recent posts in publication (auto-discovers ID)
call: paragraph_listPosts
params:
limit: 10
includeContent: false
- description: Get token data for a coined post
call: paragraph_getCoin
params:
coinId: "coin_123"
# Implementation notes
notes:
- Uses native fetch API (Node 19+). No additional dependencies.
- All tools return standardized { success, data, error } format.
- Rate limiting: Implement retry/backoff in agent if needed.
- CSV import expects text/csv raw bytes (see README for format).
- Post updates (PUT) are not supported by the Paragraph API at this time.
- Posts are published onchain immediately upon creation; slug and URL may be undefined until onchain processing completes.
---
# Paragraph OpenClaw Skill
[](https://opensource.org/licenses/ISC)
[](https://github.com/openclaw/openclaw)
[](https://paragraph.com/docs/api-reference)
[](https://nodejs.org)
## Overview
Paragraph.com reimagines blogging for the decentralized era. Posts are stored onchain, can be minted as NFTs (coins), and communities can own and govern content. This skill gives OpenClaw agents full programmatic access to Paragraph's API, enabling automated publishing workflows, subscriber management, and token-gated content strategies.
### Why integrate Paragraph with OpenClaw?
- **Automated content pipelines**: Schedule regular posts, cross-post from other platforms, or auto-publish research reports
- **Tokenized engagement**: Track coin holders, distribute rewards, and build onchain communities
- **Newsletter automation**: Manage subscriber lists, segment audiences, and trigger send-outs without manual work
- **Decentralized permanence**: Your content lives on the blockchain, immune to platform takedowns
- **Monetization ready**: Built-in token economics mean you can launch social tokens around your writing
This skill is production-ready for creators, DAOs, and Web3 projects that want to treat blogging as a protocol, not a siloed service.
## Features in Depth
### Post Management
Create posts with rich Markdown, categories, and optional newsletter dispatch. Paragraph handles onchain anchoring automatically. You can:
- Publish instantly or wait for onchain confirmation (`waitForProcessing`)
- Assign categories for discoverability
- Attach images and embedded content via Markdown
- Retrieve posts by ID or human-readable slug
- List recent posts with or without full content
**Note**: Updating posts isn't supported by Paragraph's API yet. To "edit", you delete and recreate (preserving slug if possible).
### Publication Control
Every Paragraph account has one or more publications (think: multi-author blogs under one roof). This skill can:
- Auto-detect your primary publication using just an API key
- Fetch publication metadata (name, slug, custom domains, settings)
- Look up publications by their ENS-style domain (e.g., `myblog.paragraph.eth`)
### Subscriber Relationship Management
Build and nurture your audience:
- Add subscribers individually via email or wallet address
- Tag subscribers for segmentation (e.g., "premium", "nft-holder", "early-adopter")
- Import bulk lists via CSV (ideal for migrating from Substack, Ghost, etc.)
- Track subscriber count over time
- Double opt-in support for GDPR compliance
CSV format for import:
```csv
email,wallet,tags
alice@example.com,,newsletter
bob@example.com,0x123...,nft-holder
```
### Token & Coin Operations
Paragraph's killer feature: every post can have a coin (social token). This skill exposes:
- Get coin details by ID (supply, holders, price)
- Look up coins by contract address (for external tracking)
- Discover trending/popular coins across the platform
- List coin holders (with pagination) — useful for airdrops or community analysis
Coins enable creators to launch micro-economies around their content. Readers can buy/sell the coin, aligning incentives around the writer's success.
### User & Feed Discovery
- Get user profiles by internal ID or linked wallet
- Fetch the global "For You" feed or get posts filtered by tag
- Combine with coin data to identify influential writers
## Setup Guide
### Step 1: Get your Paragraph API key
1. Log into [Paragraph](https://paragraph.com)
2. Navigate to Account Settings → Integrations
3. Click "Generate API Key"
4. Copy the key (starts with `para_` or similar)
### Step 2: Find your publication slug
Your publication slug is the URL-friendly name used in your blog's address:
- If your blog is `myblog.paragraph.eth`, the slug is `myblog`
- You can also find it in the dashboard under Publication Settings
- Alternatively, set only `PARAGRAPH_API_KEY` and call `paragraph_getMyPublication` to auto-discover
### Step 3: Configure OpenClaw
Add to your OpenClaw environment (config file or export):
```bash
export PARAGRAPH_API_KEY="pk_live_xxxxxxxx"
export PARAGRAPH_PUBLICATION_SLUG="myblog"
```
Restart or reload OpenClaw to pick up the variables.
### Step 4: Verify
```bash
# In an OpenClaw agent session
tools.paragraph_testConnection({}) # should return success: true
tools.paragraph_getMyPublication({}) # should return your publication data
```
## Real-World Use Cases
### Automated Research Publishing
If you run a Web3 research DAO, use OpenClaw to:
1. Scrape or generate daily market reports
2. Format them in Markdown with charts
3. Publish to Paragraph via `paragraph_createPost`
4. Mint a coin for each report to create prediction markets
5. Notify subscribers with the new slug
### Token-Gated Newsletter
1. Build a list of wallet holders from `paragraph_listCoinHolders`
2. Export to CSV and import as `premium` subscribers
3. Create a posts with exclusive insights
4. Use `sendNewsletter: true` to push to that segment only
### Cross-Platform Syndication
- Publish to Paragraph first (onchain timestamp)
- Cross-post to Mirror, Medium, or Twitter with proof of originality
- Track engagement across platforms using coin metrics
### Personal Content Archive
Back up all your blog content to your own knowledge base using OpenClaw's knowledge-management skill alongside this one — parse posts with `paragraph_listPosts`, extract content, and store locally in a structured format.
## Error Handling & Best Practices
### Rate Limits
Paragraph API has rate limits per API key. If you hit limits:
- Implement exponential backoff (wait 1s, 2s, 4s, 8s...)
- Batch operations (e.g., import subscribers instead of individual adds)
- Cache frequently accessed data (publication info, user profiles)
### Onchain Delays
When `waitForProcessing: false`, the post returns immediately but the slug may be undefined for a few seconds/minutes while the transaction confirms. Strategies:
- Poll `paragraph_getPostBySlug` with the returned `id` until slug appears
- Or just set `waitForProcessing: true` for simpler flow (slower)
### CSV Import Quirks
- File must be UTF-8 plain text
- Headers: `email,wallet,tags` (tags are comma-separated within the cell)
- At least one of email or wallet must be present per row
- Duplicate emails/wallets are skipped by Paragraph
### Invalid API Keys
Common causes:
- Key from test environment used in production (or vice versa)
- Key accidentally revoked
- Copy-paste error (extra whitespace)
Use `paragraph_testConnection` to validate.
## Implementation Details
- Written in pure JavaScript (ES modules)
- Uses Node's built-in `fetch` (Node 19+ / OpenClaw's Node 24)
- No external dependencies → minimal attack surface, easy to audit
- Tools are pure functions — easy to test and compose
- Error responses include human-readable messages from Paragraph API
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| `Not logged in` from any tool | `PARAGRAPH_API_KEY` missing/invalid | Set correct key, restart agent |
| `Publication not found` | Slug typo or wrong publication | Verify slug via dashboard or `getMyPublication` |
| `Slug undefined` after create | `waitForProcessing: false` and fast polling | Wait a few seconds and retry, or use `waitForProcessing: true` |
| `Rate limit exceeded` | Too many requests in short time | Add delays, batch calls, or upgrade Paragraph plan |
| CSV import: `invalid format` | Not CSV, wrong headers, binary mode | Ensure UTF-8 text with exact header row |
## Version History
- **1.2.0** (2026-03-10) — Republished to ClawHub with comprehensive docs; added LICENSE; minor improvements
- **1.1.0** — Added coin holder listing, enhanced user lookup
- **1.0.0** — Initial public release with posts, publications, subscribers, coins, feed
## License
ISC © Phil (OpenClaw)
## Contributing
This skill lives at: https://github.com/ClaireAICodes/openclaw-skill-paragraph
Issues and PRs welcome. Please test against a Paragraph sandbox account before submitting.
don't have the plugin yet? install it then click "run inline in claude" again.
formalized the 6-component structure with explicit intent, inputs (env vars and edge cases), numbered procedure with input/output per step, decision points for error handling and branching logic, output contract with json schemas for all data types, and outcome signal with success criteria and diagnostics.
this skill gives you full programmatic access to Paragraph.com's Web3-native blogging API. use it to automate content publishing workflows, manage subscribers at scale, track tokenized engagement, and integrate onchain posts into larger systems. build automated research publishing pipelines, token-gated newsletters, cross-platform syndication, or personal content archives without touching the Paragraph UI.
all required unless noted:
| name | description | required | example |
|---|---|---|---|
PARAGRAPH_API_KEY |
API authentication key from Paragraph account settings → integrations | yes | pk_live_xxxxxxxx |
PARAGRAPH_PUBLICATION_SLUG |
publication slug (URL-friendly name) for your blog | yes | myblog or jonathancolton.eth |
PARAGRAPH_PUBLICATION_ID |
publication ID as fallback (not recommended; slug is preferred) | no | pub_123abc |
PARAGRAPH_API_BASE_URL |
custom API endpoint for testing or self-hosted instances | no | https://api.paragraph.local |
PARAGRAPH_API_KEY with scope to read/write posts, subscribers, publications, and coins. keys expire or can be revoked; always validate with paragraph_testConnection before production workflows.paragraph_getMyPublication to auto-discoverwaitForProcessing: false, post slug may be undefined for seconds to minutes while transaction confirms; poll or set waitForProcessing: true to blockemail,wallet,tags; duplicates are silently skipped by Paragraph.length before iteratinginput: none (uses PARAGRAPH_API_KEY from environment)
call paragraph_testConnection(). this makes a lightweight authenticated request to verify the api key is valid and the Paragraph service is reachable.
output: { success: true } or { success: false, error: "reason" }
decision: if success is false, stop and fix PARAGRAPH_API_KEY or network connectivity before proceeding.
input: none (uses PARAGRAPH_PUBLICATION_SLUG or PARAGRAPH_PUBLICATION_ID from environment)
call paragraph_getMyPublication() to fetch the authenticated user's primary publication. this auto-discovers publication ID, name, domain, subscriber count, and other metadata. no parameters needed.
output: publication object with fields: id, name, slug, domain, subscriberCount, description, avatar, settings
decision: if you don't have PARAGRAPH_PUBLICATION_SLUG set, this call returns the default. if you need a specific publication (multi-publication account), pass publicationId explicitly to subsequent calls.
input:
title (string, required): post titlemarkdown (string, required): post body in markdown (supports images, embeds, code blocks)categories (array of strings, optional): tags for discoverability (e.g., ["web3", "blockchain"])sendNewsletter (boolean, optional, default false): notify subscribers immediatelywaitForProcessing (boolean, optional, default false): block until onchain confirmation and slug is assignedcall paragraph_createPost({ title, markdown, categories, sendNewsletter, waitForProcessing }). Paragraph publishes the post onchain immediately.
output: post object with id, title, slug (may be undefined if waitForProcessing: false), url, content, createdAt, coinId (if post has a coin)
decision: if waitForProcessing: false and you need the slug immediately, poll paragraph_getPost with the returned id every 2-3 seconds until slug appears (usually within 10-30 seconds). if waitForProcessing: true, the call blocks but guarantees slug is present on return.
input: postId (string): internal post identifier
call paragraph_getPost({ postId }). returns full post data including metadata and onchain transaction details.
output: post object as above
edge case: if post does not exist, Paragraph returns a 404 error.
input: slug (string): human-readable post identifier from URL
call paragraph_getPostBySlug({ slug }). useful for fetching a post when you only have the public-facing slug.
output: post object as above
edge case: if slug does not exist or is still pending onchain confirmation, returns 404. use this to poll when waiting for onchain processing to complete.
input:
limit (number, optional, default 10): max posts to returnoffset (number, optional, default 0): pagination offsetincludeContent (boolean, optional, default false): fetch full markdown or just metadatacall paragraph_listPosts({ limit, offset, includeContent }). auto-discovers publication ID from environment.
output: array of post objects (abbreviated if includeContent: false)
edge case: if publication has zero posts, returns empty array. if limit exceeds API max, Paragraph caps silently.
input: publicationSlug (string) or publicationId (string)
call paragraph_getPublication({ publicationSlug }) or paragraph_getPublicationByDomain({ domain }) to fetch a publication by its domain (e.g., myblog.paragraph.eth).
output: publication object with id, name, slug, description, avatar, subscriberCount, settings
edge case: if slug or domain does not exist, returns 404.
input:
email (string, optional): subscriber emailwalletAddress (string, optional): eth wallet addresstags (array of strings, optional): labels for segmentation (e.g., ["premium", "nft-holder"])call paragraph_addSubscriber({ email, walletAddress, tags }). at least one of email or wallet must be provided.
output: subscriber object with id, email, wallet, tags, subscribedAt
edge case: if email or wallet already subscribed, returns the existing subscriber (idempotent). if neither email nor wallet provided, returns validation error.
input:
limit (number, optional, default 100): max subscribers to returnoffset (number, optional, default 0): pagination offsettag (string, optional): filter to subscribers with a specific tagcall paragraph_listSubscribers({ limit, offset, tag }). auto-discovers publication ID from environment.
output: array of subscriber objects
edge case: if publication has zero subscribers, returns empty array. if tag does not exist, returns empty array. pagination is required for large lists.
input:
csvData (string or buffer): utf-8 encoded csv file with headers email,wallet,tagstags (array of strings, optional): additional tags to apply to all imported subscriberscall paragraph_importSubscribers({ csvData, tags }). bulk-loads subscribers from csv, ideal for migrations from substack, ghost, or internal lists.
output: import result object with imported (count), skipped (count), errors (array of row numbers with issues)
csv format:
email,wallet,tags
alice@example.com,,newsletter
bob@example.com,0x123...,nft-holder,premium
edge case: if csv is invalid (bad encoding, missing headers, binary), returns 400 error. if row is missing both email and wallet, that row is skipped. if email/wallet is duplicate, Paragraph silently merges with existing subscriber.
input: none (uses publication from environment)
call paragraph_getSubscriberCount(). lightweight call to fetch aggregate subscriber count.
output: object with count (number)
input:
limit (number, optional, default 10): posts to returnoffset (number, optional, default 0): pagination offsetcall paragraph_getFeed({ limit, offset }). returns trending/recent posts across the Paragraph platform.
output: array of post objects
edge case: empty if no posts exist on platform (unlikely).
input:
tag (string): tag namelimit (number, optional, default 10): posts to returnoffset (number, optional, default 0): pagination offsetcall paragraph_getPostsByTag({ tag, limit, offset }). filters platform posts by a single tag.
output: array of post objects
edge case: if tag does not exist, returns empty array.
input: coinId (string): internal coin identifier
call paragraph_getCoin({ coinId }). returns supply, price, holder count, and metadata for a post's social token.
output: coin object with id, name, symbol, supply, price, holderCount, contractAddress, postId
edge case: if coin does not exist or post has no coin, returns 404.
input: contractAddress (string): ethereum contract address
call paragraph_getCoinByContract({ contractAddress }). resolves a coin from its on-chain contract.
output: coin object as above
edge case: if contract not found on Paragraph, returns 404.
input:
limit (number, optional, default 10): coins to returnoffset (number, optional, default 0): pagination offsetcall paragraph_getPopularCoins({ limit, offset }). returns trending coins by volume or holder growth.
output: array of coin objects
input:
coinId (string): coin identifierlimit (number, optional, default 100): holders to returnoffset (number, optional, default 0): pagination offsetcall paragraph_listCoinHolders({ coinId, limit, offset }). useful for airdrops, community analysis, or reward distribution.
output: array of holder objects with wallet, balance, percentage (of supply)
edge case: if coin has no holders, returns empty array. large holder lists require pagination.
input: userId (string): internal user identifier
call paragraph_getUser({ userId }). returns user profile, wallet, publications, and follower count.
output: user object with id, name, wallet, avatar, bio, followingCount, followerCount, publications
edge case: if user does not exist, returns 404.
input: walletAddress (string): eth address
call paragraph_getUserByWallet({ walletAddress }). reverse-lookup a user by their connected wallet.
output: user object as above
edge case: if wallet not connected to any Paragraph account, returns 404.
if api key is invalid or missing: paragraph_testConnection() returns success: false. stop all subsequent calls, correct PARAGRAPH_API_KEY environment variable, and validate again before proceeding.
if publication slug is not set: you have two options: (a) set PARAGRAPH_PUBLICATION_SLUG in environment and restart agent, or (b) call paragraph_getMyPublication() without parameters to auto-discover the primary publication ID, then use that ID in subsequent calls.
if you need to create a post and want the slug immediately: set waitForProcessing: true to block until onchain confirmation. if you want fast response and don't need slug right away, use default waitForProcessing: false and poll paragraph_getPostBySlug every 2-3 seconds (max wait time 30-60 seconds typically).
if you hit a 429 rate limit error: implement exponential backoff (wait 1s, retry; if still 429, wait 2s, retry; if still 429, wait 4s, retry; if still 429, wait 8s and give up). alternatively, batch operations (e.g., import csv subscribers instead of individual adds) or cache frequently accessed data.
if csv import returns errors: check the errors array in the response; it contains row numbers that failed. common reasons: missing both email and wallet in a row, malformed tags, or non-utf-8 encoding. fix the file and retry.
if a post returns slug undefined and you used waitForProcessing: false: don't assume it failed. the post exists (you have the id) but the onchain transaction hasn't confirmed yet. poll paragraph_getPostBySlug with the id every 2-3 seconds until slug appears, or call with waitForProcessing: true next time for blocking confirmation.
if you need to "edit" a post: Paragraph API does not support PUT/PATCH operations. workaround: delete the post (if you have that tool) and recreate it, or create a new post and reference the old one. slug preservation during recreation depends on Paragraph's internal rules (usually possible if you use the same title).
if subscriber list returns empty but you expect subscribers: verify the publication ID is correct (call paragraph_getMyPublication() to confirm) and check the tag filter if you provided one. also check paragraph_getSubscriberCount() to see if count is nonzero (list might be paginated).
if you're importing subscribers and some rows are skipped: Paragraph silently skips rows with both email and wallet missing, or rows with duplicate email/wallet. this is not an error; check the skipped count in the import result.
all tools return a standardized response object:
{
"success": true|false,
"data": { /* tool-specific data */ },
"error": "human-readable error message (only if success: false)"
}
{
"id": "post_123abc",
"title": "My Web3 Blog Post",
"slug": "my-web3-blog-post",
"url": "https://myblog.paragraph.eth/my-web3-blog-post",
"content": "# Hello\n\nThis is my first post...",
"createdAt": "2024-03-10T12:00:00Z",
"publishedAt": "2024-03-10T12:00:00Z",
"categories": ["web3", "blockchain"],
"coinId": "coin_456def",
"transactionHash": "0x789..."
}
{
"id": "sub_123abc",
"email": "alice@example.com",
"wallet": "0x123...",
"tags": ["newsletter", "premium"],
"subscribedAt": "2024-03-10T12:00:00Z",
"unsubscribedAt": null
}
{
"id": "coin_123abc",
"name": "Alice's Insights",
"symbol": "ALICE",
"supply": "1000000",
"price": "0.05",
"holderCount": 245,
"contractAddress": "0x789...",
"postId": "post_456def"
}
{
"id": "pub_123abc",
"name": "My Blog",
"slug": "myblog",
"domain": "myblog.paragraph.eth",
"description": "thoughts on web3",
"avatar": "https://...",
"subscriberCount": 1250,
"settings": { /* nested config */ }
}
{
"id": "user_123abc",
"name": "Alice",
"wallet": "0x123...",
"avatar": "https://...",
"bio": "Web3 researcher",
"followingCount": 50,
"followerCount": 120,
"publications": [ /* array of publication objects */ ]
}
{
"imported": 45,
"skipped": 2,
"errors": [
{ "row": 12, "reason": "missing email and wallet" },
{ "row": 23, "reason": "duplicate email" }
]
}
all numeric fields (supply, price, counts) are returned as strings or numbers depending on precision; always coerce to number if arithmetic is needed.
success criteria:
paragraph_testConnection() returns { success: true }paragraph_getMyPublication() returns publication object with non-empty id and slugparagraph_createPost(...) returns post object with non-empty id; slug may be undefined if using waitForProcessing: false, but will appear within 30 secondsparagraph_getPostBySlug() or paragraph_getPost() returns the same post with full metadataparagraph_addSubscriber(...) returns subscriber object with the provided email/wallet and tagsparagraph_listPosts(), paragraph_listSubscribers(), etc. return arrays (may be empty if no data, but not null or error)paragraph_getCoin() or paragraph_getCoinByContract() returns coin object with supply and holder countparagraph_importSubscribers() returns result object with nonzero imported count and errors array present (even if empty)paragraph_getFeed(), paragraph_getPostsByTag(), paragraph_getUser(), etc. all return expected data structuresif any tool returns success: false, check the error message for diagnostic info. common errors:
Not logged in → invalid or expired PARAGRAPH_API_KEYPublication not found → wrong PARAGRAPH_PUBLICATION_SLUG or missing env varRate limit exceeded → back off exponentiallyOnchain processing pending → slug not yet available; poll or wait and retryNot found (404) → post/user/coin does not exist; verify id/slug/address