{"id":89326,"plugin_id":"plugin_asdk_app_6af1a0764cfc8191947efb344ca5e5d9","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-11-10T15:20:31.278Z","digest":"ec367b8b599e59e1eeb1d02f2f7c689b6087961610c366ff3ce8144ecc26043f","against":null,"payload":{"description":"Set up Moderation API (moderationapi.com) content moderation in an existing codebase - link or create the project with the modapi CLI, find where users create content, add the moderation call, act on the result, and verify webhooks. Use when the user asks to add content moderation, set up or integrate Moderation API, moderate user-generated content (messages, posts, comments, profiles, uploads), or block spam, scams or abuse in their app.","included_files":[{"relative_path":"references/sdk-snippets.md","size_in_bytes":9454}],"name":"moderation-api-setup","skill_md_contents":"---\nname: moderation-api-setup\ndescription: Set up Moderation API (moderationapi.com) content moderation in an existing codebase - link or create the project with the modapi CLI, find where users create content, add the moderation call, act on the result, and verify webhooks. Use when the user asks to add content moderation, set up or integrate Moderation API, moderate user-generated content (messages, posts, comments, profiles, uploads), or block spam, scams or abuse in their app.\nmetadata:\n  openclaw:\n    emoji: \"🛡️\"\n    homepage: https://docs.moderationapi.com/ai/skill\n    envVars:\n      - name: MODAPI_SECRET_KEY\n        required: false\n        description: Project API key. `modapi init` writes it to the app's env file; the SDKs read it from there.\n      - name: MODAPI_WEBHOOK_SECRET\n        required: false\n        description: Webhook signing secret. `modapi init` writes it to the app's env file; the SDKs read it to verify webhooks.\n---\n\n# Set up Moderation API\n\nYou add Moderation API to the user's existing app. The `modapi` CLI does the\naccount work (sign-in, project, keys). You do the code: find where users create\ncontent, moderate it on the server, and act on the result.\n\nWork in this order. Ask before each change to the user's code or account.\n\n## Which tool does which job\n\nModeration API has three tools for agents. Use each one for its own job:\n\n| Tool | Job |\n|------|-----|\n| This skill | Write the integration in the user's code. |\n| `modapi` CLI | Sign in, create or link the project, write the keys, check the setup (`init`, `doctor`). |\n| MCP server (`https://mcp.moderationapi.com/mcp`) | Operate the account: review queues, authors, policies (channels), wordlists, webhooks, stats. |\n\nDo not do account work with hand-written API calls when the CLI or the MCP\nserver does it. Do not ask for keys in the chat: the CLI writes them.\n\n## Use the docs\n\nThis skill gives the steps and the decisions. The docs give the details, and\nthey change with the API. Before you use a field, event or option that this\nskill does not show, read its page:\n\n- Index of all pages: `https://docs.moderationapi.com/llms.txt`. Add `.md` to\n  any docs URL to get the page as Markdown.\n- Exact request and response fields: the API reference pages listed there, or\n  `modapi <command> --schema` for a request body.\n- If the docs and the SDK types disagree, follow the SDK types and tell the\n  user.\n\n## 1. Check the starting point\n\n- Run `modapi --version`. If it is missing or older than 0.4.0, install it:\n  `brew install moderation-api/tap/modapi` (`brew upgrade modapi` to update),\n  or `go install github.com/moderation-api/cli/cmd/modapi@latest`.\n  Without Homebrew or Go, ask the user to download the binary for their system\n  from https://github.com/moderation-api/cli/releases and put it on their PATH.\n- Run `modapi doctor -o json` in the project root. It tells you if a project\n  key exists, if it works, if the SDK is installed, and if the env file is in\n  `.gitignore`. Each check that is not `ok` has a `remedy`.\n- If `doctor` says the user is not signed in, ask them to run `modapi login`\n  now (it opens their browser; `modapi login --device` over SSH). The next\n  steps need it.\n- Search the code for an existing integration (`@moderation-api/sdk`,\n  `moderation_api`, `moderation-api/sdk-php`, `moderationapi.com`). If one\n  exists, do not set up a second one: fill the gaps doctor reports.\n- Check if a ready-made integration fits better than code. Tell the user and\n  ask before you write code:\n  - **WordPress site**: install the\n    [Moderation API plugin](https://wordpress.org/plugins/moderation-api-automated-content-moderation/),\n    then connect the site under **Integrate → Platforms** in the dashboard.\n  - **Discord server** or **Slack workspace** that the user wants moderated:\n    connect it under **Integrate → Platforms** in the dashboard. No code.\n  - Other platforms: see https://moderationapi.com/integrations.\n\n  A custom app that has its own Discord or Slack bot code is not this case:\n  add the moderation call to the bot's code.\n- If the app already uses another moderation service, stop and ask the user\n  whether to replace it or run both.\n\n## 2. Plan and get approval\n\nRead the codebase and write a short plan for the user:\n\n- **The project**: a new Moderation API project, or one the user already has.\n  `modapi projects list -o json` shows their projects. Ask which to use; do\n  not pick one for them.\n- **Shadow mode**: ask if they want to start in shadow mode. In shadow mode\n  (`DRY_RUN`) Moderation API analyzes everything, and the dashboard and review\n  queue show the real results, but every response says `allow`. The app\n  changes nothing for users while the team checks the results. Switching it\n  on later is one command, with no code change. Recommend it for an app that\n  is already live.\n- **The platform**: what it is, who uses it, whether children use it. For a\n  new project you pass this to `init` so it suggests the right policies.\n- **The content entry points**: every server-side place where user content is\n  created or changed, for example chat messages, posts, comments, reviews,\n  profile names and bios, listing titles, image uploads. Give file and function.\n- **Channels**: one channel for each kind of content (for example `messages`,\n  `posts`, `profiles`, `reviews`). Each channel has its own policies, and\n  webhooks tell you the channel, so the app knows which table to update.\n- **For each entry point**: its channel, and what to do on `reject` and\n  `review` (see step 5).\n- **Content not saved yet**: if an entry point moderates before the record\n  exists, choose one way (see step 4) and say which.\n- **Webhooks**: whether the app needs to act when a moderator decides later.\n\nShow the plan and wait for a \"yes\". Moderation policy is a product decision:\npropose, do not decide alone.\n\n## 3. Link or create the project\n\nTo use a project the user already has:\n\n```bash\nmodapi init --project \"<project ID or name>\" -o json\n```\n\nTo create a new one:\n\n```bash\nmodapi init --new --name \"<app name>\" --domain <their domain> \\\n  --context \"<what the platform is and who uses it>\" \\\n  --brief \"<what to moderate: the problems and the guidelines>\" \\\n  [--used-by-minors] -o json\n```\n\n- Without `--project` or `--new`, init fails with `validation_error` when the\n  organization has projects, and lists them. Ask the user, then run it again\n  with their choice.\n- Linking makes a new key for the project, named after this computer and\n  directory. The project's other keys keep working.\n- `init` writes `MODAPI_SECRET_KEY` and `MODAPI_WEBHOOK_SECRET` to `.env.local`\n  (Next.js) or `.env`, and adds that file to `.gitignore`. It never prints them.\n- If it fails with `authentication_error`, the user must sign in. Ask them to\n  run `modapi login` (it opens their browser; `modapi login --device` over SSH).\n  Do not ask for keys in the chat.\n- If `status` is `already_set_up`, a key exists: keep it and continue.\n- Run `--dry-run -o json` first if you want to see what init will do.\n- Add `--install` to install the SDK, or run the `install_command` from the\n  result. There is no Go SDK: in Go, call the REST API.\n- `init --dry-run` only previews what `init` does. It is not shadow mode.\n\nThen set up what the plan agreed:\n\n- **Channels**: `modapi channels list --project-id <id> -o json` shows the\n  existing ones. Add one with\n  `modapi channels create --project-id <id> --name \"Messages\" --key messages --content-type message -o json`.\n- **Shadow mode**: `modapi projects update <id> --global-flagging-mode DRY_RUN`\n  for the whole project, or `modapi channels update <id> --flagging-mode DRY_RUN`\n  for one channel.\n\n## 4. Add the moderation call\n\nMake one small helper in the app's server code that calls `content.submit`,\nand call it from each approved entry point. Snippets for each SDK are in\n[references/sdk-snippets.md](references/sdk-snippets.md).\n\nSend with every call:\n\n- `content`: `{type: \"text\", text}`, `{type: \"image\", url}`, or\n  `{type: \"object\", data: {...}}` for content with several fields (a listing\n  with title, description and photos).\n- `channel`: the channel key for this entry point.\n- `contentId`: the ID of the record in the app's database. Webhooks send it\n  back as `item.id`. Sending the same `contentId` again (after an edit)\n  updates the item instead of adding a second one.\n- `authorId`: the user's ID. It turns on author trust levels and blocking.\n- `conversationId`: the chat, thread or room, when there is one. It gives the\n  AI the context of the conversation.\n- `metadata`: optional small facts that help reviewers.\n\n**Content not saved yet.** Choose one way per entry point:\n\n- Moderate first (recommended when `reject` must stop the content): leave out\n  `contentId`. The API makes one and returns it as `content.id`. Save it on\n  the record, so webhooks can find the record later.\n- Save first, then moderate with the new ID, and delete or hide the record on\n  `reject`. Simpler IDs, but rejected content exists for a moment.\n\n**The app's own spam checks.** If the app already has a verdict (IP ban,\nCAPTCHA fail, fraud score), send it as `clientAction`, for example\n`{action: \"review\", behavior: \"escalate\", source: \"fraud-score\", reason: \"...\"}`.\n`escalate` only makes the result stricter; `override` replaces it. Do not run\nthe two checks side by side: with `clientAction`, moderators see the app's\nverdict in the review queue.\n\n**Authors.** Moderators work better when they know who wrote the content.\nWhen a user signs up or changes their profile, send their name, email,\nprofile picture and profile link (`external_link`) with `authors.create` or\n`authors.update` (`external_id` = the `authorId` you send). See\nhttps://docs.moderationapi.com/users/updating-authors.md. For more context\nin the review queue (plan, admin links), the team can set up\n[custom data](https://docs.moderationapi.com/users/custom-data.md) later.\n\n**User reports.** If the app lets users report content, send each report\nwith `actions.execute` and a report action, so it goes to the review queue.\nSee https://docs.moderationapi.com/guides/user-flagged-content.md. Propose it\nin the plan; do not build it unasked.\n\n**Reviews in the app's own tool.** If moderators decide in the app's own admin\ntool instead of the review queue, send each decision with `content.allow` or\n`content.reject` and `moderator` (the reviewer's email or user ID in the\norganization). Without `moderator`, the decision counts as automated: it is\nnot attributed to anyone and does not become a casebook case. An unknown\n`moderator` is a 400 error. See\nhttps://docs.moderationapi.com/actions/content-actions.md.\n\n## 5. Act on the result\n\nRead `recommendation.action`:\n\n| Action   | Default handling |\n|----------|------------------|\n| `allow`  | Save and publish as normal. |\n| `review` | Save it, and hide it or publish it, as agreed in the plan. It is in the review queue for a moderator. |\n| `reject` | Do not publish. Show the user a short, neutral message. |\n\nTreat any other value as `review`: the SDK types allow values that may be\nadded later.\n\nAlso read:\n\n- `content.modified`: when it is not `null`, the API masked part of the\n  content (for example contact details or URLs). Save and show this version,\n  not the original. For object content it holds the fields.\n- `policies[].flagged_fields` (object content): the keys of the fields that a\n  policy flagged. Hold back only those fields, not the whole update, when the\n  plan says so.\n- `author.block` and `author.trust_level`: a blocked author's content can be\n  refused without a call.\n\nIn shadow mode the action is always `allow` and `reason_codes` has `dry_run`.\nKeep the full handling code anyway, so switching shadow mode off needs no\ncode change.\n\nRules:\n\n- Call from the server only. The key must never reach the browser or the app bundle.\n- **Fail open.** If the API is down or times out, log it and allow the content,\n  so moderation never stops users from posting. Use a short timeout.\n- Moderate on create and on edit.\n- Do not store the full API response on every record. `contentId` is enough\n  to find it in the dashboard.\n\n## 6. Webhooks (when the plan needs them)\n\nModerators decide on `review` items later in the dashboard. A webhook tells the\napp the decision.\n\n1. Add a POST route, for example `/webhooks/moderation`. Verify the signature\n   against the **raw** request body with the SDK (`validateWebhook` in\n   TypeScript, `construct_event` in Python and Ruby, `constructEvent` in PHP).\n   They read `MODAPI_WEBHOOK_SECRET`. See the snippets file.\n2. Handle `event.type`. In `queue_item.*` events, `data.object.item` has\n   `id` (the `contentId` you sent, or the generated `content.id`),\n   `author_id` (your user ID) and `channel_key` (route on it to find the\n   right table):\n   - `queue_item.rejected`: hide or delete the content.\n   - `queue_item.allowed`: publish content you held for review.\n   - `queue_item.action`: a custom action; `data.object.key` names it.\n   - `author.blocked` / `author.suspended` / `author.unblocked`: update the\n     user. These events have no `item`: your user ID is\n     `data.object.author.external_id`, and `author.block.until` is a time in\n     **milliseconds** (or `null`).\n\n   The full payloads are at https://docs.moderationapi.com/actions/webhooks.md.\n3. Register the URL once it is public:\n   `modapi webhooks create --name \"App\" --url https://<host>/webhooks/moderation -e QUEUE_ITEM_REJECTED -e QUEUE_ITEM_ALLOWED -e AUTHOR_BLOCKED`\n   For local testing, use a tunnel (for example `ngrok http 3000`).\n\n## 7. Verify\n\nFirst check the code against these common mistakes. Fix each one, or tell\nthe user why it does not apply:\n\n- [ ] The code acts on `recommendation.action`, not on `evaluation.flagged`.\n      The recommendation includes the team's rules, thresholds and review\n      routing; `flagged` only says that a policy matched.\n- [ ] `review` has its own handling, not the same as `allow` or `reject`.\n- [ ] Every place where users create content is covered, not only text:\n      images, profile names and bios, display names, uploads.\n- [ ] Blocked authors are handled: the app reads `author.block`, or acts on\n      the `author.blocked` webhook.\n- [ ] Webhooks are set up when content can be held for review, or when the\n      team takes actions in the dashboard (block an author, reject later).\n      Without them, the app never learns the decision.\n- [ ] The webhook route checks the signature on the raw body, and handles\n      the same event twice without harm (webhooks are retried).\n- [ ] Every call sends `authorId`, and `conversationId` where content is part\n      of a chat or thread. They turn on author history and context.\n- [ ] Content that users can edit sends a stable `contentId`, and edits are\n      moderated again with the same `contentId`.\n- [ ] Each kind of content uses its own `channel`.\n- [ ] Calls run on the server only, with a short timeout, and fail open.\n- [ ] No key is in the code, in a committed file, or in the chat.\n\nThen:\n\n1. Run `modapi doctor`. Fix every `fail`.\n2. Send one test: `modapi content submit --content '{\"type\":\"text\",\"text\":\"Buy followers now, DM me on WhatsApp +1 555 0100\"}' --jq '.recommendation.action'`\n3. Start the app and create content through one real entry point. Confirm the\n   request in the dashboard (the link is in the `init` result).\n4. Tell the user what you changed, file by file, and give them the link to\n   review the suggested policies.\n5. In shadow mode, tell the user how to go live when they trust the results:\n   `modapi projects update <id> --global-flagging-mode ACTIVE` (or\n   `modapi channels update <id> --flagging-mode ACTIVE` per channel).\n\n## 8. Next: operate it with the MCP server\n\nWhen the integration works, tell the user what their agent can do next with\nthe MCP server. Give two or three examples that fit their app, for example:\n\n- \"Go through today's unresolved queue items, resolve the obvious spam, and\n  show me the ones you are unsure about.\"\n- \"Is user 8812 a bad actor? Check their history and block them if it is a\n  pattern.\"\n- \"These comments were flagged but are fine, and these got through. Fix the\n  policy.\"\n- \"How did moderation go this month?\"\n\nIf tools named like `queue-items-list` or `channels-update` are already\navailable to you, the server is connected (the Moderation API plugin includes\nit): say so and stop. If not, give the connect steps for the user's agent:\n\n- Claude Code: `claude mcp add --transport http moderation-api https://mcp.moderationapi.com/mcp`\n- Claude Desktop or claude.ai: Settings > Connectors > Add custom connector,\n  with the URL above.\n- Codex or ChatGPT: install the Moderation API plugin from the plugin\n  directory. It includes the server.\n- Cursor and other clients: add a remote MCP server with the URL above.\n\nThe user signs in with their Moderation API account in the browser (OAuth).\nDo not connect it without asking.\n\n## When Moderation API cannot do it\n\nIf the user asks for something that Moderation API does not seem to support:\n\n1. Check first. Search the docs (`https://docs.moderationapi.com/llms.txt`),\n   the MCP tools if connected, and `modapi --help`. Only say it is not possible\n   after this check.\n2. Tell the user plainly what is not supported. Give the closest thing that\n   works today, if there is one. Never make up an endpoint, a parameter or an\n   SDK method.\n3. Offer to file a feature request with the Moderation API team. Draft it\n   with:\n   - what the user tried to do, and why (their use case)\n   - what you checked, and the workaround you used, if any\n   - the language, framework and SDK\n4. Show the draft to the user. Do not put user content, keys or personal\n   data in it unless the user adds them. Send it only after the user says yes:\n   use the MCP server's feedback tool if it has one, or else give the user the\n   draft to paste at https://moderationapi.productlane.com/request.\n\n## Never\n\n- Print, read aloud or commit `.env` values. Refer to them by name.\n- Ask the user to paste an API key into the chat. Use `modapi login`, or ask\n  them to put the key in the env file themselves.\n- Call the API from client-side code.\n- Create test users, test content or a second project in an existing app\n  without asking.\n- Send a feature request or any other message to Moderation API without the\n  user's yes.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}