Skip to main content

Fail-closed verify-before-you-act gate for AI agents. One-line adapters for LangChain, LangGraph, CrewAI, and the OpenAI Agents SDK. Signed receipts, pay-per-call via x402.

Project description

verity-guard

A fail-closed verify-before-you-act gate for AI agents — in one line, for the framework you already use.

Your agent is about to wire money, send an email, run rm -rf, or publish something. verity-guard asks an independent service for an allow / review / block second opinion at the exact moment a mistake becomes permanent — and hands back a signed, independently re-verifiable verdict you can prove to an auditor later without trusting anyone.

Not "we signed a receipt that a call happened" (everyone does that now). A re-verifiable verdict — cryptographic proof that an action was independently judged safe, or that a claim was checked and supported. Fail-closed across four functions: guard actions, verify facts, detect prompt-injection, redact PII/secrets.

  • 🔒 Fail-closed — when unsure it escalates to review or block, never a confident wrong allow.
  • 🧾 Ed25519-signed verdicts — every paid result carries a receipt that verifies offline against our published key at /.well-known/verity-pubkey.json, forever, without us. verify_receipt() is the convenient free live check (it POSTs to the issuer — use the key directly if you want an independent one).
  • 🔑 Keyless & non-custodial — this SDK holds no wallet and never pays silently. Paid routes answer HTTP 402; your own x402 layer settles the disclosed USDC micro-payment on Base.
  • 🧩 Native adapters — LangChain, LangGraph, CrewAI, OpenAI Agents SDK. Base install pulls in none of them.

Live now: guard_action from $0.02/call. Full wire-in guide → https://veritylayer.dev/guard

Node / TypeScript? npm i @veritylayer/guard — the same keyless client plus a Vercel AI SDK adapter. Source lives in js/.


Install

pip install verity-guard                 # tiny — just httpx
pip install "verity-guard[x402]"         # + the built-in payer (bring your own wallet key)
pip install "verity-guard[langgraph]"    # + your framework of choice

30-second quickstart

VerityLayer is pay-per-call over x402 — no account, no API key, no subscription. Point the client at a wallet you funded with USDC on Base and you get real verdicts:

import os
from verity_guard import VerityClient, x402_payer

v = VerityClient(http=x402_payer(os.environ["VERITY_WALLET_KEY"]))

res = v.guard(
    "Wire $4,000 USDC to 0x9a3f…c012 (invoice #221)",
    context="Invoice arrived via a scraped web page; address never seen before.",
    policy="No new payees without human review.",
)
print(res.decision, res.risk)          # -> block 0.9
print(res.safer_alternative)           # -> "Halt payment. Cross-verify via known channels…"
print(v.verify_receipt(res.receipt).valid)   # -> True  (free live check; the receipt itself
                                            #          verifies offline against our published key)

Your key never leaves your process. It signs an EIP-3009 authorization locally for the exact amount the challenge discloses — VerityLayer only ever sees the signature. Fund the address wallet_address(os.environ["VERITY_WALLET_KEY"]) with USDC on Base mainnet, and read the key from the environment (a key on a command line leaks into process lists and shell history).

No wallet yet?

VerityClient() with no payer is honest about it rather than pretending — paid calls return a structured payment_required result carrying the live challenge, and no verdict is claimed, because none was purchased:

v = VerityClient()
res = v.guard("Wire $4,000 …")
res.payment_required   # True
res.price              # "$0.02"
res.decision           # None  <- nothing fabricated

Receipt verification is free forever and needs no wallet — check our signatures before you ever pay us:

VerityClient().verify_receipt(some_receipt).valid   # True

To actually pay per call, hand the client an x402-wrapped HTTP client that holds your wallet:

# sync: any x402-wrapped requests.Session / httpx.Client
v = VerityClient(http=my_x402_client)

# async: an x402-wrapped httpx.AsyncClient
from verity_guard import AsyncVerityClient
v = AsyncVerityClient(http=my_async_x402_client)

The SDK never sees your key — it just POSTs; your x402 layer settles the 402 and retries. (See veritylayer.dev/guard for wallet setups.)


Framework adapters

LangGraph — drop-in guarded tool node

