> ## Documentation Index
> Fetch the complete documentation index at: https://contract-auditor.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# API keys and secrets

> Where each credential comes from, how to set it in GitHub Actions, and what turns on when you do.

The generated workflow already references every secret this action can use. Each
one is skipped silently when unset, so the file runs exactly as `init` wrote it
and nothing here is required. Adding a secret turns a feature on without editing
any YAML.

<Note>
  The audit itself needs no credential at all. The deterministic layer costs
  nothing, calls nothing, and finds most drift. Everything on this page is
  optional.
</Note>

## Setting a secret

<Steps>
  <Step title="Open the repository's secrets">
    **Settings → Secrets and variables → Actions → New repository secret.**
    The name must match exactly, including case.
  </Step>

  <Step title="Paste the value, not a file path">
    GitHub stores the value encrypted and masks it in logs. It cannot be read
    back afterwards, only overwritten.
  </Step>

  <Step title="Re-run the workflow">
    Secrets are read at job start. A run already in progress will not pick up a
    secret added while it is running.
  </Step>
</Steps>

Or from the CLI, which is quicker when you are setting several:

```bash theme={null}
gh secret set OPENROUTER_API_KEY --body "sk-or-v1-..."
gh secret set CONTRACT_AUDIT_SLACK_WEBHOOK_URL < webhook.txt   # from a file
gh secret list                                                  # names only
```

An **organisation** secret (Settings → Secrets and variables → Actions at the
org level) covers every repository you grant it to, which is the right choice
for a model key shared across services. An **environment** secret is scoped to a
deployment environment and needs `environment:` on the job; the generated
workflow does not use one.

## What each secret turns on

| Secret                                              | Turns on                                                                     | Cost                |
| --------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------- |
| `OPENROUTER_API_KEY`                                | the judgment pass: drift that needs reading comprehension rather than lookup | per call, see below |
| `CONTRACT_AUDIT_SLACK_WEBHOOK_URL`                  | Slack alerts, counts and severities only                                     | free                |
| `CONTRACT_AUDIT_TELEGRAM_BOT_TOKEN` + `..._CHAT_ID` | Telegram alerts                                                              | free                |
| `CONTRACT_AUDIT_WEBHOOK_URL` + `..._SECRET`         | the full JSON report POSTed to a sink of your own                            | free                |
| `CONTRACT_AUDIT_MEMORY_URL` + credentials           | self-improvement, and with it the verification gate                          | storage only        |

None of them is needed for the audit to run and comment on a pull request.

***

## OPENROUTER\_API\_KEY

Turns on the stage that reads code and decides whether it matches a sentence in
your documentation. Without it the run is free and still catches most drift.

