# The Do-Not-Call List for Free AI Tiers: A Request-Level Policy You Can Version

> Source: <https://dev.to/hackjs_8688/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version-5cf>
> Published: 2026-10-17 05:56:34+00:00

A weekly dependency bot opens a pull request against a release branch. A bot posts an AI-written summary of the diff into the PR thread. Nobody reviews that summary before merge. That is the whole incident, and it needed no attacker.

This is a composite of ordinary failure modes, not a customer report. It happens because teams gate at the pipeline level. The risky call is usually a single request inside a green pipeline.

This article is an anti-use field guide. It teaches where a free AI lane must not be the caller, and what to call instead.

A paid tier and a free tier can run the same prompt. The difference is who owns the failure. Free access usually means fewer contractual guarantees, shared capacity, and terms you did not negotiate.

So the question is never "is the model good enough". The question is "what is the blast radius when this specific call is wrong". Classify the request, then pick the lane.

Ask these for every request, not every service.

A request that is public, reversible, and tool-free is a fine candidate for a free lane. A request that is internal, irreversible, and shell-capable is not.

Policies in prose drift. Policies in a file can be hashed, diffed, and pinned per release.

Start with a small JSON policy. Keep it boring on purpose.

```
{
  "version": 3,
  "hard_deny": {
    "data_class": ["secret", "pii"],
    "sink": ["signed_artifact", "prod_config", "money"]
  },
  "sandbox_only": {
    "data_class": ["internal"],
    "agent_tools": ["shell", "network"]
  },
  "allow": {
    "task": ["summarize", "rename_suggestion", "changelog_draft"]
  }
}
```

Note the shape. Deny rules are absolute. Sandbox rules are advisory and require human review. The allow list is explicit, so unknown tasks fall to review instead of shipping.

This runs before the model call. It is stdlib-only and short enough to read in a review.

``` python
# classify.py — Python 3.11+, no third-party deps
from __future__ import annotations

import hashlib
import json
import sys
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

class Decision(str, Enum):
    ALLOW = "ALLOW"
    SANDBOX = "SANDBOX"
    DENY = "DENY"

@dataclass(frozen=True)
class Request:
    task: str
    data_class: str          # public | internal | pii | secret
    sink: str                # draft | comment | signed_artifact | prod_config | money
    reversible: bool
    human_in_loop: bool
    agent_tools: tuple[str, ...] = ()

def load_policy(path: Path) -> tuple[dict, str]:
    raw = path.read_bytes()
    return json.loads(raw), hashlib.sha256(raw).hexdigest()[:12]

def classify(req: Request, policy: dict) -> tuple[Decision, list[str]]:
    reasons: list[str] = []

    for name, blocked in policy["hard_deny"].items():
        if getattr(req, name) in blocked:
            reasons.append(f"hard_deny:{name}={getattr(req, name)}")
    if reasons:
        return Decision.DENY, reasons

    for name, gated in policy["sandbox_only"].items():
        value = getattr(req, name)
        hit = any(v in value for v in gated) if isinstance(value, tuple) else value in gated
        if hit:
            reasons.append(f"sandbox_only:{name}={value}")

    if req.task not in policy["allow"]["task"]:
        reasons.append(f"task_not_allowlisted:{req.task}")

    if not req.reversible and not req.human_in_loop:
        reasons.append("irreversible_without_review")

    return (Decision.SANDBOX if reasons else Decision.ALLOW), reasons or ["clean"]
```

The policy hash is the important line. Every decision record carries the exact policy version that produced it.

The log is JSONL. One line per request. It is the artifact you will actually use during an incident.

``` python
def emit(req: Request, decision: Decision, reasons: list[str], policy_hash: str, out=sys.stdout) -> None:
    record = {
        "policy": policy_hash,
        "decision": decision.value,
        "reasons": reasons,
        **{k: list(v) if isinstance(v, tuple) else v for k, v in req.__dict__.items()},
    }
    out.write(json.dumps(record) + "\n")
```

Run it against a batch and read the distribution before you trust it.

```
python classify.py policy.json < requests.jsonl > decisions.jsonl
jq -r 'select(.decision != "ALLOW") | .reasons[]' decisions.jsonl \
  | sort | uniq -c | sort -rn | head
```

That second command is the debugging workflow. It tells you which rule fires most. If `task_not_allowlisted` dominates, your policy is too narrow. If `hard_deny:sink=signed_artifact` fires often, you have a design problem upstream.

A classifier without tests is a comment. Parametrize the matrix and run it in CI.

``` python
# test_classify.py
import pytest
from classify import Decision, Request, classify

POLICY = {
    "version": 3,
    "hard_deny": {"data_class": ["secret", "pii"],
                  "sink": ["signed_artifact", "prod_config", "money"]},
    "sandbox_only": {"data_class": ["internal"], "agent_tools": ["shell", "network"]},
    "allow": {"task": ["summarize", "rename_suggestion", "changelog_draft"]},
}

CASES = [
    (Request("summarize", "public", "draft", True, False), Decision.ALLOW),
    (Request("summarize", "pii", "draft", True, True), Decision.DENY),
    (Request("summarize", "public", "prod_config", True, True), Decision.DENY),
    (Request("summarize", "internal", "comment", True, True), Decision.SANDBOX),
    (Request("deploy", "public", "draft", False, False, ("shell",)), Decision.SANDBOX),
]

@pytest.mark.parametrize("req,expected", CASES)
def test_matrix(req, expected):
    assert classify(req, POLICY)[0] is expected
```

The fifth case is the one that saves you. A capable agent on an unlisted task lands in review, not in production.

Most free-lane failures have a cheaper replacement. Reach for that first.

| Red flag | Why the free lane is wrong | Better alternative | 
|---|---|---|
| Output signs or approves anything | No accountability trail on a shared tier | Deterministic signer, human approver | 
| Input carries PII or secrets | Terms and retention are not yours | Redaction step, then classify again | 
| Caller holds shell or network tools | Irreversible side effects, no rollback | Sandbox with no credentials | 
| Same prompt runs on every commit | Cost and drift scale silently | Cached fixture, golden-file test | 
| Output lands in a config or migration | One bad token is an outage | Schema validation, then review | 
| Task is formatting or renaming | Deterministic problem, probabilistic tool | Formatter, AST rewrite, linter | 
| Latency sits on a user request path | Shared capacity, unknown queueing | Precompute, or a committed tier | 

Print that table. Tape it to the review checklist. The first three rows are hard stops.

Write these before adoption, not after the first incident. Trigger any one and move the workload.

Criterion five is the quiet one. A sandbox nobody reads is a production lane with extra steps.

MonkeyCode ships as an open-source project with a free model allowance and a free server option. Those are operator-stated availability claims.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Treat both as shared capacity, not as entitlements. Quotas, limits, and terms change, so read the project's current documentation instead of trusting any blog post, including this one.

In this policy, that lane maps to `task in allow`, `data_class == public`, `sink == draft`, and `agent_tools == ()`. Summarize a public doc. Draft a changelog entry a human will edit. Suggest a test name. Keep the classifier in front of it, and the free server stays a staging surface rather than a signing surface.

If you want to test the workflow, start with the summarizer task. Leave the deploy bot out of it.

This classifier is a policy check, not an enforcement boundary. A caller can pass wrong labels. Pair it with egress rules, scoped credentials, and separate identities per lane.

Skip this approach in three cases:

The pattern is small on purpose. Copy the two files, replace the allow list with your real tasks, and run the test matrix once. If more than half the cases land in `DENY`, your pipeline shape is the problem, and no tier will fix it.
