<!-- https://cutedyno.com/docs/guides/agent-governance -->

# 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.

```typescript
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](/docs/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.

```typescript
const { secret } = await cutedyno.apiKeys.create({
  name: 'Content agent',
  scope: 'agent',
  requireApproval: true,
  maxPostsPerDay: 10,
});
```

Now every publish from that key stops for review:

```typescript
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`](/docs/api/update-policy) when the requirement belongs to the customer rather than to one integration:

```typescript
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](/docs/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.

```typescript
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

```typescript
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

```typescript
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:

```typescript
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, apiKeyId
```

`events` 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](/dashboard/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:

```typescript
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](/docs/mcp) 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](/docs/authentication) — scopes and key management in full
- [MCP server](/docs/mcp) — connect the agent
- [Webhooks](/docs/webhooks) — approval events
- [Errors](/docs/errors) — `account_not_allowed`, `daily_post_limit`, `platform_not_allowed`, `approval_required`
