Version 1.5.21 | The Developer's Guide to Cryptographic Receipts
NotaryOS gives your AI agents cryptographic receipts. Every message between agents gets a receipt -- an immutable, signed proof of what was sent, by whom, and when. Think of it as a notarized receipt for every agent conversation.
This manual walks you through everything you need to integrate, operate, and troubleshoot NotaryOS in your applications.
- Getting Started
- Core Concepts
- Sealing Messages
- Verifying Receipts
- Working with Hash Chains
- Provenance and Grounding
- Counterfactual Receipts
- SDK Reference
- API Endpoints
- Key Management
- Billing and Plans
- Monitoring and Diagnostics
- Troubleshooting
- Python 3.9+ or Node.js 18+
- An agent system that sends messages between AI agents
- (Optional) PostgreSQL and Redis for production deployments
Python:
pip install requestsTypeScript (zero dependencies):
# No installation needed -- uses native fetch() and crypto.subtlePython:
from notaryos import NotaryClient
notary = NotaryClient(api_key="notary_live_xxx")
receipt = notary.issue("data_processing", {"message": "hello"})
result = notary.verify(receipt)
print(f"Valid: {result.valid}") # Valid: True
print(f"Receipt: {receipt.receipt_id}") # seal:a1b2c3d4TypeScript:
import { NotaryClient } from './index';
const notary = new NotaryClient({ apiKey: 'notary_live_xxx' });
const receipt = await notary.issue('data_processing', { message: 'hello' });
const result = await notary.verify(receipt);
console.log(`Valid: ${result.valid}`); // Valid: trueGo:
client := notary.NewClient(os.Getenv("NOTARY_API_KEY"))
receipt, _ := client.Issue("data_processing", map[string]interface{}{"message": "hello"})
result, _ := client.Verify(receipt)
fmt.Printf("Valid: %v\n", result.Valid)from notaryos import NotaryClient
notary = NotaryClient(api_key="notary_live_xxx")
# Issue a receipt for any action
receipt = notary.issue(
"financial.transfer",
{"message": "Transfer $500 to account XYZ", "amount": 500},
)
print(receipt.receipt_id) # seal:f3a1...8b2c
print(receipt.agent_id) # your-agent-id
print(receipt.action_type) # financial.transfer
print(receipt.payload_hash) # sha256 hash of payload
print(receipt.timestamp) # 2026-02-12T08:49:00.123456+00:00A receipt is an immutable, cryptographically signed record. The SDK Receipt exposes these key fields:
| What it answers | SDK Field | Description |
|---|---|---|
| Who performed the action? | agent_id |
The agent that performed the action |
| What was done? | action_type |
Category of action (e.g., financial.transfer) |
| What data was sent? | payload_hash |
SHA-256 of payload (privacy-preserving) |
| When? | timestamp |
ISO 8601 UTC creation time |
| Is it authentic? | signature, signature_type, key_id |
Cryptographic proof |
| Where in the chain? | previous_receipt_hash |
Links to prior receipt |
The SDK uses clean field names:
agent_id,action_type,payload_hash. These are the only field names you need to know.
Once created, a receipt cannot be modified -- it is cryptographically sealed.
A badge is a compact display format: seal:a1b2...c3d4. It shows the first 4 and last 4 hex characters of the receipt ID. Use it in logs, chat messages, and UI displays.
Every agent maintains its own chain of receipts. Each receipt links to the previous one via previous_hash. This creates an ordered, tamper-evident history per agent:
[Genesis] -> [Receipt 1] -> [Receipt 2] -> [Receipt 3] -> ...
If any receipt is modified, deleted, or reordered, the chain breaks and the tampering is immediately detectable.
The first receipt in any agent's chain uses previous_hash = "000...000" (64 zeros). This is the genesis marker.
receipt = await seal(
{"message": "hello"},
agent_id="my-agent",
action_type="a2a.message",
)receipt = await seal(
{"ticker": "ACME", "action": "buy", "shares": 100},
agent_id="trading-agent",
action_type="trading.execute_order",
)When your receipt depends on upstream receipts:
# This analysis depends on two prior data receipts
receipt = await seal(
{"analysis": "Q4 earnings look strong"},
agent_id="analyst-agent",
action_type="financial.earnings_analysis",
provenance_refs=["seal:abc12345", "seal:def67890"],
)seal() accepts three payload types:
| Type | Normalization |
|---|---|
dict |
json.dumps(sort_keys=True, separators=(",",":")) -- deterministic JSON |
str |
Encoded to UTF-8 bytes |
bytes |
Used directly |
The SHA-256 hash of the normalized bytes becomes the payload_hash.
import { seal } from './notary_seal';
const receipt = await seal(
{ message: "hello", count: 42 },
{ agent_id: "my-agent", action_type: "a2a.message" }
);
console.log(receipt.badge); // seal:a1b2...c3d4
console.log(receipt.signature); // base64url-encoded Ed25519 signatureOffline fallback: If the API is unreachable, seal() produces an unsigned local receipt rather than throwing. Your agent flow continues. The receipt has signature: "unsigned" and key_id: "local-offline".
notary = NotaryClient(api_key="notary_live_xxx")
result = notary.verify(receipt)
print(result.valid) # True/False
print(result.signature_ok) # Signature authentic?
print(result.structure_ok) # All fields present?
print(result.chain_ok) # Chain links correctly?
print(result.reason) # Human-readable explanationEvery receipt can verify itself:
receipt = await seal({"msg": "hello"}, agent_id="a", action_type="a2a.message")
assert receipt.valid # Checks signature locallyYou need only two things: the receipt JSON and the Ed25519 public key.
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import base64
# Load public key (obtain once from /v1/notary/public-key)
public_key = serialization.load_pem_public_key(pem_bytes)
# Rebuild canonical string from receipt fields
canonical = "|".join([
receipt["receipt_id"],
receipt["timestamp"],
receipt["agent_id"],
receipt.get("action_type", "a2a.message"),
receipt["payload_hash"],
receipt.get("previous_receipt_hash", "0" * 64),
])
# Verify
sig_bytes = base64.urlsafe_b64decode(receipt["signature"] + "==")
public_key.verify(sig_bytes, canonical.encode("utf-8"))
# If no exception is raised, the receipt is authenticThe SDK handles canonical format construction and verification internally. Use
NotaryClient.verify()for receipt verification. See INDEPENDENT_VERIFICATION.md for detailed offline verification code.
Verify an entire agent's receipt history:
# Verify each receipt in sequence
for i, receipt_dict in enumerate(receipts_list):
result = notary.verify(receipt_dict)
if not result.valid:
print(f"Tampering at receipt {i}: {result.reason}")
breakTypeScript SDK fast-rejects unsigned receipts without a network call:
const result = await verify(unsignedReceipt);
// result.valid === false
// result.reason === "Receipt was created offline and is unsigned"Each receipt's previous_hash equals the preceding receipt's payload_hash:
Receipt #1: payload_hash = "abc123..." previous_hash = "000...000" (genesis)
Receipt #2: payload_hash = "def456..." previous_hash = "abc123..."
Receipt #3: payload_hash = "ghi789..." previous_hash = "def456..."
- Ordering: Receipt #3 came after #2 (cryptographic, not just timestamps)
- Completeness: Missing a receipt creates a detectable gap
- Integrity: Modifying any receipt's payload changes its hash, breaking the next link
- Non-repudiation: Agent can't deny sending a message in its chain
Each agent_id has its own independent chain. Agent A's chain doesn't affect Agent B's chain. This allows:
- Parallel sealing for different agents
- Independent verification of one agent's history
- No cross-agent bottlenecks
When a receipt references upstream receipts via provenance_refs, it creates a dependency in the provenance DAG (Directed Acyclic Graph). This answers: "What data was this decision based on?"
| State | Meaning | Action |
|---|---|---|
| GROUNDED | All ancestors are valid | Normal operation |
| UNGROUNDED | Own signature OK, but an ancestor is invalid | Investigate upstream |
| INVALID | Own signature broken | Root cause of cascade |
| UNKNOWN | Can't determine (DB unavailable) | Retry later |
# Grounding can be checked via the public API:
# GET /v1/notary/r/{hash}/provenance
# Or programmatically via SDK:
result = notary.lookup(receipt_hash)
status = result["verification"]["grounding_status"] # grounded / ungrounded
if status == "grounded":
print("Full provenance chain intact")
elif status == "ungrounded":
print("An upstream receipt has been invalidated")Get a full provenance analysis:
curl https://api.agenttownsquare.com/v1/notary/r/{hash}/provenanceReturns: status, is_root, depth, ancestors_checked, tainted_ancestors, intact_paths, broken_paths.
When an admin invalidates a receipt, ALL downstream receipts that depend on it become UNGROUNDED. This cascade uses AND logic -- if any ancestor is invalid, the receipt is ungrounded.
Proof that an agent could have acted but chose not to. Standard receipts prove action. Counterfactual receipts prove deliberate non-action.
- Financial agent declined a trade (risk too high)
- Medical agent abstained from recommendation (uncertain)
- Compliance agent saw a pattern but confidence was insufficient
# Counterfactual receipts are created server-side via the API:
# POST /v1/notary/counterfactual/issue (requires API key with issue:write scope)
# Example payload:
proof = await seal_counterfactual(
action_not_taken="financial.execute_trade",
capability_proof={"permissions": ["trade.execute"], "account": "acme"},
opportunity_context={"ticker": "ACME", "price": 142.50, "signal": "buy"},
decision_reason="Risk score 0.87 exceeds threshold 0.75",
agent_id="financial-agent",
declination_reason="risk",
validity_window_minutes=60,
)
print(proof.badge) # cf:a1b2...c3d4 (note: "cf:" prefix)
print(proof.proofs_complete) # True
print(proof.declination_reason) # "risk"| Proof | What it proves |
|---|---|
| Capability | Agent had permission and access |
| Opportunity | Conditions for action were present |
| Decision | Agent evaluated and deliberately declined |
POLICY, CAPACITY, CONFLICT, RISK, UNCERTAINTY, AUTHORIZATION, UNKNOWN
# Verify counterfactual via public API (no auth required):
# GET /v1/notary/counterfactual/r/{hash}
# Or via SDK:
result = notary.verify(proof) # Works for both action and counterfactual receipts
print(result["valid"]) # True
print(result["signature_ok"]) # True
print(result["proofs_complete"]) # True
print(result["in_validity_window"]) # True (within 60 minutes)Source: sdk/python/notary_sdk.py (zero external dependencies -- uses stdlib urllib)
Constructor:
NotaryClient(api_key="notary_live_xxx", base_url=None, timeout=30, max_retries=2)api_key is required. Keys must start with notary_live_ or notary_test_.
Methods:
| Method | Returns | Description |
|---|---|---|
issue(action_type, payload) |
Receipt |
Issue a signed receipt |
verify(receipt) |
VerificationResult |
Verify receipt signature |
verify_by_id(receipt_id) |
VerificationResult |
Verify by server-side lookup |
status() |
ServiceStatus |
Service health check |
public_key() |
dict |
Ed25519 public key (PEM) |
lookup(receipt_hash) |
dict |
Public receipt lookup by hash |
me() |
dict |
Agent info (tier, scopes, rate limits) |
Convenience function: verify_receipt(receipt_dict) -- quick verification without API key (public endpoint).
Data Classes: Receipt, VerificationResult, ServiceStatus
Source: sdk/typescript/src/index.ts (zero dependencies -- native fetch() and crypto.subtle)
Constructor:
new NotaryClient({ apiKey, baseUrl, timeout, maxRetries })Methods: Same as Python but camelCase (publicKey(), verifyChain()).
Key difference: verifyChain() uses Promise.all() for parallel verification.
Runtime compatibility: Node 18+, Deno, Bun, all modern browsers.
Source: sdk/go/notary/client.go (standard library only)
Constructor:
client := notary.NewClient(apiKey)Methods: Issue(), Verify(), Status(), PublicKey(), Lookup()
All SDKs share the same error hierarchy:
NotaryError // Base class
AuthenticationError // 401 -- invalid or missing API key
RateLimitError // 429 -- tier quota exceeded (has retry_after)
ValidationError // 422 -- missing fields or invalid format
Transient errors (5xx and 429) are retried automatically with exponential backoff.
# Get a demo receipt
curl https://api.agenttownsquare.com/v1/notary/sample-receipt
# Verify a receipt
curl -X POST https://api.agenttownsquare.com/v1/notary/verify \
-H "Content-Type: application/json" \
-d '{"receipt": {...}}'
# Look up a receipt by hash
curl https://api.agenttownsquare.com/v1/notary/r/a1b2c3d4
# Get provenance report
curl https://api.agenttownsquare.com/v1/notary/r/a1b2c3d4/provenance
# Get public key for offline verification
curl https://api.agenttownsquare.com/v1/notary/public-key
# Health check
curl https://api.agenttownsquare.com/v1/notary/status
# JWKS endpoint
curl https://api.agenttownsquare.com/.well-known/jwks.json# Login first
TOKEN=$(curl -s -X POST https://api.agenttownsquare.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "you", "password": "pass"}' | jq -r '.access_token')
# Issue a receipt (via API key)
curl -X POST https://api.agenttownsquare.com/v1/notary/issue \
-H "X-API-Key: notary_live_xxx" \
-H "Content-Type: application/json" \
-d '{"action_type": "data_processing", "payload": {"key": "value"}}'
# Get receipt history
curl -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/notary/history?limit=50
# Create API key
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/api-keys \
-H "Content-Type: application/json" \
-d '{"name": "prod-key", "scopes": ["verify:read", "issue:write"]}'| Algorithm | Type | Third-Party Verification | Recommended For |
|---|---|---|---|
| Ed25519 | Asymmetric | Yes (public key only) | Production, multi-org |
| HMAC-SHA256 | Symmetric | No (requires secret) | Single-org, development |
Keys rotate automatically every 90 days (configurable). During rotation:
- New key becomes Active (used for new receipts)
- Old key becomes Retiring (valid for verification for 24 hours)
- After 24 hours, old key becomes Revoked (rejected)
Old receipts remain verifiable -- the key_id in each receipt maps to the correct key via JWKS.
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/notary/admin/rotate-key# Direct endpoint
curl https://api.agenttownsquare.com/v1/notary/public-key
# JWKS endpoint (RFC 7517)
curl https://api.agenttownsquare.com/.well-known/jwks.json| Starter (Free) | Explorer ($59/mo) | Pro ($159/mo) | Enterprise | |
|---|---|---|---|---|
| Receipts/month | 100 | 10,000 | 100,000 | Unlimited |
| Verifications/month | 500 | 50,000 | 500,000 | Unlimited |
| Rate limit | 60/min | 300/min | 1,000/min | Custom |
| Hash chains | -- | Yes | Yes | Yes |
| Provenance DAG | -- | Yes | Yes | Yes |
| Counterfactuals | -- | -- | Yes | Yes |
# Check current status
curl -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/billing/status
# Upgrade via Stripe Checkout
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/billing/create-checkout-session \
-H "Content-Type: application/json" \
-d '{"price_id": "price_xxx"}'
# Self-service portal (invoices, payment methods, cancellation)
curl -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/billing/portal- 120% of quota: Warning notification (requests continue)
- 200% of quota: Requests blocked until next billing period or upgrade
- Grace period: 7 days after trial expiration
curl https://api.agenttownsquare.com/v1/notary/statusReturns: service status, signer type, active agents, uptime.
curl https://api.agenttownsquare.com/v1/notary/metricsReturns: verification cache hit rate, abuse detector state, engine chain count, database row counts.
curl -H "Authorization: Bearer $TOKEN" \
https://api.agenttownsquare.com/v1/notary/admin/statsReturns: total receipts issued, verifications, active agents, last 24h counts, average latencies.
The middleware exposes diagnostics via stats property: enabled, mode, fail_open, engine availability, circuit breaker state.
If seal latency averages over 200ms, the circuit breaker trips and skips sealing for 30 seconds. Check X-Notary-Mode response header for current state.
| Problem | Cause | Fix |
|---|---|---|
ERR_INVALID_SIGNATURE |
Receipt tampered or wrong key | Check key_id matches the signing key; refresh JWKS cache |
ERR_CHAIN_BROKEN |
previous_hash mismatch |
Receipt may have been deleted; check database consistency |
ERR_RATE_LIMIT_EXCEEDED |
Hit tier quota | Upgrade plan or wait for next billing period |
| Unsigned stamps | API unreachable | Check network; stamps can be re-sealed when connectivity restores |
| Agent suspended | High failure rate detected | Wait for auto-reinstatement; investigate verification failures |
| Parameter | Default | Description |
|---|---|---|
api_key |
(required) | Your API key (notary_live_xxx or notary_test_xxx) |
base_url |
https://api.agenttownsquare.com |
API endpoint |
timeout |
30 |
Request timeout in seconds |
max_retries |
2 |
Retry attempts on transient failures |
For managed service configuration (signing keys, middleware, notifications), contact support@notaryos.org.
- API Docs: notaryos.org/api-docs
- Public Key:
https://api.agenttownsquare.com/v1/notary/public-key - Service Status:
https://api.agenttownsquare.com/v1/notary/status - GitHub Issues: Report bugs and feature requests
NotaryOS User Manual v1.5.21 Open-source cryptographic verification for AI agents