Agent governance
Give an AI agent posting access without giving it the keys to your brand.
An agent that can publish to your company's social accounts is one bad prompt away from an incident, and unlike a human it will not hesitate before hitting send. CuteDyno's answer is not to trust the model. It is to bound what any key can do, in the API, on the server, where the agent cannot argue with it.
Six controls, which compose:
| Control | Set on | Effect |
|---|---|---|
| Scope | Key | Which operations the key may call at all |
requireApproval | Key or profile | Posts wait for a human before reaching a platform |
allowedAccountIds | Key | Only these connected accounts may be posted to |
maxPostsPerDay | Key or profile | Hard cap per UTC day |
allowedPlatforms | Profile | Only these platforms may be posted to |
expiresIn | Key | The key stops working on its own |
Start with scope
The cheapest control is the one that removes the capability entirely.
const { secret } = await cutedyno.apiKeys.create({
name: 'Reporting agent',
scope: 'readonly',
});A readonly key can answer "how did last week's posts do?" and cannot publish, connect an account, or change a webhook. If that is all your agent needs, stop here — no other control matters, because there is nothing to govern.
agent sits between readonly and full: it can draft, schedule, and publish, but it cannot manage keys, profiles, or webhooks. Use it for anything that writes. Reserve full for your own backend.
See Authentication for the complete scope table.
Require approval
The control that matters most. With requireApproval, a post created by the agent does not publish — it goes to pending_approval and an email goes to a human with an approve/reject link.
const { secret } = await cutedyno.apiKeys.create({
name: 'Content agent',
scope: 'agent',
requireApproval: true,
maxPostsPerDay: 10,
});Now every publish from that key stops for review:
const { post } = await cutedyno.posts.create({
content: 'Introducing our autumn range.',
accountIds,
publishNow: true,
});
post.status; // 'pending_approval'
post.approvers; // ['you@yourcompany.com']Note what publishNow: true did: nothing. The flag is a request, and the policy overrides it. This is the whole point — the agent cannot opt out of review by choosing different parameters, and it does not need to know that review exists.
Approval routes to whoever you nominate in approvers, and to the human who created the key when you nominate nobody. An agent-scope key cannot nominate its own approver at all; review always goes to the key's owner. Otherwise an agent could name an address it controls and approve its own work, which is not review.
Set the same thing on a profile with PUT /v1/policy when the requirement belongs to the customer rather than to one integration:
await cutedyno.policy.update({
profileId: 'prof_customer_42',
requireApproval: true,
});Policy and key are ORed. Either one demanding review is enough, so a customer can tighten their own profile without your cooperation, and you can tighten one agent without touching the customer.
What the human sees
An email with the post text, the target accounts, and two links. Approving releases the post to its original schedule; rejecting leaves it unpublished with the reviewer's comment attached to the post. Everyone nominated must approve before it publishes.
Editing a post that is already approved sends it back for review. Approval attaches to the content, not to the post ID.
Watch the approval.requested, approval.approved, and approval.rejected webhooks to reflect the state in your own UI.
Restrict the accounts
An agent that manages one product's Instagram has no business posting to the corporate LinkedIn.
await cutedyno.apiKeys.create({
name: 'Product IG agent',
scope: 'agent',
allowedAccountIds: ['3f2a…'],
});Any post naming an account outside the list is rejected with account_not_allowed before anything is queued. Either an account's id or its platformAccountId satisfies the list, so you can write it with whichever ID you already store.
Profile-level allowedPlatforms does the same thing one level up — useful when a customer is only paying for LinkedIn, and you would rather the answer to "can I post this to TikTok?" be platform_not_allowed than a support conversation.
Cap the volume
await cutedyno.apiKeys.create({
name: 'Content agent',
scope: 'agent',
maxPostsPerDay: 5,
});A loop that decides to post every 30 seconds hits daily_post_limit on the sixth attempt rather than filling a feed with 200 posts. The count is posts created by that key, resetting at midnight UTC.
This is a blast-radius control, not a rate limit. Set it near the real ceiling of what the agent should ever do in a day — if a well-behaved agent posts three times a day, five is a reasonable cap and fifty is not.
Expire the key
await cutedyno.apiKeys.create({
name: 'Campaign agent (Q3)',
scope: 'agent',
requireApproval: true,
expiresIn: 90,
});Keys that outlive the project they were made for are how credentials leak. An expiring key means a forgotten integration fails closed with api_key_expired instead of quietly retaining access for years.
Watch what happened
Governance without an audit trail is a policy nobody can check. Two logs, both filterable by key:
const { events } = await cutedyno.events.list({ limit: 50 });
// actorType: 'api_key' | 'user' | 'system'
// action: 'post.created', 'post.publish_requested', 'account.connected', …
const { logs } = await cutedyno.logs.list({ limit: 50 });
// method, path, statusCode, requestId, apiKeyIdevents is the semantic trail — who did what to which resource. logs is the raw HTTP record, useful when you need to know exactly what an agent sent. Both are visible in the dashboard under Activity.
Read these after a surprise. An agent that produced 40 rejected posts in an hour is visible here long before anyone complains about the content.
A worked setup
A content agent in Claude, allowed to draft and schedule for two brand accounts, with everything reviewed and a hard ceiling:
const { secret } = await cutedyno.apiKeys.create({
name: 'Claude content agent',
scope: 'agent',
requireApproval: true,
allowedAccountIds: [instagramId, linkedinId],
maxPostsPerDay: 8,
expiresIn: 180,
});Give secret to the MCP server as CUTEDYNO_API_KEY. The agent can now research, draft, schedule, and report. It cannot publish unreviewed, cannot reach any other account, cannot exceed eight posts a day, cannot change its own permissions, and stops working in six months.
The agent does not need to be told any of this, and cannot talk its way out of it. That is the difference between a policy and a prompt.
Next
- Authentication — scopes and key management in full
- MCP server — connect the agent
- Webhooks — approval events
- Errors —
account_not_allowed,daily_post_limit,platform_not_allowed,approval_required