Your agent just said no to a user. No explanation. No alternative. Just cold words.
The user left. The ticket doubled. The AI team blamed the model.
The real bug was invisible: you had no idea why it refused. Policy? Tone? A hallucinated constraint?
You need a refusal card.
A refusal card is a structured record shown at the exact dead end. It turns a one-line no into a decision point.
Here is the flow:
User request -> Agent decision -> Refusal? -> Generate card -> Validate -> Show card
In this tutorial, I will build one using MonkeyCode. It is an open-source project. Its free tier currently offers 10 million tokens and a free server option. As of August 2026, that is enough for this experiment.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Step 1: Define the card schema
A card must answer five things:
- What did the user ask?
- What did the agent decide?
- Why? With concrete evidence.
- What alternatives exist?
- Can a human override?
Here is a Python schema:
card = {
'request': 'Cancel my subscription',
'action_taken': 'declined',
'reasons': [
{'type': 'policy', 'detail': 'Only admins can cancel', 'evidence': 'user.role == viewer'}
],
'alternatives': ['Ask an admin'],
'human_override_possible': True,
'stop_condition_triggered': True
}
Validation is straightforward:
def validate(card):
errors = []
if 'request' not in card: errors.append('Missing request')
if not card.get('reasons'): errors.append('Need at least one reason')
for reason in card['reasons']:
if 'evidence' not in reason: errors.append('Reason needs evidence')
return errors
Run this:
python -c 'from validate import validate; sample = {}; print(validate(sample))'
Step 2: Generate the card with a free model
MonkeyCode exposes an OpenAI-compatible API. Set your key and base URL as environment variables.
import os, json
from openai import OpenAI
client = OpenAI(
api_key=os.environ['MONKEYCODE_API_KEY'],
base_url=os.environ.get('MONKEYCODE_BASE_URL')
)
def generate_card(user_request, agent_response):
prompt = f'Convert this into a refusal card JSON: {user_request} -- {agent_response}'
completion = client.chat.completions.create(
model=os.environ.get('MONKEYCODE_MODEL', 'free'),
messages=[{'role': 'user', 'content': prompt}],
temperature=0
)
return json.loads(completion.choices[0].message.content)
The model name may differ. Check your dashboard.
Step 3: Test locally
python test_generator.py
My test printed a valid card. Then I ran the validator. It returned no errors.
Step 4: Deploy to a free server
MonkeyCode's free server lets you run a small HTTP service. Wrap the function in Flask:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/refusal_card', methods=['POST'])
def endpoint():
data = request.get_json()
card = generate_card(data['user_request'], data['agent_response'])
return jsonify(card), 200 if not validate(card) else 422
Follow MonkeyCode's deployment guide for the free server. Then test with a small Python script:
import requests
response = requests.post(
'https://your-free-server.monkeycode.example/refusal_card',
json={'user_request': 'Cancel my subscription', 'agent_response': 'Sorry, I cannot cancel.'}
)
print(response.json())
You will get a structured refusal card.
Step 5: Wire it into your agent
Before your agent returns a refusal, call this service.
if should_refuse:
card = generate_card(user_request, response)
show(card)
Now the user sees why, what else to try, and whether a human can override.
Limitations
- Free tier rate limits may block high traffic.
- The model can emit malformed JSON. Add a retry.
- Evidence is only as good as the agent's response. If the agent lies, the card lies.
- Model names and quotas change. Verify them before production.
Who should skip this
If your agent never makes high-stakes decisions, skip it. If you already have deterministic policy reasons, skip it.
But if your agent is a probabilistic black box, start here. The free tokens are enough to test a few hundred cases. That will expose the first lie.
Try it with your own logs. MonkeyCode's free tier is a good playground for this pattern.
Top comments (0)