> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kettio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent loop

> Combine the MCP server and the REST API into a generate → score → decide cycle.

There are two ways to reach the same scoring pipeline, and the right one depends on where your
agent runs.

<CardGroup cols={2}>
  <Card title="MCP — rank_assets" icon="plug" href="/quickstart/agent-setup">
    Takes local file paths and uploads them for you. Best for repo-local and interactive work.
  </Card>

  <Card title="REST — POST /api/v1/rank" icon="server" href="/guides/rank-api">
    Takes fetchable URLs. Best for volume, scheduled jobs, and services with no client attached.
  </Card>
</CardGroup>

<Note>
  Both paths hit the same model. The difference is transport and where the files live — MCP
  handles the upload step, REST expects you to have already published the assets.
</Note>

## The cycle

```mermaid theme={null}
flowchart TD
    A[Collect or edit creatives] --> B{Where does the agent run?}
    B -->|In a repo, via MCP| C[rank_assets<br/>local paths]
    B -->|As a service| D[Publish to https:// URLs]
    D --> E[POST /api/v1/rank]
    C --> F{Read the result}
    E --> F
    F -->|copy lift low| G[Edit copy, re-rank]
    F -->|gap under 0.30| H[POST /api/v1/pairwise]
    F -->|clear winner| I[Launch]
    G --> A
    H --> F
```

<Steps>
  <Step title="Collect the creatives">
    Gather the files you want scored. The agent works from its own filesystem — Kettio's tools
    take local paths directly. See the [tool reference](/mcp/tools).
  </Step>

  <Step title="Rank them">
    Via MCP, call `rank_assets` with local paths. Via REST, publish to durable `https://` URLs —
    or inline a `data:image/` payload for small images — then call `POST /api/v1/rank`. Up to 20
    assets either way.
  </Step>

  <Step title="Decide from the response">
    Use the ordering, `copy_lift` to see whether the words are earning their place,
    `panel_outcome` to see where refinement changed the order, and
    `summary.scoring_evaluations` to track spend.
  </Step>
</Steps>

## Decision rules worth encoding

These are the checks worth writing into the agent rather than leaving to a model's judgment:

<AccordionGroup>
  <Accordion title="Never act on a batch with silent failures" icon="triangle-exclamation">
    `summary.assets_failed > 0` means the ordering you are reading is incomplete. Either re-run
    the failed assets or mark the result provisional.

    ```javascript theme={null}
    if (summary.assets_failed > 0) throw new Error("incomplete batch");
    ```
  </Accordion>

  <Accordion title="Treat near-ties as ties" icon="scale-balanced">
    A small gap between rank 1 and rank 2 is not a decision. Either escalate to
    [`/api/v1/pairwise`](/guides/pairwise-api) or report both. See
    [Reading confidence](/concepts/confidence).
  </Accordion>

  <Accordion title="Budget in evaluations, not assets" icon="gauge">
    An asset with `copy_context` can cost twice what a bare image costs. An agent that budgets
    by asset count will hit `429` earlier than it expects. See
    [Limits and billing](/concepts/limits-and-billing).
  </Accordion>

  <Accordion title="Keep the audience fixed across a comparison" icon="users">
    Scores are relative within a batch and conditioned on the audience. Changing the audience
    between runs and comparing the numbers is not a valid comparison.
  </Accordion>
</AccordionGroup>

## Rate limit backoff

The Rank API allows 60 scoring evaluations per minute per key and returns `Retry-After` on
`429`. Honour it rather than retrying on a fixed interval:

```javascript theme={null}
async function rankWithBackoff(body, attempt = 0) {
  const response = await fetch("https://kettio.com/api/v1/rank", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.KETTIO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfterSeconds = Number(response.headers.get("Retry-After") ?? 5);
    await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000));
    return rankWithBackoff(body, attempt + 1);
  }

  return response.json();
}
```

## Related

<CardGroup cols={2}>
  <Card title="MCP installation" icon="plug" href="/mcp/installation">
    Connect Claude Desktop, Codex, or any stdio MCP client.
  </Card>

  <Card title="Rank API" icon="sliders" href="/guides/rank-api">
    Goals, asset types, copy context, and refinement behavior.
  </Card>
</CardGroup>
