<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Screwtape.ai]]></title><description><![CDATA[Screwtape.ai]]></description><link>https://screwtape.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 31 Oct 2026 17:30:13 GMT</lastBuildDate><atom:link href="https://screwtape.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[An audit trail for agent payments]]></title><description><![CDATA[When an AI agent approves a payment, your auditor will ask what was checked and who allowed it. What an agent payment audit trail needs, and how to keep…]]></description><link>https://screwtape.hashnode.dev/an-audit-trail-for-agent-payments</link><guid isPermaLink="true">https://screwtape.hashnode.dev/an-audit-trail-for-agent-payments</guid><category><![CDATA[finance]]></category><category><![CDATA[audit]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Accounts payable,]]></category><dc:creator><![CDATA[Screwtape.ai]]></dc:creator><pubDate>Wed, 28 Oct 2026 12:54:56 GMT</pubDate><content:encoded><![CDATA[<p><em>This article was created with the help of AI by the Screwtape.ai team. Product details were checked against screwtape.ai/pricing, /docs and /openapi.json on 2026-10-24.</em></p>
<p>Finance teams already know how to audit a person approving a payment: there is an approver, a timestamp, a policy and a document trail. When an AI agent does the approving, those pieces are often missing. The agent's log says what it did. It rarely shows what was checked, against which rule, and that nobody changed the record later.</p>
<h2>Questions an auditor will ask</h2>
<p>For any agent-approved payment, expect these:</p>
<ol>
<li><strong>What was the agent about to pay?</strong> The invoice, vendor and amount as the agent understood them.</li>
<li><strong>What was checked?</strong> Required fields, totals, purchase order match, duplicate check, policy limits.</li>
<li><strong>What was the result, and when?</strong></li>
<li><strong>Who or what allowed the payment to go out?</strong> And was that approval tied to this exact payment?</li>
<li><strong>Can you show the record hasn't been changed?</strong></li>
</ol>
<p>An agent's own chat log answers the first question at best.</p>
<h2>What a good trail looks like</h2>
<ul>
<li><strong>One record per decision</strong>, written at the time of the check, not reconstructed afterwards.</li>
<li><strong>Written by something other than the agent.</strong> If the agent writes its own audit log, the log is only as trustworthy as the agent.</li>
<li><strong>Tamper-evident.</strong> Each record signed, and linked to the one before, so an edit or a missing entry shows.</li>
<li><strong>Tied to the payment.</strong> An approval should be usable once, for this invoice, this amount and this vendor, and expire quickly.</li>
<li><strong>Exportable.</strong> Your auditor wants a file, not a dashboard login.</li>
<li><strong>Kept long enough</strong> for your retention policy.</li>
</ul>
<h2>How Screwtape.ai does it</h2>
<p>Screwtape.ai decides whether an AI agent is allowed to spend money, and keeps the proof. It is a separate check that runs outside your agent.</p>
<ul>
<li>Every verdict comes with a <strong>signed receipt</strong>. You can verify it yourself with <code>POST /v1/receipts/verify</code> or against the public keys at <code>/.well-known/screwtape-receipt-keys.json</code>.</li>
<li>Receipts can be <strong>exported</strong> as JSONL, CSV or JSON (<code>GET /v1/receipts/export</code>).</li>
<li>On the <strong>Team</strong> plan ($199/mo): 15-minute single-use pass tokens bound to the exact invoice, amount and vendor; linked, tamper-evident audit receipts; policy rules; 1-year receipt retention.</li>
<li>On <strong>Finance Control</strong> ($399/mo): the same, plus up to 5 policy sets or entities, unlimited API keys, 7-year receipt retention and an auditor export bundle.</li>
</ul>
<p>Invoice content is not stored by default; retention is opt-in per key (see screwtape.ai/privacy).</p>
<p>A receipt records what was checked and the result. It does not make the decision right: verdicts come from AI models and automated checks and can be wrong, so keep a person in charge of payments.</p>
<p><strong>Compare the Team and Finance Control plans: <a href="https://screwtape.ai/pricing">screwtape.ai/pricing</a>.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Stop your AI agent paying the same invoice twice]]></title><description><![CDATA[If an AI agent can approve payments, add a duplicate-invoice check before money moves. What to compare, where the check should live, and what to keep as…]]></description><link>https://screwtape.hashnode.dev/stop-your-ai-agent-paying-the-same-invoice-twice</link><guid isPermaLink="true">https://screwtape.hashnode.dev/stop-your-ai-agent-paying-the-same-invoice-twice</guid><category><![CDATA[Accounts payable,]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[automation]]></category><category><![CDATA[finance]]></category><dc:creator><![CDATA[Screwtape.ai]]></dc:creator><pubDate>Wed, 28 Oct 2026 12:53:41 GMT</pubDate><content:encoded><![CDATA[<p><em>This article was created with the help of AI by the Screwtape.ai team. Product details were checked against screwtape.ai/pricing, /docs and /openapi.json on 2026-10-24.</em></p>
<p>Duplicate payments are an old accounts-payable problem. The same invoice arrives twice: once by email and once through the portal, or re-sent with a new number after a reminder. A person pays one, a colleague pays the other.</p>
<p>AI agents don't remove that problem. An agent that reads invoices and queues payments will do exactly what it is told, every time a document arrives. If two copies arrive, it can approve both, quickly and confidently.</p>
<p>The fix is not a better prompt. It is a check that runs <strong>before</strong> the payment goes out, outside the agent, every time.</p>
<h2>What a duplicate check should compare</h2>
<p>A useful duplicate check looks at more than the invoice number, because duplicates rarely look identical:</p>
<ul>
<li><strong>Vendor + invoice number + amount.</strong> The obvious case: the same invoice submitted twice.</li>
<li><strong>Vendor + amount + date within a short window.</strong> Catches a re-sent invoice with a new number, or a typo in the number.</li>
<li><strong>Currency.</strong> 1,000 USD and 1,000 EUR are not the same bill.</li>
</ul>
<p>The result should not be a plain yes or no. "Possible duplicate" is a real answer, and the right response to it is to stop and ask a person, not to guess.</p>
<h2>Where the check should live</h2>
<p>Put it between "the agent decided to pay" and "the payment is released":</p>
<ol>
<li>The agent extracts the invoice and proposes a decision.</li>
<li>A separate check compares it with invoices already seen, and checks the basics: required fields present, line items adding up to the total.</li>
<li>Only a clean result lets the payment continue. A duplicate or possible duplicate goes to a person.</li>
</ol>
<p>Keeping the check outside the agent matters. An agent asked "have you paid this already?" answers from whatever is in its context. A separate service answers from a record.</p>
<h2>What to keep as proof</h2>
<p>When an auditor or a controller asks why a payment went out, "the agent said it was fine" is not an answer. Keep, for every decision:</p>
<ul>
<li>what was checked and the result,</li>
<li>when it was checked,</li>
<li>a record you can show later that has not been edited since.</li>
</ul>
<h2>How Screwtape.ai does it</h2>
<p>Screwtape.ai decides whether an AI agent is allowed to spend money, and keeps the proof. It is a separate check that runs outside your agent.</p>
<ul>
<li><strong>Duplicate invoice check</strong> (<code>POST /v1/duplicate</code>): compares an invoice with the ones your API key recorded earlier, by vendor, invoice number and amount, with an optional same-vendor, same-amount, near-date rule. The answer is <code>unique</code>, <code>duplicate</code>, <code>possible_duplicate</code>, <code>replay</code> or <code>not_checked</code>. Duplicates and possible duplicates escalate to a person. Only keyed fingerprints are stored, and only when you ask it to store them.</li>
<li><strong>Invoice checks</strong>: required fields, line items plus tax equal the total, and fields that must match the purchase order values you pass in.</li>
<li><strong>Signed receipts</strong>: every verdict comes with a receipt you can verify yourself.</li>
</ul>
<p>On the Team and Finance Control plans you also get 15-minute single-use pass tokens bound to the exact invoice, amount and vendor, linked tamper-evident receipts, and policy rules such as amount caps, a vendor allowlist and second approval.</p>
<p>A duplicate check only knows what it has seen, so it works best when every invoice your agent handles goes through it. Verdicts come from AI models and automated checks, and they can be wrong. Keep a person in charge of payments.</p>
<p><strong>Try it free (100 calls a day), then see the team plans: <a href="https://screwtape.ai/pricing">screwtape.ai/pricing</a>.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Stop false consensus in multi-agent swarms]]></title><description><![CDATA[Majority vote is the default way to reconcile a multi-agent run. Three agents answer, two or three agree, and the orchestrator moves on. The trouble is…]]></description><link>https://screwtape.hashnode.dev/stop-false-consensus-in-multi-agent-swarms</link><guid isPermaLink="true">https://screwtape.hashnode.dev/stop-false-consensus-in-multi-agent-swarms</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Screwtape.ai]]></dc:creator><pubDate>Wed, 21 Oct 2026 00:12:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad8024b945ab40ef25df3bf/cefa79a8-7c17-49fc-8eb0-8e274259903c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Majority vote is the default way to reconcile a multi-agent run. Three agents answer, two or three agree, and the orchestrator moves on. The trouble is that agents in the same swarm often share a prompt, a retrieved document, or each other's messages. When they agree, they may just be repeating the same mistake. That's <strong>false consensus</strong>: high agreement with little independent evidence behind it.</p>
<p>This post shows how to check for it with <a href="https://screwtape.ai">Screwtape.ai</a>, a judge-agent API with two endpoints:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>You send</th>
<th>You get</th>
</tr>
</thead>
<tbody><tr>
<td><code>POST /v1/judge</code></td>
<td><code>task</code>, <code>agent_output</code> (required), <code>evidence[]</code>, <code>criteria[]</code>, <code>retain</code> (optional)</td>
<td><code>verdict</code> (<code>pass</code></td>
</tr>
<tr>
<td><code>POST /v1/consensus</code></td>
<td><code>task</code>, <code>answers[]</code> (min 2, each <code>{agent_id?, output}</code>), <code>retain</code> (optional)</td>
<td><code>consensus_answer</code>, <code>agreement_score</code>, <code>false_consensus_risk</code> (<code>low</code></td>
</tr>
</tbody></table>
<p>Auth is <code>Authorization: Bearer &lt;key&gt;</code>. Free keys come from <a href="https://screwtape.ai/signup">screwtape.ai/signup</a> and include 100 calls per UTC day. Without a key, each call can be paid with an x402 <code>PAYMENT-SIGNATURE</code> header: $0.005 USDC on Base (<code>eip155:8453</code>).</p>
<h2>1. The curl</h2>
<p>This is the request from the OpenAPI <code>grounded</code> example, field for field:</p>
<pre><code class="language-bash">curl -s https://screwtape.ai/v1/judge \
  -H "Authorization: Bearer $SCREWTAPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"task":"State the crew size and launch site.","agent_output":"The crew size was 4. The mission launched from Cape Canaveral.","evidence":["The mission launched on 2024-03-01 from Cape Canaveral.","The crew size was 4."],"criteria":["crew size","Cape Canaveral"]}'
</code></pre>
<p><strong>Real response with no key</strong> (I ran this on 2026-10-20 without an <code>Authorization</code> header). The API answers <code>402</code> and describes the x402 payment it accepts:</p>
<pre><code class="language-json">{"error":"payment_required","message":"PAYMENT-SIGNATURE header is required. Send a Bearer API key, or pay per call with x402.","request_id":"97bf7d4e-4930-41eb-88ce-89c9e75dcd65","x402":{"x402Version":2,"error":"PAYMENT-SIGNATURE header is required. Send a Bearer API key, or pay per call with x402.","resource":{"url":"https://screwtape.ai/v1/judge","description":"One Screwtape.ai judge or consensus call","mimeType":"application/json","serviceName":"Screwtape.ai"},"accepts":[{"scheme":"exact","network":"eip155:8453","amount":"5000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","payTo":"0x028Bb9A57B8C27c22f6B39dd00e7eFF1FB60344B","maxTimeoutSeconds":300,"extra":{"name":"USD Coin","version":"2","assetTransferMethod":"eip3009"}}],"extensions":{}}}
</code></pre>
<p><strong>With a key, the</strong> <code>200</code> <strong>response has this shape.</strong> This is the example published on the screwtape.ai homepage for the deterministic engine. It's an example, not a call I ran:</p>
<pre><code class="language-json">{
  "verdict": "pass",
  "score": 0.9,
  "reasons": [
    "2 of 2 claims are grounded in the supplied evidence.",
    "2 of 2 criteria matched the output.",
    "Deterministic string checks only. No model was called."
  ],
  "engine": "deterministic",
  "retention": { "retained": false },
  "request_id": "8d0c2f1e-6b4a-4e3a-9c1d-2f6a0b7e5d44"
}
</code></pre>
<p>When the service has a model key configured, <code>engine</code> is <code>llm</code>, and the default model is <code>openai/gpt-oss-120b</code>. The wording of the reasons will differ.</p>
<h2>2. Catching copied agreement</h2>
<pre><code class="language-bash">curl -s https://screwtape.ai/v1/consensus \
  -H "Authorization: Bearer $SCREWTAPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"task":"State the crew size.","answers":[{"agent_id":"alpha","output":"The crew size was 4."},{"agent_id":"beta","output":"The crew size was 4."},{"agent_id":"gamma","output":"The crew size was 4."}]}'
</code></pre>
<p>Example shape, from the homepage (not executed here):</p>
<pre><code class="language-json">{
  "consensus_answer": "The crew size was 4.",
  "agreement_score": 1,
  "false_consensus_risk": "high",
  "reasons": [
    "Answers are nearly verbatim. High agreement may be copied rather than independent.",
    "Agreement is token overlap, not a model judgment."
  ],
  "request_id": "..."
}
</code></pre>
<p><code>agreement_score: 1</code> together with <code>false_consensus_risk: "high"</code> is the point. A majority vote would accept this answer immediately. Screwtape tells you the agreement looks copied, so you can bring in an independent check before you trust it.</p>
<h2>3. A shared client (stdlib only)</h2>
<p>All three framework examples import this file. Note the explicit <code>User-Agent</code>: the Cloudflare edge in front of screwtape.ai returned <code>403</code> to Python's default <code>Python-urllib/x.y</code> agent when I tested.</p>
<pre><code class="language-python"># screwtape_client.py
"""Minimal Screwtape.ai HTTP client (stdlib only). Spec: https://screwtape.ai/openapi.json"""
import json
import os
import urllib.request

BASE = "https://screwtape.ai"


def _post(path: str, body: dict) -&gt; dict:
    req = urllib.request.Request(
        BASE + path,
        data=json.dumps(body).encode(),
        headers={
            "Authorization": f"Bearer {os.environ['SCREWTAPE_API_KEY']}",
            "Content-Type": "application/json",
            # Cloudflare in front of screwtape.ai returns 403 to the default
            # "Python-urllib/x.y" User-Agent, so send an explicit one.
            "User-Agent": "screwtape-example/1.0",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.load(resp)


def judge(task: str, agent_output: str, evidence=None, criteria=None) -&gt; dict:
    """POST /v1/judge -&gt; {verdict: pass|fail|retry, score, reasons, request_id, ...}"""
    body = {"task": task, "agent_output": agent_output}
    if evidence:
        body["evidence"] = evidence
    if criteria:
        body["criteria"] = criteria
    return _post("/v1/judge", body)


def consensus(task: str, answers: list[dict]) -&gt; dict:
    """POST /v1/consensus. answers: [{"agent_id": "...", "output": "..."}], at least 2.
    -&gt; {consensus_answer, agreement_score, false_consensus_risk: low|medium|high, reasons, request_id}"""
    return _post("/v1/consensus", {"task": task, "answers": answers})
</code></pre>
<h2>4. CrewAI: a task guardrail</h2>
<p>CrewAI tasks accept a <code>guardrail</code> callable that takes a <code>TaskOutput</code> and returns <code>(bool, Any)</code>. If it returns <code>False</code>, the second value goes back to the agent as feedback, and the task retries up to <code>guardrail_max_retries</code> times.</p>
<pre><code class="language-python"># crewai_guardrail.py
# pip install crewai   (tested against crewai 1.15.x)
# Needs SCREWTAPE_API_KEY plus whatever LLM your crew uses (default: OPENAI_API_KEY).
from crewai import Agent, Crew, Task
from crewai.tasks.task_output import TaskOutput

from screwtape_client import judge

EVIDENCE = [
    "The mission launched on 2024-03-01 from Cape Canaveral.",
    "The crew size was 4.",
]


def screwtape_guardrail(output: TaskOutput) -&gt; tuple[bool, str]:
    result = judge(
        task="State the crew size and launch site.",
        agent_output=output.raw,
        evidence=EVIDENCE,
        criteria=["crew size", "Cape Canaveral"],
    )
    if result["verdict"] == "pass":
        return True, output.raw
    # On fail/retry, CrewAI sends this feedback back to the agent and retries.
    return False, "Screwtape verdict: {}. {}".format(result["verdict"], " ".join(result["reasons"]))


writer = Agent(
    role="Mission summarizer",
    goal="Answer only from the supplied facts.",
    backstory="You summarize mission briefs without adding details.",
)

task = Task(
    description="Facts: " + " ".join(EVIDENCE) + " State the crew size and launch site.",
    expected_output="Two short sentences.",
    agent=writer,
    guardrail=screwtape_guardrail,
    guardrail_max_retries=2,
)

if __name__ == "__main__":
    print(Crew(agents=[writer], tasks=[task]).kickoff().raw)
</code></pre>
<h2>5. LangGraph: route on false-consensus risk</h2>
<p>The consensus check is a node, and a conditional edge routes on <code>false_consensus_risk</code>. The agent node is a stub, so this graph runs with only a Screwtape key.</p>
<pre><code class="language-python"># langgraph_consensus.py
# pip install langgraph   (tested against langgraph 1.2.x)
# Needs only SCREWTAPE_API_KEY. The three "agents" are stubs; swap in your LLM calls.
from typing import TypedDict

from langgraph.graph import END, START, StateGraph

from screwtape_client import consensus

TASK = "State the crew size."


class State(TypedDict, total=False):
    answers: list[dict]
    verdict: dict


def agents(state: State) -&gt; State:
    # Replace with real agent calls. Identical answers are what false consensus looks like.
    outs = {"alpha": "The crew size was 4.", "beta": "The crew size was 4.", "gamma": "The crew size was 4."}
    return {"answers": [{"agent_id": k, "output": v} for k, v in outs.items()]}


def score(state: State) -&gt; State:
    return {"verdict": consensus(TASK, state["answers"])}


def route(state: State) -&gt; str:
    return "escalate" if state["verdict"]["false_consensus_risk"] == "high" else "accept"


def accept(state: State) -&gt; State:
    print("accepted:", state["verdict"]["consensus_answer"])
    return {}


def escalate(state: State) -&gt; State:
    print("needs independent check:", state["verdict"].get("reasons"))
    return {}


g = StateGraph(State)
g.add_node("agents", agents)
g.add_node("score", score)
g.add_node("accept", accept)
g.add_node("escalate", escalate)
g.add_edge(START, "agents")
g.add_edge("agents", "score")
g.add_conditional_edges("score", route, {"accept": "accept", "escalate": "escalate"})
g.add_edge("accept", END)
g.add_edge("escalate", END)
app = g.compile()

if __name__ == "__main__":
    app.invoke({})
</code></pre>
<h2>6. AutoGen: use Screwtape's MCP tools</h2>
<p>Screwtape exposes <code>judge</code> and <code>consensus</code> over MCP Streamable HTTP at <code>https://screwtape.ai/mcp</code>, using the same auth and quota as REST. AutoGen's <code>mcp_server_tools</code> turns them into agent tools.</p>
<pre><code class="language-python"># autogen_mcp.py
# pip install "autogen-agentchat" "autogen-ext[mcp,openai]" "mcp&lt;2"
# (tested with autogen 0.7.5; autogen-ext 0.7.5 fails to import with mcp 2.x, hence the pin)
# Needs SCREWTAPE_API_KEY and OPENAI_API_KEY.
import asyncio
import os

from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.tools.mcp import StreamableHttpServerParams, mcp_server_tools


async def main() -&gt; None:
    server = StreamableHttpServerParams(
        url="https://screwtape.ai/mcp",
        headers={"Authorization": f"Bearer {os.environ['SCREWTAPE_API_KEY']}"},
    )
    tools = await mcp_server_tools(server)  # -&gt; judge, consensus
    agent = AssistantAgent(
        name="reviewer",
        model_client=OpenAIChatCompletionClient(model="gpt-4o-mini"),
        tools=tools,
        system_message=(
            "Before you accept another agent's answer, call the judge tool with the task, "
            "the answer as agent_output, and any evidence. Report the verdict and reasons."
        ),
    )
    await Console(agent.run_stream(task=(
        "Task: State the crew size and launch site. "
        "Answer from worker agent: 'The crew size was 5. The mission launched from Vandenberg.' "
        "Evidence: 'The mission launched on 2024-03-01 from Cape Canaveral.', 'The crew size was 4.'"
    )))


if __name__ == "__main__":
    asyncio.run(main())
</code></pre>
<p>You can call <code>tools/list</code> without a key, so discovery works before you sign up. <code>tools/call</code> needs a Bearer key or an x402 payment.</p>
<h2>What it doesn't do</h2>
<ul>
<li><p>It doesn't run locally. It's a hosted API.</p>
</li>
<li><p>In deterministic mode, agreement is token overlap and grounding is claim-vs-evidence token matching. It isn't semantic reasoning. The <code>reasons</code> say so.</p>
</li>
<li><p>Judged content is kept in memory only, unless you send <code>retain: true</code> with an API key. Retained rows expire after 24 hours (<a href="https://screwtape.ai/privacy">privacy</a>).</p>
</li>
</ul>
<h2>Try it</h2>
<p>Get a free key (100 calls/day): <strong><a href="https://screwtape.ai/signup">https://screwtape.ai/signup</a></strong>. Then read the <a href="https://screwtape.ai/docs">API reference</a> or the raw <a href="https://screwtape.ai/openapi.json">OpenAPI</a>.</p>
<p>This article was created with the help of AI by the Screwtape.ai team.</p>
]]></content:encoded></item></channel></rss>