Replace ToolNode with GuardedToolNode: every proposed tool call is checked before it runs. Blocked calls never execute — the model gets the block reason + safer alternative and revises.

from verity_guard import VerityClient
from verity_guard.integrations.langgraph import GuardedToolNode

tools = [wire_funds, send_email, search_web]
guarded = GuardedToolNode(tools, VerityClient(http=my_x402_client),
                          policy="No new payees without human review.")
graph.add_node("tools", guarded)     # instead of ToolNode(tools)

OpenAI Agents SDK — per-tool-call guardrail (highest-frequency wire-in)

from verity_guard import VerityClient
from verity_guard.integrations.openai_agents import (
    build_guard_tool, build_output_guardrail, build_tool_input_guardrail,
)
v = VerityClient()

# (a) give the agent a guard tool it can call
agent = Agent(name="Treasurer", tools=[build_guard_tool(v)])

# (b) verify the final answer before it leaves
agent = Agent(..., output_guardrails=[build_output_guardrail(v, mode="guard",
                 policy="No new payees without human review.")])

# (c) guard the arguments of EVERY tool call (newer SDKs)
wire_funds.tool_input_guardrails = [build_tool_input_guardrail(v)]

LangChain — a guard tool, or gate any function

from verity_guard import VerityClient, guard
from verity_guard.integrations.langchain import build_guard_tool

v = VerityClient(http=my_x402_client)
tools = [..., build_guard_tool(v)]           # explicit tool the agent can call

@guard(v, policy="No new payees without human review.")   # or gate a function directly
def wire_funds(to: str, amount: float): ...  # raises BlockedAction if VerityLayer blocks

CrewAI

from verity_guard import VerityClient
from verity_guard.integrations.crewai import build_guard_tool

guard_tool = build_guard_tool(VerityClient(http=my_x402_client),
                              default_policy="No new payees without human review.")
agent = Agent(role="Treasurer", tools=[guard_tool])

The checks

Method What it answers Route (tier quick) From
guard(action, …) Should this action proceed? allow / review / block suite /check/quick $0.02
verify(claim, …) Is this claim true? supported / unsupported / uncertain engine /verify (grounded) $0.25 by default ⚠️
detect_injection(content, …) Is this untrusted text a prompt-injection? suite /sentinel/quick $0.02

⚠️ verify() is the one method that does not default to the $0.02 tier. A bare v.verify(claim) runs the grounded tier — live web citations, $0.25, 12.5× the $0.02 anchored above. It's the right default for "is this claim true" (the cheap tier is ungrounded and won't cite), but you should choose it, not discover it on a bill. Pass tier="quick" for $0.02 or tier="pro" for $0.35. Every price is disclosed in the 402 before anything is signed, and x402_payer's $1.00 cap bounds any single call regardless. | moderate(content, …) | Safe to publish? publish / review / block | suite /sieve/quick | $0.02 | | redact(payload, …) | Any PII/secrets? returns a redacted copy | suite /redact/quick | $0.02 | | verify_receipt(receipt) | Is this signed verdict authentic? | engine /receipt/verify | free |

Every result is a VerityResult (a dict subclass — future fields never get dropped) with helpers: .decision, .risk, .allowed, .blocked, .flagged, .reasons, .safer_alternative, .receipt, .price, .payment_required.

Verify a receipt without trusting us

Every paid verdict ships an Ed25519-signed receipt. VerityClient.verify_receipt() asks our server whether our own signature is good -- convenient, but that is the issuer vouching for itself. You do not have to accept that.

# The two verifiers are repo files, NOT part of the installed wheel -- grab either one:
curl -O https://raw.githubusercontent.com/meloliva14/verity-guard/main/verify_receipt.py
curl -O https://raw.githubusercontent.com/meloliva14/verity-guard/main/verify_receipt.js

curl -o receipt.json https://api.veritylayer.dev/receipt/selftest
curl -o pubkey.json  https://api.veritylayer.dev/.well-known/verity-pubkey.json

python verify_receipt.py receipt.json pubkey.json     # stdlib + cryptography
node   verify_receipt.js receipt.json pubkey.json     # zero dependencies

Both scripts import nothing from VerityLayer and make no network call at verification time. They deliberately do not share code with the service that signs -- if the two agreed because they were the same code, that would prove nothing.

