Spend rules live in exactly one TypeScript implementation (src/policy.ts,
evaluateSpend) consumed by the control plane (protocol.ts), planner
pre-checks (agents/run.ts), and any future control surface. The engine
mirrors the chain programs rule-for-rule, including evaluation order
(zero-amount → cap → budget → approval), lazy rolling-window semantics with
anchor advancement, and the 8-entry blocklist bound. Local control-plane
escrows carry deadlines copied from their intents and unwind via a
permissionless deadline refund (POST /escrows/:taskId/expire-refund),
matching expire_refund_escrow on-chain. Chain programs remain independent
implementations by necessity (Rust/Solidity); conformance is pinned by unit
vectors on this module plus on-chain unit tests of their own helpers.
- No unsupervised spend above policy. Chain rails enforce, atomically on-chain: allowed-action
bit, per-transaction cap, rolling session budget, recipient blocklist, and approval threshold.
The threshold can only be bypassed with a real registry-owner signature (Solana: owner as main
authority or first
remaining_accountsentry; EVM: caller is owner, or a single-use amount-boundedgrantSpendApprovalis consumed). - Delegates never touch owner wallets. Delegated sources are the delegate's own token account or the program-owned treasury. Owner wallets move only under owner signatures.
- Escrow funds have exactly two exits: to the beneficiary (verifier-signed; verifier ≠ owner ≠
beneficiary enforced at funding) or back to the exact original funding source (owner refund, or
permissionless refund after
expires_at). - Canonical accounts everywhere — escrow ATAs must equal the canonical derivation; delegate
records must live at canonical PDAs / mapping keys; refund destinations pinned by
escrow.payer. - Deterministic reconciliation hashes via code-point-ordered canonical JSON.
- PCI boundary: raw card data is structurally rejected (
number/pan/cvv*body keys); card rails operate exclusively on processor tokens; brand detection exists for routing labels, not for accepting PANs. - Settlement requires captured funds. Fiat release endpoints observe provider state and refuse
(
400) unless the payment isauthorized(captured now) or alreadycaptured. An intent that never completed checkout cannot settle a task — no reputation, no ledger release. - Commitment-bound verification. Intents may pin an expected 32-byte semantic hash at request
time (
reconciliationCommitment). When pinned,release_escrow(Solana),releaseEscrow(EVM), and the verifier-driven fiat release all reject any other hash — making verification evidence non-malleable after the task is scoped. Zero commitment preserves open verification; webhook-driven releases are hash-exempt because capture itself is the provider's attestation.
- Treasury sweeps are owner-only and destination-constrained (
withdraw_treasury). - Blocklist applies to intents AND spends (request-time parity added).
- All transfers use
transfer_checkedwith stored decimals; works with Token-2022.
nonReentranton all value-moving functions; checks-effects-interactions throughout.- Safe ERC-20 handling tolerates missing return values (USDT-style) via returndata-size checks.
- Verifier designation is two-step (
designateVerifier) and self-dealing combinations revert. - Above-threshold delegate spends consume single-use owner approvals bounded by amount + time.
- Escrow = PaymentIntent with
capture_method=manual: funds are held, not charged; release = capture; pre-capture unwind = cancel; post-capture unwind = refund. - Webhooks verified via HMAC-SHA256 over
${timestamp}.${rawBody}with 300s replay tolerance and constant-time comparison. - Dashboard-initiated captures are trusted-operator events; by default they only RECORD the capture
(
PAYMENT_CAPTURED) and the escrow stays FUNDED until the verifier releases via Accural. Operators may opt into auto-settlement withACCURAL_FIAT_AUTO_RELEASE_ON_CAPTURE=true.
- UPI has no merchant-side authorization hold, and collects capture automatically. Therefore
the verifier gate lives in Accural, not the network: a
payment.capturedwebhook records the settlement but leaves the escrow FUNDED pending verifier release (or the explicit auto-release opt-in above). Refunds unwind through/rails/razorpay/refund. - Webhooks verified via HMAC-SHA256 of the raw body against
RAZORPAY_WEBHOOK_SECRET.
- Bearer token (constant-time compare) required on mutating routes when
ACCURAL_API_TOKENis set;/webhooks/*are exempt because they authenticate via provider HMACs. - Bundle execution fails closed without credentials; allowlisted programs only; simulate-first.
- Request bodies capped (default 1 MB); loopback bind by default; loud warning when signing keys exist without an API token.
- Every reconciliation record stores the SHA-256 of its canonical JSON payload.
verifyLedger(exposed atGET /ledger/verify) recomputes all hashes from stored inputs; any post-hoc edit of amount/outcome/purpose/policy snapshot breaks the chain and is reported with both hashes. - Provider webhooks are treated as at-least-once: capture events are deduplicated per task
(
PAYMENT_CAPTUREDrecorded once), auto-released escrows answer redelivery with200 already-released, and refund endpoints observe provider state before acting so a repeated call cannot double-refund.
- FX is out of scope. Fiat charges occur in native currency (INR paise, USD cents); policy
budgets are USDC-denominated. Callers pass
budgetAmountexplicitly for fiat flows — operators own conversion. Do not assume 1:1. - Reputation Sybil bounds: self-dealing triples are blocked, but many-registries farming is
possible; reputation is informational, not a trust root.
total_volume_minormixes mints. - Session windows are lazy — reset happens at next spend after expiry, not on wall clock alone.
- Blocked recipients capped at 8 (fixed-size accounts).
- Token-2022 extension behaviors (transfer fees, confidential transfers) are not compensated.
- Bundle phases are separate transactions; partial progress leaves monotone, safe state but callers must handle continuation.
- EVM contract is unaudited; treat as reference implementation. No native-ETH settlement in v1.
- Webhook idempotency: providers may redeliver; release paths are idempotent by status checks, but operators should deduplicate by event id at the edge for strict once-only semantics.