<Steps>
  <Step title="Create the key">
    Sign in at [openrouter.ai/keys](https://openrouter.ai/keys) and create a
    key. It begins `sk-or-v1-`.
  </Step>

  <Step title="Add credit">
    OpenRouter is prepaid. The evaluation in this repository — 16 cases, 159
    model calls — cost **\$0.076**, so a few dollars lasts a long time.
  </Step>

  <Step title="Set it">
    `gh secret set OPENROUTER_API_KEY`
  </Step>
</Steps>

### Using a different provider

The client sends whatever model id it is given to whatever endpoint it is
pointed at, so any OpenAI-compatible API works. Set `base-url` alongside the
key:

```yaml theme={null}
          api-key: ${{ secrets.OPENAI_API_KEY }}
          base-url: https://api.openai.com/v1
          model: gpt-4o-mini
```

The default is `https://openrouter.ai/api/v1` with `z-ai/glm-5.3-flash`. Azure
OpenAI, Together, Groq, Fireworks and a self-hosted vLLM all speak the same
protocol. The secret's *name* is yours to choose — only the input matters.

### Keeping the bill small

`workers` controls parallelism, not spend. What controls spend is how many
endpoints the model is asked about, and the deterministic layer already settles
most of them before the model is called. Two further levers:

* `contract-middleware` removes routes outside the published contract from the
  audit entirely, so no model call is ever made for them.
* `exclude-paths` does the same for paths you name.

***

## Slack

Sends a short message with counts and severities. **Never endpoint paths** — a
channel holds people who cannot read the code.

<Steps>
  <Step title="Create an app">
    [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** →
    From scratch. Name it, pick the workspace.
  </Step>

  <Step title="Enable incoming webhooks">
    **Incoming Webhooks** → toggle on → **Add New Webhook to Workspace** →
    choose the channel.
  </Step>

  <Step title="Copy the URL">
    It looks like `https://hooks.slack.com/services/T…/B…/…`. This URL *is* the
    credential; anyone holding it can post to that channel.
  </Step>

  <Step title="Set it">
    `gh secret set CONTRACT_AUDIT_SLACK_WEBHOOK_URL`
  </Step>
</Steps>

## Telegram

<Steps>
  <Step title="Create the bot">
    Message [@BotFather](https://t.me/BotFather), send `/newbot`, follow the
    prompts. He replies with a token like `123456789:AA…`.
  </Step>

  <Step title="Find the chat id">
    Add the bot to the group (or message it directly), send it any message,
    then:

    ```bash theme={null}
    curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates" \
      | grep -o '"chat":{"id":[-0-9]*'
    ```

    A group id is negative, e.g. `-1001234567890`. A direct-message id is
    positive.
  </Step>

  <Step title="Set both">
    ```bash theme={null}
    gh secret set CONTRACT_AUDIT_TELEGRAM_BOT_TOKEN
    gh secret set CONTRACT_AUDIT_TELEGRAM_CHAT_ID
    ```

    Neither works without the other.
  </Step>
</Steps>

<Note>
  Both channels are gated on verification and on `notify-min-severity`, which
  defaults to `high`. A clean run stays silent rather than posting "nothing to
  report" every morning.
</Note>

## Your own sink

`CONTRACT_AUDIT_WEBHOOK_URL` receives the full JSON report as a POST — the
whole findings array, not a formatted message. Intended for a database or a
dashboard rather than a chat channel.

`CONTRACT_AUDIT_WEBHOOK_SECRET` is sent with it as the `X-Auditor-Token` header
so your receiver can tell a real delivery from anyone who guessed the URL:

```python theme={null}
if request.headers.get("X-Auditor-Token") != os.environ["AUDITOR_TOKEN"]:
    return Response(status=401)
```

Set the secret even if the URL is unguessable. An endpoint that accepts
unauthenticated POSTs is one someone will eventually find.

***

## Memory, for self-improvement

The ledger of which claims survived their test. It lives wherever your
credentials point and nowhere else: nothing is stored in your repository,
nothing ships inside the image, and no history is shared between projects.

Five backends, chosen by the URL scheme:

| `memory-url`                   | Store                                                     | Also needs                                        |
| ------------------------------ | --------------------------------------------------------- | ------------------------------------------------- |
| `s3://bucket/prefix`           | AWS, Cloudflare R2, MinIO, Spaces, Backblaze B2           | `memory-key-id`, `memory-secret`, `memory-region` |
| `https://host/ledger.jsonl`    | anything accepting GET and PUT, including a presigned URL | `memory-token` (sent as a bearer token)           |
| `cloudinary://cloud/public_id` | Cloudinary raw storage                                    | `memory-key-id`, `memory-secret`                  |
| `ipfs://name`                  | a Pinata-compatible pinning service                       | `memory-token`                                    |
| `file:///abs/path`             | local file                                                | — (development only)                              |

For a non-AWS S3 store, give the endpoint in the URL:

```
s3://my-bucket/contract-auditor?endpoint=https://<account>.r2.cloudflarestorage.com
```

### Cloudflare R2, end to end

<Steps>
  <Step title="Create the bucket">
    Cloudflare dashboard → **R2** → Create bucket. Note the account id from the
    URL.
  </Step>

  <Step title="Create an API token">
    **R2 → Manage R2 API Tokens → Create API token**, permission **Object Read
    & Write**, scoped to that bucket. Copy the Access Key ID and Secret Access
    Key — the secret is shown once.
  </Step>

  <Step title="Set the secrets">
    ```bash theme={null}
    gh secret set CONTRACT_AUDIT_MEMORY_URL \
      --body 's3://my-bucket/contract-auditor?endpoint=https://<account>.r2.cloudflarestorage.com'
    gh secret set CONTRACT_AUDIT_MEMORY_KEY_ID
    gh secret set CONTRACT_AUDIT_MEMORY_SECRET
    gh secret set CONTRACT_AUDIT_MEMORY_REGION --body auto
    ```
  </Step>

  <Step title="Prove it round-trips">
    `make memory-check` writes a row and reads it back, so a misconfigured
    store fails here rather than silently keeping no memory.
  </Step>
</Steps>

<Warning>
  Turning memory on also turns on the verification gate for your repository,
  because a claim with no verdict teaches nothing. Expect runs to take longer:
  the gate writes a test beside your code, runs it, and deletes it.
</Warning>

***

## GITHUB\_TOKEN

You do not create this one. Actions mints it per run, and the generated workflow
declares what it needs:

```yaml theme={null}
permissions:
  contents: read          # check out the code
  pull-requests: write    # post the summary comment
```

If the comment step fails with `Resource not accessible by integration`, the
repository default is capping the token: **Settings → Actions → General →
Workflow permissions → Read and write**. A workflow cannot grant itself more
than the repository allows.

***

## Forks

This catches people out, and it is worth knowing before you test on one.

**A fork does not inherit the upstream repository's secrets.** If you forked a
project to try this out, every secret above has to be set again on your fork.
`gh secret list` on the fork shows what it actually has, which is nothing until
you add it.

**A `pull_request` run from a fork gets no secrets at all**, even secrets the
base repository holds. This is a GitHub security boundary, not a setting: it
stops a stranger's pull request from exfiltrating your keys. So on a pull
request from a fork the audit runs its deterministic layer, posts its comment,
and makes no model calls — which is the correct behaviour, and worth expecting
rather than debugging.

Pull requests **within** one repository, and `push` runs, have the secrets.

<Note>
  Workflows are disabled on a new fork. Open the **Actions** tab and choose
  "I understand my workflows, go ahead and enable them" before anything runs.
</Note>

## Checking what is set

```bash theme={null}
gh secret list                      # names and update times, never values
gh run view <run-id> --log | grep -i "no api-key"
```

The run log states plainly which optional features were skipped, so a secret
that was set under the wrong name shows up as the feature quietly not running:

```
no api-key supplied, running the deterministic layer only (no model calls)
memory     off (no memory-url; set one to turn on self-improvement)
```

## Rotating and revoking

Secrets cannot be read back, so rotation is always "set the new value over the
old one". Overwrite with `gh secret set <NAME>`, then revoke the old credential
at the provider — in that order, so no run lands in the gap.

If a key leaks, revoke it at the provider first. Removing the GitHub secret
stops this workflow using it but does nothing about the key itself.