They ship in the sdist and live in the repo, but they are not in the wheel that pip install resolves to, because the wheel is scoped to the package directory. That is why the commands above fetch them rather than assuming you have them. verify_receipt_offline() below is installed.

In-process, if you already use this package:

from verity_guard import verify_receipt_offline
ok, why = verify_receipt_offline(receipt, public_key_hex, claim="the claim you care about")

Install with pip install "verity-guard[offline]" -- cryptography is optional because most users only want the guard. A missing verifier raises; it is never reported as an invalid signature, because "I could not check" and "this is forged" must never render the same.

A valid signature is not permission. It proves the content was signed and is unaltered. It says nothing about whether the receipt vouches for the action you are about to take -- a real receipt over some other claim verifies perfectly. Pass claim= to check that binding, or send {"receipt": ..., "claim": ...} to POST /receipt/verify and read the bound field.

The canonicalization is specified in RECEIPTS.md, including the two places it deliberately diverges from RFC 8785. Those are named rather than left for you to hit.

Make the receipt load-bearing

Checking a signature and calling that "verified" is the mistake this exists to stop. A valid signature says we signed this. It does not say the receipt permits what you are about to do. Every one of these is a real signature that must not open the gate:

A genuine receipt that... why it must still be refused
is about a different claim you verified the wrong thing
says unsupported we checked, and it was false
says uncertain we checked, and we do not know
was issued six weeks ago true then; the world moved
is a test: true self-test proves signing works, vouches for nothing
already authorized another action replayed
from verity_guard import require_receipt, ReceiptRejected

try:
    require_receipt(receipt, PUBKEY, claim="Invoice 4417 is unpaid")
except ReceiptRejected as e:
    log.error("not authorized: %s", e.reason)   # machine-readable code, not English
    raise

send_dunning_email()   # unreachable unless the receipt vouches for exactly that claim

claim is required and has no default — there is deliberately no keyword-free path to a signature-only check, because that is the failure mode. Use verify_receipt_offline when a signature really is all you want to know.

Defaults are the strict ones: only supported passes, receipts older than 15 minutes are stale, and self-test receipts are refused. Widen them explicitly — accept=(...), max_age_seconds=None, allow_test=True — so loosening a gate is always something you can find in a diff. accept is an allowlist, so a verdict nobody anticipated is refused rather than tolerated.

Pass any mutable set as seen to make receipts single-use. It is recorded only on success, so a receipt refused for being stale is not silently burned:

require_receipt(r, PUBKEY, claim=c, seen=self.spent)   # second call raises [replayed]

check_receipt(...) is the same logic returning (ok, reason, detail) if you would rather branch than catch. Both still raise when cryptography is missing — "I could not check" and "I checked, and no" must never arrive in the same shape.

Doctrine

Fail-closed (uncertainty → the safe verdict, never a confident wrong one) · evidence is never invented · allow/review/block are priced identically (no block-to-bill) · pricing is disclosed and paid per use via x402 · VerityLayer holds no key and never charges silently.

MIT · veritylayer.dev · wire-in guide

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

verity_guard-0.4.0.tar.gz (134.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

verity_guard-0.4.0-py3-none-any.whl (35.7 kB view details)

Uploaded Python 3

File details

Details for the file verity_guard-0.4.0.tar.gz.

File metadata

  • Download URL: verity_guard-0.4.0.tar.gz
  • Upload date:
  • Size: 134.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for verity_guard-0.4.0.tar.gz
Algorithm Hash digest
SHA256 96ba616443793025924732945a5c2ca129953da84430cf0befb83010ef11f5c8
MD5 46f3b65b800da028cd0744bb6576a9b8
BLAKE2b-256 6a040b5004b0561f0c4b5405b58670c896a06edc69bad70cfe64a7605bf94453

See more details on using hashes here.

File details

Details for the file verity_guard-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: verity_guard-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 35.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for verity_guard-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 798237734a486473441db69792b5fc85483f2cab9cb02d884c9f633b897d3f2a
MD5 bf6cd0987a6212eedb671d1b231b84ac
BLAKE2b-256 5ea37bfe70202200e690d419f1245093808c009681f34c7ef570be834e8e67b3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page