{
  "schemaVersion": "1.0",
  "item": {
    "slug": "towns-protocol",
    "name": "Towns Protocol Skills",
    "source": "tencent",
    "type": "skill",
    "category": "通讯协作",
    "sourceUrl": "https://clawhub.ai/andreyz/towns-protocol",
    "canonicalUrl": "https://clawhub.ai/andreyz/towns-protocol",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/towns-protocol",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=towns-protocol",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md",
      "references/BLOCKCHAIN.md",
      "references/DEBUGGING.md",
      "references/DEPLOYMENT.md",
      "references/INTERACTIVE.md",
      "references/MESSAGING.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/towns-protocol"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/towns-protocol",
    "agentPageUrl": "https://openagent3.xyz/skills/towns-protocol/agent",
    "manifestUrl": "https://openagent3.xyz/skills/towns-protocol/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/towns-protocol/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Critical Rules",
        "body": "MUST follow these rules - violations cause silent failures:\n\nUser IDs are Ethereum addresses - Always 0x... format, never usernames\nMentions require BOTH - <@{userId}> format in text AND mentions array in options\nTwo-wallet architecture:\n\nbot.viem.account.address = Gas wallet (signs & pays fees) - MUST fund with Base ETH\nbot.appAddress = Treasury (optional, for transfers)\n\n\nSlash commands DON'T trigger onMessage - They're exclusive handlers\nInteractive forms use type property - Not case (e.g., type: 'form')\nNever trust txHash alone - Verify receipt.status === 'success' before granting access"
      },
      {
        "title": "Key Imports",
        "body": "import { makeTownsBot, getSmartAccountFromUserId } from '@towns-protocol/bot'\nimport type { BotCommand, BotHandler } from '@towns-protocol/bot'\nimport { Permission } from '@towns-protocol/web3'\nimport { parseEther, formatEther, erc20Abi, zeroAddress } from 'viem'\nimport { readContract, waitForTransactionReceipt } from 'viem/actions'\nimport { execute } from 'viem/experimental/erc7821'"
      },
      {
        "title": "Handler Methods",
        "body": "MethodSignatureNotessendMessage(channelId, text, opts?) → { eventId }opts: { threadId?, replyId?, mentions?, attachments? }editMessage(channelId, eventId, text)Bot's own messages onlyremoveEvent(channelId, eventId)Bot's own messages onlysendReaction(channelId, messageId, emoji)sendInteractionRequest(channelId, payload)Forms, transactions, signatureshasAdminPermission(userId, spaceId) → booleanban / unban(userId, spaceId)Needs ModifyBanning permission"
      },
      {
        "title": "Bot Properties",
        "body": "PropertyDescriptionbot.viemViem client for blockchainbot.viem.account.addressGas wallet - MUST fund with Base ETHbot.appAddressTreasury wallet (optional)bot.botIdBot identifier\n\nFor detailed guides, see references/:\n\nMessaging API - Mentions, threads, attachments, formatting\nBlockchain Operations - Read/write contracts, verify transactions\nInteractive Components - Forms, transaction requests\nDeployment - Local dev, Render, tunnels\nDebugging - Troubleshooting guide"
      },
      {
        "title": "Project Initialization",
        "body": "bunx towns-bot init my-bot\ncd my-bot\nbun install"
      },
      {
        "title": "Environment Variables",
        "body": "APP_PRIVATE_DATA=<base64_credentials>   # From app.towns.com/developer\nJWT_SECRET=<webhook_secret>              # Min 32 chars\nPORT=3000\nBASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/KEY  # Recommended"
      },
      {
        "title": "Basic Bot Template",
        "body": "import { makeTownsBot } from '@towns-protocol/bot'\nimport type { BotCommand } from '@towns-protocol/bot'\n\nconst commands = [\n  { name: 'help', description: 'Show help' },\n  { name: 'ping', description: 'Check if alive' }\n] as const satisfies BotCommand[]\n\nconst bot = await makeTownsBot(\n  process.env.APP_PRIVATE_DATA!,\n  process.env.JWT_SECRET!,\n  { commands }\n)\n\nbot.onSlashCommand('ping', async (handler, event) => {\n  const latency = Date.now() - event.createdAt.getTime()\n  await handler.sendMessage(event.channelId, 'Pong! ' + latency + 'ms')\n})\n\nexport default bot.start()"
      },
      {
        "title": "Config Validation",
        "body": "import { z } from 'zod'\n\nconst EnvSchema = z.object({\n  APP_PRIVATE_DATA: z.string().min(1),\n  JWT_SECRET: z.string().min(32),\n  DATABASE_URL: z.string().url().optional()\n})\n\nconst env = EnvSchema.safeParse(process.env)\nif (!env.success) {\n  console.error('Invalid config:', env.error.issues)\n  process.exit(1)\n}"
      },
      {
        "title": "onMessage",
        "body": "Triggers on regular messages (NOT slash commands).\n\nbot.onMessage(async (handler, event) => {\n  // event: { userId, spaceId, channelId, eventId, message, isMentioned, threadId?, replyId? }\n\n  if (event.isMentioned) {\n    await handler.sendMessage(event.channelId, 'You mentioned me!')\n  }\n})"
      },
      {
        "title": "onSlashCommand",
        "body": "Triggers on /command. Does NOT trigger onMessage.\n\nbot.onSlashCommand('weather', async (handler, { args, channelId }) => {\n  // /weather San Francisco → args: ['San', 'Francisco']\n  const location = args.join(' ')\n  if (!location) {\n    await handler.sendMessage(channelId, 'Usage: /weather <location>')\n    return\n  }\n  // ... fetch weather\n})"
      },
      {
        "title": "onReaction",
        "body": "bot.onReaction(async (handler, event) => {\n  // event: { reaction, messageId, channelId }\n  if (event.reaction === '👋') {\n    await handler.sendMessage(event.channelId, 'I saw your wave!')\n  }\n})"
      },
      {
        "title": "onTip",
        "body": "Requires \"All Messages\" mode in Developer Portal.\n\nbot.onTip(async (handler, event) => {\n  // event: { senderAddress, receiverAddress, amount (bigint), currency }\n  if (event.receiverAddress === bot.appAddress) {\n    await handler.sendMessage(event.channelId,\n      'Thanks for ' + formatEther(event.amount) + ' ETH!')\n  }\n})"
      },
      {
        "title": "onInteractionResponse",
        "body": "bot.onInteractionResponse(async (handler, event) => {\n  switch (event.response.payload.content?.case) {\n    case 'form':\n      const form = event.response.payload.content.value\n      for (const c of form.components) {\n        if (c.component.case === 'button' && c.id === 'yes') {\n          await handler.sendMessage(event.channelId, 'You clicked Yes!')\n        }\n      }\n      break\n    case 'transaction':\n      const tx = event.response.payload.content.value\n      if (tx.txHash) {\n        // IMPORTANT: Verify on-chain before granting access\n        // See references/BLOCKCHAIN.md for full verification pattern\n        await handler.sendMessage(event.channelId,\n          'TX: https://basescan.org/tx/' + tx.txHash)\n      }\n      break\n  }\n})"
      },
      {
        "title": "Event Context Validation",
        "body": "Always validate context before using:\n\nbot.onSlashCommand('cmd', async (handler, event) => {\n  if (!event.spaceId || !event.channelId) {\n    console.error('Missing context:', { userId: event.userId })\n    return\n  }\n  // Safe to proceed\n})"
      },
      {
        "title": "Common Mistakes",
        "body": "MistakeFixinsufficient funds for gasFund bot.viem.account.address with Base ETHMention not highlightingInclude BOTH <@userId> in text AND mentions arraySlash command not workingAdd to commands array in makeTownsBotHandler not triggeringCheck message forwarding mode in Developer PortalwriteContract failingUse execute() for external contractsGranting access on txHashVerify receipt.status === 'success' firstMessage lines overlappingUse \\n\\n (double newlines), not \\nMissing event contextValidate spaceId/channelId before using"
      },
      {
        "title": "Resources",
        "body": "Developer Portal: https://app.towns.com/developer\nDocumentation: https://docs.towns.com/build/bots\nSDK: https://www.npmjs.com/package/@towns-protocol/bot\nChain ID: 8453 (Base Mainnet)"
      }
    ],
    "body": "Towns Protocol Bot SDK Reference\nCritical Rules\n\nMUST follow these rules - violations cause silent failures:\n\nUser IDs are Ethereum addresses - Always 0x... format, never usernames\nMentions require BOTH - <@{userId}> format in text AND mentions array in options\nTwo-wallet architecture:\nbot.viem.account.address = Gas wallet (signs & pays fees) - MUST fund with Base ETH\nbot.appAddress = Treasury (optional, for transfers)\nSlash commands DON'T trigger onMessage - They're exclusive handlers\nInteractive forms use type property - Not case (e.g., type: 'form')\nNever trust txHash alone - Verify receipt.status === 'success' before granting access\nQuick Reference\nKey Imports\nimport { makeTownsBot, getSmartAccountFromUserId } from '@towns-protocol/bot'\nimport type { BotCommand, BotHandler } from '@towns-protocol/bot'\nimport { Permission } from '@towns-protocol/web3'\nimport { parseEther, formatEther, erc20Abi, zeroAddress } from 'viem'\nimport { readContract, waitForTransactionReceipt } from 'viem/actions'\nimport { execute } from 'viem/experimental/erc7821'\n\nHandler Methods\nMethod\tSignature\tNotes\nsendMessage\t(channelId, text, opts?) → { eventId }\topts: { threadId?, replyId?, mentions?, attachments? }\neditMessage\t(channelId, eventId, text)\tBot's own messages only\nremoveEvent\t(channelId, eventId)\tBot's own messages only\nsendReaction\t(channelId, messageId, emoji)\t\nsendInteractionRequest\t(channelId, payload)\tForms, transactions, signatures\nhasAdminPermission\t(userId, spaceId) → boolean\t\nban / unban\t(userId, spaceId)\tNeeds ModifyBanning permission\nBot Properties\nProperty\tDescription\nbot.viem\tViem client for blockchain\nbot.viem.account.address\tGas wallet - MUST fund with Base ETH\nbot.appAddress\tTreasury wallet (optional)\nbot.botId\tBot identifier\n\nFor detailed guides, see references/:\n\nMessaging API - Mentions, threads, attachments, formatting\nBlockchain Operations - Read/write contracts, verify transactions\nInteractive Components - Forms, transaction requests\nDeployment - Local dev, Render, tunnels\nDebugging - Troubleshooting guide\nBot Setup\nProject Initialization\nbunx towns-bot init my-bot\ncd my-bot\nbun install\n\nEnvironment Variables\nAPP_PRIVATE_DATA=<base64_credentials>   # From app.towns.com/developer\nJWT_SECRET=<webhook_secret>              # Min 32 chars\nPORT=3000\nBASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/KEY  # Recommended\n\nBasic Bot Template\nimport { makeTownsBot } from '@towns-protocol/bot'\nimport type { BotCommand } from '@towns-protocol/bot'\n\nconst commands = [\n  { name: 'help', description: 'Show help' },\n  { name: 'ping', description: 'Check if alive' }\n] as const satisfies BotCommand[]\n\nconst bot = await makeTownsBot(\n  process.env.APP_PRIVATE_DATA!,\n  process.env.JWT_SECRET!,\n  { commands }\n)\n\nbot.onSlashCommand('ping', async (handler, event) => {\n  const latency = Date.now() - event.createdAt.getTime()\n  await handler.sendMessage(event.channelId, 'Pong! ' + latency + 'ms')\n})\n\nexport default bot.start()\n\nConfig Validation\nimport { z } from 'zod'\n\nconst EnvSchema = z.object({\n  APP_PRIVATE_DATA: z.string().min(1),\n  JWT_SECRET: z.string().min(32),\n  DATABASE_URL: z.string().url().optional()\n})\n\nconst env = EnvSchema.safeParse(process.env)\nif (!env.success) {\n  console.error('Invalid config:', env.error.issues)\n  process.exit(1)\n}\n\nEvent Handlers\nonMessage\n\nTriggers on regular messages (NOT slash commands).\n\nbot.onMessage(async (handler, event) => {\n  // event: { userId, spaceId, channelId, eventId, message, isMentioned, threadId?, replyId? }\n\n  if (event.isMentioned) {\n    await handler.sendMessage(event.channelId, 'You mentioned me!')\n  }\n})\n\nonSlashCommand\n\nTriggers on /command. Does NOT trigger onMessage.\n\nbot.onSlashCommand('weather', async (handler, { args, channelId }) => {\n  // /weather San Francisco → args: ['San', 'Francisco']\n  const location = args.join(' ')\n  if (!location) {\n    await handler.sendMessage(channelId, 'Usage: /weather <location>')\n    return\n  }\n  // ... fetch weather\n})\n\nonReaction\nbot.onReaction(async (handler, event) => {\n  // event: { reaction, messageId, channelId }\n  if (event.reaction === '👋') {\n    await handler.sendMessage(event.channelId, 'I saw your wave!')\n  }\n})\n\nonTip\n\nRequires \"All Messages\" mode in Developer Portal.\n\nbot.onTip(async (handler, event) => {\n  // event: { senderAddress, receiverAddress, amount (bigint), currency }\n  if (event.receiverAddress === bot.appAddress) {\n    await handler.sendMessage(event.channelId,\n      'Thanks for ' + formatEther(event.amount) + ' ETH!')\n  }\n})\n\nonInteractionResponse\nbot.onInteractionResponse(async (handler, event) => {\n  switch (event.response.payload.content?.case) {\n    case 'form':\n      const form = event.response.payload.content.value\n      for (const c of form.components) {\n        if (c.component.case === 'button' && c.id === 'yes') {\n          await handler.sendMessage(event.channelId, 'You clicked Yes!')\n        }\n      }\n      break\n    case 'transaction':\n      const tx = event.response.payload.content.value\n      if (tx.txHash) {\n        // IMPORTANT: Verify on-chain before granting access\n        // See references/BLOCKCHAIN.md for full verification pattern\n        await handler.sendMessage(event.channelId,\n          'TX: https://basescan.org/tx/' + tx.txHash)\n      }\n      break\n  }\n})\n\nEvent Context Validation\n\nAlways validate context before using:\n\nbot.onSlashCommand('cmd', async (handler, event) => {\n  if (!event.spaceId || !event.channelId) {\n    console.error('Missing context:', { userId: event.userId })\n    return\n  }\n  // Safe to proceed\n})\n\nCommon Mistakes\nMistake\tFix\ninsufficient funds for gas\tFund bot.viem.account.address with Base ETH\nMention not highlighting\tInclude BOTH <@userId> in text AND mentions array\nSlash command not working\tAdd to commands array in makeTownsBot\nHandler not triggering\tCheck message forwarding mode in Developer Portal\nwriteContract failing\tUse execute() for external contracts\nGranting access on txHash\tVerify receipt.status === 'success' first\nMessage lines overlapping\tUse \\n\\n (double newlines), not \\n\nMissing event context\tValidate spaceId/channelId before using\nResources\nDeveloper Portal: https://app.towns.com/developer\nDocumentation: https://docs.towns.com/build/bots\nSDK: https://www.npmjs.com/package/@towns-protocol/bot\nChain ID: 8453 (Base Mainnet)"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/andreyz/towns-protocol",
    "publisherUrl": "https://clawhub.ai/andreyz/towns-protocol",
    "owner": "andreyz",
    "version": "2.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/towns-protocol",
    "downloadUrl": "https://openagent3.xyz/downloads/towns-protocol",
    "agentUrl": "https://openagent3.xyz/skills/towns-protocol/agent",
    "manifestUrl": "https://openagent3.xyz/skills/towns-protocol/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/towns-protocol/agent.md"
  }
}