Deterministic governance and control plane for AI-initiated financial actions
AgentShield is a security-first transaction orchestration system that places a deterministic control plane between an AI agent and a payment rail. An LLM may interpret a natural-language request and propose a structured transaction, but it does not own identity, spending authority, policy decisions, payment state, or execution rights.
The core rule is simple:
The model proposes. AgentShield decides.
The current reference implementation uses Groq for intent interpretation, Razorpay in test mode as the payment adapter, SQLite/WAL for persistence and concurrency controls, FastAPI for the API boundary, and React + TypeScript + Vite for the demo control console.
The orchestrator itself has no dependency on any specific LLM vendor, including Claude. It is written entirely against a provider-neutral IntentParser interface defined in integrations/intent_parser.py. Groq is simply the active implementation of that interface, selected at the composition root (ApplicationContainer). There is no Claude-specific branch, fallback, or special-cased behavior anywhere in application/orchestrator.py.
AgentShield is designed as an AP2-aligned reference implementation for agent-initiated payment governance.
flowchart TD
U[User / Client] --> UI[React Control Console]
UI --> API[FastAPI API]
API --> AUTHN[AuthenticatedPrincipal]
AUTHN --> ORCH[AgentShield Orchestrator]
ORCH --> PARSER[IntentParser interface]
PARSER --> LLM[Groq - active provider]
LLM --> ANALYSIS[Structured AgentRequestAnalysis]
ORCH --> AUTHZ[Server-owned Authorization Authority]
ORCH --> POLICY[Deterministic Policy Engine]
POLICY --> CAT[Server-owned Catalog]
AUTHZ --> ORCH
POLICY --> ORCH
ORCH --> HASH[Intent Hasher - SHA-256]
HASH --> MANDATE[AP2-aligned HMAC Mandate]
MANDATE --> VERIFY[Mandate Verification / Revalidation]
VERIFY --> IDEMP[WAL Idempotency Store]
IDEMP --> PAY[Razorpay Adapter]
PAY --> TXN[Transaction Store]
ORCH --> TXN
ORCH --> AUDIT[Hash-chained Audit Trail]
RZ[Razorpay Webhook] --> WH[Webhook Handler]
WH --> RECON[Reconciliation Engine]
RECON --> TXN
RECON --> IDEMP
RECON --> AUDIT
RECON --> TEL[Webhook Telemetry]
REC[Recovery Engine] --> AUTHZ
REC --> TXN
REC --> AUDIT
REC --> SM[Transaction State Machine]
ORCH --> SM
RECON --> SM
Note that the orchestrator only ever talks to the PARSER interface node — it has no direct edge to any named model vendor. Which concrete adapter sits behind that node is a deployment/config decision, not an orchestration-code decision.
A normal execution follows this order:
1. Authenticate caller
2. Derive server-owned user_id / agent_id
3. Parse natural-language request via the configured IntentParser (Groq)
4. Validate the LLM output
5. Reject any identity drift in the model output
6. Create/load the server-owned transaction record
7. Move CREATED → INTENT_VALIDATED
8. Resolve the server-owned authorization
9. Verify authorization deterministically
10. Snapshot the exact authorization onto the transaction
11. Evaluate deterministic policy + catalog facts
12. Move → POLICY_APPROVED
13. Canonicalize + hash governed intent
14. Create HMAC-signed mandate
15. Verify the mandate
16. Move → MANDATE_VALID
17. Revalidate the authorization before external execution
18. Acquire idempotency key atomically
19. Move → LOCK_ACQUIRED
20. Create the Razorpay order
21. Verify returned amount/currency/status
22. Persist Razorpay order ID
23. Move → DISPATCHED
24. Wait for an authenticated webhook
25. Reconcile the external event
26. Update transaction + idempotency + audit state
27. Recover only through explicit recovery transitions
AgentShield uses a strict trust hierarchy.
TRUST BOUNDARY
┌───────────────────────────────────────────────────────────────┐
│ Untrusted / advisory │
│ │
│ User text → LLM interpretation → structured proposal │
│ │
└───────────────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Server-owned control plane │
│ │
│ Identity → Authorization → Policy → Catalog → Hash │
│ → Mandate → Idempotency → Payment → Reconciliation │
│ │
└───────────────────────────────────────────────────────────────┘
The model can:
- interpret natural-language intent;
- identify merchant/SKU/amount/currency information from the request;
- produce a structured analysis for deterministic validation.
The model cannot independently:
- authenticate the caller;
- choose which user or agent owns the transaction;
- grant itself additional spending authority;
- override a revoked/inactive authorization;
- raise a policy limit;
- override catalog price or merchant facts;
- bypass idempotency;
- declare a payment successful;
- choose an unsafe recovery path.
The orchestrator explicitly verifies that the structured model output remains bound to the server-provided user_id, agent_id, and intent_id. This check is implemented once, against the interface's output contract, not per-vendor.
AgentShield does not sandbox the LLM or Razorpay process; it isolates financial authority at the application control-plane boundary.
These are the properties the implementation is designed to preserve.
The execution endpoint derives identity from the authenticated principal rather than accepting user_id and agent_id in the execution payload.
HTTP Authorization header
↓
AuthenticatedPrincipal
↓
user_id / agent_id
↓
Orchestrator
The LLM receives server-owned identity context and the orchestrator rejects analysis results that attempt to change it. This check is provider-agnostic: it runs identically regardless of which IntentParser implementation produced the analysis.
SQLiteAuthorizationAuthority owns creation, lookup, selection, revocation, deactivation, and lifecycle state.
A missing authorization is represented as None; the system does not fabricate a synthetic authorization record.
AuthorizationEvaluation binds the decision to the exact authorization object that was evaluated. An evaluation cannot claim that decision auth_A was produced from authorization auth_B.
The exact server-owned authorization used for execution is stored on the transaction as an authorization_snapshot.
This gives the transaction a durable record of the authority that governed it at the point of execution.
The policy engine checks the concrete proposal against server-owned policy and catalog facts.
Examples include:
- identity match;
- INR-only boundary;
- authorization currency match;
- maximum/minimum amount;
- merchant allowlist;
- SKU allowlist;
- maximum quantity;
- catalog existence;
- catalog merchant match;
- catalog currency match;
- exact catalog-derived amount;
- bank-rail availability.
The governed authorization + proposal are canonicalized and hashed using SHA-256.
Conceptually:
canonical(authorization, proposal)
↓
SHA-256 digest
↓
intent_hash
A material change to the governed authorization or proposal changes the hash.
The mandate contains the governed identity, merchant, amount, intent hash, nonce, issuance time, and expiry. It is protected by HMAC-SHA256.
Verification checks:
- user binding;
- agent binding;
- merchant binding;
- amount binding;
- nonce binding;
- issuance time;
- expiry;
- recomputed intent hash;
- HMAC signature.
Authorization is not treated as permanently valid merely because an earlier check passed. The execution path can revalidate current server-owned authorization state before an external payment call.
This protects against stale authorization reuse after revocation/deactivation.
The idempotency store uses a database primary key as the concurrency authority.
There is deliberately no unsafe check-then-insert pattern:
BAD:
check(key)
↓
if missing:
insert(key)
GOOD:
INSERT OR IGNORE
↓
primary-key uniqueness
↓
one winner
If a payment call may have reached Razorpay but the client does not know the outcome, AgentShield records:
DISPATCHED → UNKNOWN
It does not assume failure and blindly retry.
The intended path is:
UNKNOWN
↓
RECONCILE_PENDING
↓
reconcile external evidence
Razorpay webhook signatures are verified over the raw request body using HMAC comparison before event parsing/reconciliation proceeds.
Webhook event IDs are tracked separately from payment execution idempotency because:
execution identity ≠ delivery identity
This prevents repeated webhook delivery from being treated as a new financial action.
Transaction and audit reads are scoped to the authenticated principal's user_id and agent_id.
A caller must not be able to read another principal's transaction or audit trail merely by guessing its identifier.
Audit events contain:
sequence
previous_event_hash
event_hash
The chain can be independently verified using verify_chain().
Recovery actions use the transaction state machine and re-check the current authorization state where required. Recovery is not an escape hatch around governance.
The transaction lifecycle is explicit and fail-aware.
stateDiagram-v2
[*] --> CREATED
CREATED --> INTENT_VALIDATED
INTENT_VALIDATED --> POLICY_APPROVED
POLICY_APPROVED --> MANDATE_VALID
MANDATE_VALID --> LOCK_ACQUIRED
LOCK_ACQUIRED --> DISPATCHED
DISPATCHED --> SUCCESS
DISPATCHED --> UNKNOWN
UNKNOWN --> RECONCILE_PENDING
RECONCILE_PENDING --> SUCCESS
RECONCILE_PENDING --> FAILED_SAFE_TO_RETRY
SUCCESS --> COMPLETED
DISPATCHED --> STOCKOUT_DETECTED
STOCKOUT_DETECTED --> REFUNDING
REFUNDING --> REFUNDED
REFUNDED --> REROUTING
REROUTING --> RECOVERED
RECOVERED --> COMPLETED
The code does not permit arbitrary state jumps. TransactionStateMachine is the source of truth for legal transitions.
sequenceDiagram
participant C as Caller
participant API as FastAPI
participant O as Orchestrator
participant A as Authorization Authority
participant P as Policy Engine
participant M as Mandate Engine
participant R as Razorpay
C->>API: Authenticated execution request
API->>O: user_id/agent_id from principal
O->>A: Find usable authorization
A-->>O: Server-owned authorization
O->>A: Deterministic authorization check
A-->>O: AuthorizationEvaluation
O->>P: Proposal + server authorization + policy
P-->>O: PolicyDecision
O->>M: Create mandate
M-->>O: Signed mandate
O->>M: Verify mandate
M-->>O: valid
O->>A: Revalidate current authorization
A-->>O: Still valid
O->>R: Create order
R-->>O: Order response
An authorization can be:
- active;
- inactive;
- revoked;
- expired.
Selection and execution are deterministic. A stale authorization snapshot does not grant authority after the current authorization has become unusable.
AgentShield deliberately separates authorization from policy.
"Has this user granted this agent this bounded authority?"
Examples:
maximum amount
allowed merchants
allowed SKUs
maximum quantity
currency
expiry
active/revoked state
"Does this concrete transaction satisfy the platform's deterministic operating rules?"
Examples:
currency must be INR
amount must be within platform bounds
merchant must be allowed
catalog SKU must exist
catalog price must match
catalog merchant must match
payment rail must be available
This separation prevents one model or one object from becoming a universal authority.
engine/hashing.py canonicalizes the governed authorization and proposal before hashing.
The important property is determinism:
same authorization + same proposal
↓
same canonical form
↓
same SHA-256 hash
Changing a material field changes the digest.
engine/mandate.py creates an HMAC-SHA256 signature over a structured payload containing:
user_id
agent_id
merchant_id
amount_paise
intent_hash
nonce
issued_at
expires_at
The current implementation uses a shared server secret supplied through MANDATE_SECRET_KEY.
The audit trail links each event to the previous event hash:
Event 1
event_hash = H1
↓
Event 2
previous_event_hash = H1
event_hash = H2
↓
Event 3
previous_event_hash = H2
event_hash = H3
Editing a historical event breaks subsequent verification.
AgentShield assumes external systems can fail after a request has already crossed a boundary.
LOCK_ACQUIRED
↓
Razorpay request
↓
network / external failure
↓
UNKNOWN
↓
reconciliation
The system does not blindly retry from UNKNOWN.
For fulfillment-side problems such as stockouts, the state machine represents:
STOCKOUT_DETECTED
↓
REFUNDING
↓
REFUNDED
↓
REROUTING
↓
RECOVERED
↓
COMPLETED
Each successful transition is persisted and auditable.
flowchart LR
RZ[Razorpay] -->|Signed webhook| WH[Webhook Handler]
WH --> SIG[Signature verification]
SIG --> PARSE[Event parsing]
PARSE --> RECON[Reconciliation Engine]
RECON --> DEDUP[Deduplication]
RECON --> CORRELATE[Correlation]
RECON --> VALIDATE[Validation]
RECON --> TXN[Transaction Store]
RECON --> IDEMP[Idempotency Store]
RECON --> AUDIT[Audit Trail]
RECON --> TEL[Telemetry]
The webhook path is intentionally separate from the request/execute path.
A payment request and a webhook delivery have different identities and failure modes.
api/
FastAPI transport, authentication dependency, HTTP request/response models
application/
Orchestrator and dependency container, vendor-neutral, no Claude dependency
engine/
Deterministic security engines
integrations/
IntentParser interface + the active Groq adapter, plus the Razorpay adapter
(a reference third-party adapter may exist here as an unwired example only)
models/
Pydantic domain contracts and state representations
webhooks/
Razorpay webhook authentication and parsing
recovery/
Deterministic recovery workflows
evaluation/
Deterministic control-plane and adversarial evaluation
frontend/
React + TypeScript control console
tests/
Unit, integration, concurrency, security and end-to-end tests
agentshield-main/
├── api/
│ ├── dependencies.py # dependency injection + authenticated principal
│ └── main.py # FastAPI application and HTTP routes
│
├── application/
│ ├── container.py # runtime dependency graph (wires GroqIntentParser only)
│ └── orchestrator.py # authoritative execution ordering, vendor-agnostic
│
├── engine/
│ ├── authorization.py # authorization verification + persistence
│ ├── audit.py # hash-chained audit trail
│ ├── catalog.py # server-owned catalog
│ ├── hashing.py # canonicalization + SHA-256 intent hashing
│ ├── idempotency.py # WAL-backed execution idempotency
│ ├── mandate.py # HMAC-signed mandate creation/verification
│ ├── policy.py # deterministic policy engine
│ ├── reconciliation.py # webhook deduplication + reconciliation
│ ├── state_machine.py # legal transaction transitions
│ ├── telemetry.py # webhook telemetry persistence
│ └── transaction_store.py # transaction persistence
│
├── evaluation/
│ ├── runner.py # deterministic evaluator
│ ├── scenarios.py # evaluation cases
│ ├── adversarial_runner.py # adversarial orchestrator evaluation
│ └── adversarial_scenarios.py
│
├── integrations/
│ ├── intent_parser.py # provider-neutral IntentParser interface
│ ├── groq.py # active LLM adapter, implements IntentParser
│ └── razorpay.py # Razorpay payment adapter
│
├── models/
│ ├── api.py # API request/response contracts
│ ├── authorization.py # authorization domain contracts
│ ├── intent.py # structured intent contracts
│ ├── mandate.py # mandate contracts
│ ├── policy.py # policy contracts
│ ├── telemetry.py # webhook telemetry contracts
│ ├── transaction.py # transaction/state contracts
│ ├── webhook.py # webhook contracts
│ └── ...
│
├── recovery/
│ └── transaction.py # governed recovery engine
│
├── webhooks/
│ └── razorpay.py # raw-body signature verification + parsing
│
├── frontend/
│ └── src/ # React control console / Flight Recorder
│
├── scripts/
│ ├── seed_demo_data.py
│ ├── run_evaluation.py
│ ├── run_adversarial.py
│ └── groq_smoke_test.py
│
├── reports/
│ ├── evaluation.json
│ └── adversarial.json
│
├── tests/ # comprehensive automated test suite
├── .env.example
├── requirements.txt
└── server.py # configured application entry point
Unauthenticated liveness endpoint.
Example:
curl http://localhost:8000/healthzResponse:
{"status":"ok"}Runs the complete governed execution flow.
The current demo/API authentication mechanism uses a configured Bearer token. The authenticated principal is resolved server-side.
Required headers:
Authorization: Bearer <AGENTSHIELD_API_TOKEN>
Idempotency-Key: <unique-logical-execution-key>
Content-Type: application/json{
"user_message": "Buy exactly one shoe_001 from merchant_001 for ₹4500.",
"merchant_context": {
"source": "demo"
}
}curl -X POST http://localhost:8000/v1/agent/execute \
-H "Authorization: Bearer $AGENTSHIELD_API_TOKEN" \
-H "Idempotency-Key: demo-key-001" \
-H "Content-Type: application/json" \
-d '{"user_message":"Buy exactly one shoe_001 from merchant_001 for ₹4500."}'The response contains the governed transaction and the generated mandate.
Returns a transaction only when the authenticated principal owns it.
Returns the transaction's audit trail, also ownership-scoped.
The event chain contains sequence and hash-link information that can be independently verified by the audit engine.
Receives and reconciles authenticated Razorpay events.
Required headers:
X-Razorpay-Signature: <signature>
X-Razorpay-Event-Id: <event-id>The API verifies the raw body signature before parsing the event.
Copy the template:
cp .env.example .env| Variable | Purpose |
|---|---|
RAZORPAY_KEY_ID |
Razorpay test-mode key ID |
RAZORPAY_KEY_SECRET |
Razorpay test-mode secret |
RAZORPAY_WEBHOOK_SECRET |
Secret used to verify incoming Razorpay webhooks |
MANDATE_SECRET_KEY |
Server secret for HMAC mandate signing; minimum 32 bytes |
AGENTSHIELD_API_TOKEN |
API Bearer credential; minimum 32 characters in the current configuration |
AGENTSHIELD_API_USER_ID |
Server-owned demo principal user ID |
AGENTSHIELD_API_AGENT_ID |
Server-owned demo principal agent ID |
GROQ_API_KEY |
Groq API key |
GROQ_MODEL |
Active Groq model identifier |
| Variable | Default | Purpose |
|---|---|---|
DATABASE_PATH |
state.db |
Prefix used for SQLite state databases |
MANDATE_TTL_SECONDS |
300 |
Mandate lifetime |
MAX_RETRIES |
3 |
Configured retry limit |
REQUEST_TIMEOUT_SECONDS |
10.0 |
Razorpay client timeout |
LLM_PROVIDER |
groq |
Current configuration accepts groq |
Generate strong random values with:
python -c "import secrets; print(secrets.token_hex(32))"- Python 3.11+
- Node.js 18+
- npm
- Groq account/API key for the active intent parser
- Razorpay test-mode credentials for payment-adapter integration
git clone <repository-url>
cd agentshield-mainpython -m venv .venv
source .venv/bin/activateWindows:
.venv\Scripts\activatepip install -r requirements.txtcp .env.example .envFill in the required values described above.
python -m scripts.seed_demo_dataThe demo seeder creates a server-owned authorization and catalog entry used by the frontend sample flow.
For the actual demo, the explicit demo policy must be enabled:
set -a
source .env
set +a
export AGENTSHIELD_DEMO_MODE=true
uvicorn server:app --reload --port 8000cd frontend
npm install
npm run devThe frontend is a demonstration/control console, not a general-purpose checkout interface.
The repository includes two intentionally simple sample requests.
Buy exactly one shoe_001 from merchant_001 for ₹4500.
Expected path:
Authorization ✓
Policy ✓
Mandate ✓
Idempotency ✓
Razorpay ✓ / DISPATCHED
Buy shoe_001 from merchant_001 for ₹9000.
The demo authorization limits the amount, so the control plane should reject the request before payment execution.
The frontend's centerpiece is the Flight Recorder, a visual lifecycle view of every governed transaction, rendered as an explicit sequence:
Authorization → Policy → Mandate → Idempotency → Razorpay → Webhook → Reconciliation
Each stage is backed by the real transaction and audit records rather than a simulated or illustrative timeline, so what the Flight Recorder shows is exactly what the control plane did.
The project has an intentionally heavy test suite because payment governance must be tested as a set of invariants, not just as happy-path business logic.
The test suite covers areas including:
authorization
authorization authority
identity binding
policy
catalog
hashing
mandates
idempotency
concurrency
state machine
transaction store
audit chain
webhooks
reconciliation
recovery
orchestration
API boundaries
container wiring
telemetry
end-to-end flows
adversarial scenarios
Run the suite:
pytest -qevaluation/runner.py tests the control plane without depending on a live LLM, Razorpay, or network access.
Run:
python -m scripts.run_evaluationThe evaluator checks whether deterministic authorization + policy + catalog controls produce the expected allow/block result, independent of which IntentParser implementation is configured.
The checked-in report currently records:
Total cases: 71
Passed: 71
Failed: 0
Authorization bypasses: 0
Policy bypasses: 0
Unsafe executions: 0
Duplicate executions: 0
These are the values stored in reports/evaluation.json in this repository snapshot.
The adversarial runner simulates hostile or malformed LLM behavior without granting the test parser any real payment authority.
Run:
python -m scripts.run_adversarialThe repository's checked-in adversarial report currently records:
Total scenarios: 20
Passed: 20
Authorization bypasses: 0
Policy bypasses: 0
Unsafe executions: 0
Execution-after-block: 0
The scenarios include examples such as:
- user identity spoofing;
- agent identity spoofing;
- intent ID spoofing;
- revoked authorization;
- inactive authorization;
- amount escalation;
- quantity inflation;
- currency escalation;
- catalog amount bypass;
- merchant confusion;
- SKU confusion;
- bank-rail outage bypass;
- hallucinated approval;
- prompt-injection attempts;
- hidden quantity/currency manipulation.
The frontend is a React + TypeScript + Vite application under frontend/.
Its role is primarily operational visibility and demonstration.
It shows:
- request intent;
- authorization result;
- policy result;
- mandate status;
- idempotency state;
- Razorpay dispatch state;
- transaction identifiers;
- intent hash;
- authorization snapshot;
- audit events;
- blocked vs approved outcomes;
UNKNOWN/reconciliation semantics where applicable.
The current demo frontend reads VITE_AGENTSHIELD_API_TOKEN from its build-time environment and uses it as a Bearer token. Because Vite VITE_* variables are client-visible.
integrations/razorpay.py contains the payment adapter.
The orchestrator only calls the payment integration after the governance gates have succeeded.
Before trusting the created order, the orchestrator verifies:
returned amount == governed transaction amount
returned currency == governed currency
returned status == created
This prevents the external provider response from silently changing the transaction semantics.
Recovery is intentionally separate from ordinary execution.
flowchart TD
A[External ambiguity / fulfillment failure] --> B{Current transaction state}
B -->|UNKNOWN| C[Reconciliation]
C --> D{External evidence}
D -->|Success| E[SUCCESS]
D -->|Safe failure| F[FAILED_SAFE_TO_RETRY]
B -->|STOCKOUT_DETECTED| G[Recovery Engine]
G --> H[REFUNDING]
H --> I[REFUNDED]
I --> J[REROUTING]
J --> K[RECOVERED]
K --> L[COMPLETED]
The recovery engine uses the same server-owned authorization authority and transaction state machine instead of bypassing them.
AgentShield is designed against the following classes of failure/attack:
| Threat | Control |
|---|---|
| LLM claims different user | server-owned principal + identity binding |
| LLM claims different agent | server-owned principal + identity binding |
| LLM claims a fake approval | deterministic authorization/policy |
| Amount escalation | authorization + policy + catalog |
| Quantity manipulation | authorization + policy + catalog |
| Currency manipulation | INR + policy + authorization + catalog checks |
| Merchant substitution | authorization + catalog |
| SKU substitution | authorization + catalog |
| Replay / duplicate execution | idempotency key + DB uniqueness |
| Stale/revoked authorization reuse | current authorization revalidation |
| Mandate tampering | intent hash + HMAC |
| Webhook spoofing | raw-body HMAC signature verification |
| Webhook replay | webhook event deduplication |
| Uncertain payment outcome | UNKNOWN + reconciliation |
| Invalid state transition | state machine |
| Cross-user transaction access | principal ownership checks |
| Audit alteration | hash-chained audit trail |
A model saying "approved" is not authorization.
Identity, authorization, policy, catalog, transaction state, and external payment state are authoritative only when established by trusted application components.
Unknown is not failure. Failure is not success. Recovery must be stateful.
External payment state comes from verified provider evidence, not from a local assumption after an uncertain network call.
The transaction is tied to authorization through a canonical intent hash and signed mandate.
The idempotency ledger uses the database primary key as the source of truth for duplicate prevention.
Audit is part of the control plane, not an afterthought added after execution.
The control plane's guarantees must not depend on which LLM vendor is plugged in. Orchestration correctness is proven against the IntentParser interface, not against any single provider's behavior.
# Start API (local demo)
set -a
source .env
set +a
export AGENTSHIELD_DEMO_MODE=true
uvicorn server:app --reload --port 8000
# Run all tests
pytest -q
# Run deterministic evaluation
python -m scripts.run_evaluation
# Run adversarial evaluation
python -m scripts.run_adversarial
# Seed local demo state
python -m scripts.seed_demo_data
# Run Groq smoke test
python -m scripts.groq_smoke_testcd frontend
npm install
npm run dev
npm run build
npm run lint
npm run previewConsider:
"Buy exactly one shoe_001 from merchant_001 for ₹4500."
The system does not do this:
LLM → "Looks authorized" → Razorpay
Instead it does:
Natural language
↓
IntentParser interpretation (Groq)
↓
Strict structured validation
↓
Server-owned identity
↓
Server-owned authorization lookup
↓
Authorization verification
↓
Authorization snapshot
↓
Deterministic policy
↓
Catalog verification
↓
SHA-256 intent hash
↓
HMAC mandate
↓
Mandate verification
↓
Atomic idempotency acquisition
↓
Razorpay order creation
↓
Provider-response validation
↓
DISPATCHED
↓
Signed webhook
↓
Deduplicated reconciliation
↓
Audit + final state
If any deterministic gate fails, the system stops before crossing the payment boundary.
MIT License.
Copyright (c) 2026 Parv