tandardized interfaces:
EIP-712 typed data provides domain separation and structured payload fields
EIP-4361 (Sign-In with Ethereum) supplies origin, URI, chain-id, nonce, and issuance timestamps
ERC-20 and ERC-2612 define approve, allowance, spender, value, nonce, and deadline
Permit2 separates one-time signature transfers from persistent allowance transfers
CAIP-2 and WalletConnect namespaces provide chain labels and session scope boundaries
Step 2: Classify Approval Scope
Allowance requests must be categorized before rule evaluation. The classifier distinguishes between:
ONE_TIME: Single-use signature with explicit value and deadline
BOUNDED: Fixed maximum allowance with optional expiration
TIME_LIMITED: Allowance valid only within a specific timestamp window
UNLIMITED: No explicit cap or expiration, granting persistent transfer authority
Step 3: Integrate AI Risk Scoring as Evidence-Only
External risk models may flag lookalike domains, impersonation patterns, or unusual urgency. These scores must never override deterministic logic. The architecture enforces an evidence_only authority flag, ensuring AI output supplements rather than dictates the hold decision.
Step 4: Evaluate Against a Rule Engine
A deterministic rule engine evaluates the extracted packet against predefined conditions. Rules combine origin verification, counterparty status, approval scope, and chain context. When a rule matches, the engine generates a hold payload with a specific rule ID, decision state, and user-visible explanation.
Step 5: Generate Hold Payload and Enforce Release Check
If a hold is triggered, the wallet displays a structured warning and blocks signature release. Before allowing the flow to continue, a release check validates four mandatory fields: one source-path fact, one decoded wallet-action fact, one authority-scope fact, and one user-visible hold reason. Missing any field prevents signature release.
Implementation Example (TypeScript)
interface SignatureGatePacket {
event_type: 'pre_signature_verification.v1';
request_context: {
origin: string;
chain_label: string;
signing_method: string;
nonce: string;
issued_at: number;
};
authority_classification: {
spender_address: string;
counterparty_status: 'known' | 'new' | 'blacklisted';
approval_scope: 'one_time' | 'bounded' | 'time_limited' | 'unlimited';
deadline_or_expiration: number | null;
};
risk_assessment: {
model_score: number;
model_authority: 'evidence_only';
flagged_patterns: string[];
};
gate_decision: {
rule_id: string;
decision: 'allow' | 'hold' | 'reject';
user_visible_reason: string;
remediation_path: string;
};
}
class PreSignatureVerifier {
private readonly ruleEngine: RuleEngine;
constructor(ruleEngine: RuleEngine) {
this.ruleEngine = ruleEngine;
}
async evaluate(request: SigningRequest): Promise<SignatureGatePacket> {
const context = this.extractContext(request);
const authority = this.classifyAuthority(request);
const risk = await this.fetchRiskEvidence(request);
const decision = this.ruleEngine.evaluate(context, authority, risk);
return {
event_type: 'pre_signature_verification.v1',
request_context: context,
authority_classification: authority,
risk_assessment: risk,
gate_decision: decision
};
}
private extractContext(req: SigningRequest) {
return {
origin: req.domain || 'unknown',
chain_label: req.chainId ? `eip155:${req.chainId}` : 'unspecified',
signing_method: req.method,
nonce: req.nonce || crypto.randomUUID(),
issued_at: Date.now()
};
}
private classifyAuthority(req: SigningRequest) {
const spender = req.spender || '0x0000000000000000000000000000000000000000';
const scope = this.determineScope(req.allowance, req.deadline);
return {
spender_address: spender,
counterparty_status: this.checkCounterparty(spender),
approval_scope: scope,
deadline_or_expiration: req.deadline || null
};
}
private determineScope(allowance: bigint, deadline: number | null): ApprovalScope {
if (allowance === 0n) return 'one_time';
if (deadline !== null && allowance < MAX_UINT256) return 'time_limited';
if (allowance < MAX_UINT256) return 'bounded';
return 'unlimited';
}
async releaseCheck(packet: SignatureGatePacket): boolean {
const hasOrigin = packet.request_context.origin !== 'unknown';
const hasAction = packet.authority_classification.spender_address !== '0x0000000000000000000000000000000000000000';
const hasScope = packet.authority_classification.approval_scope !== undefined;
const hasReason = packet.gate_decision.user_visible_reason.length > 0;
return hasOrigin && hasAction && hasScope && hasReason;
}
}
Architecture Rationale
The pipeline separates extraction, classification, risk assessment, and rule evaluation into distinct stages. This prevents AI scoring from contaminating deterministic field validation. The releaseCheck method enforces a minimum metadata threshold before signature release, ensuring no hold can be bypassed without complete context. The evidence_only authority flag guarantees that model outputs never suppress warnings or weaken spending caps. This design aligns with wallet tooling capabilities like MetaMask Snaps transaction insights and signature insights, which can inspect origin and calldata before confirmation without requiring universal wallet standardization.
Pitfall Guide
1. AI Overreach in Decision Logic
Explanation: Allowing model scores to directly approve or reject signatures introduces non-deterministic behavior. AI models hallucinate, drift, and lack cryptographic grounding.
Fix: Enforce model_authority: 'evidence_only' at the type level. Route all decisions through a deterministic rule engine that treats AI scores as weighted inputs, not execution triggers.
2. Vague Warning Copy
Explanation: Generic messages like "This looks risky" or "Proceed with caution" provide zero verifiable context. Users cannot audit what authority they are granting.
Fix: Mandate structured warning templates that specify the action, the reason, and a user-verifiable next step. Example: "New spender requests broad token access from an unverified origin. Reopen from a verified entry point or reduce the approval limit."
3. Ignoring Permit2 and ERC-2612 Nuances
Explanation: Treating all approvals as standard ERC-20 approve calls misses signed permit mechanics. Permit2 separates one-time signature transfers from allowance transfers, and ERC-2612 introduces deadline-based permits.
Fix: Implement a scope classifier that explicitly detects Permit2 allowance transfers and ERC-2612 signed permits. Map deadline and nonce fields to expiration logic rather than assuming infinite validity.
4. Missing Deadline and Nonce Validation
Explanation: Replay protection is not guaranteed by EIP-712 alone. Without validating deadline and nonce, signed permits can be replayed or expired allowances can be reused.
Fix: Require deadline_or_expiration extraction for all permit-based requests. Reject or hold signatures where deadline < current_timestamp or nonce conflicts with existing state.
5. Over-Collecting User Data
Explanation: Review tickets and packet logs that capture chat history, seed phrases, private keys, or full message contents create compliance liabilities and attack surfaces.
Fix: Restrict packet payloads to request metadata only. Post-action review tickets should log origin, wallet event, approval scope, rule ID, model score, user action, and final fraud label. Never persist cryptographic secrets or conversational data.
6. Assuming Universal Wallet Support
Explanation: Building hard dependencies on MetaMask Snaps, WalletConnect namespaces, or specific signature insight APIs breaks compatibility with legacy wallets or alternative clients.
Fix: Implement graceful degradation. If structured insights are unavailable, fall back to standard prompt behavior while logging the capability gap. Use feature flags to enable packet gating only when wallet tooling supports it.
7. Skipping the Release Check
Explanation: Allowing users to bypass holds without validating the four mandatory fields (origin, action, scope, reason) creates a security bypass. The product ships a "polished guess" instead of a verified state.
Fix: Enforce the release check at the wallet interface layer. Block signature release until hasOrigin && hasAction && hasScope && hasReason evaluates to true. Log all bypass attempts for audit trails.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| DeFi Aggregator | Permit2-aware scope classifier + bounded defaults | High volume of allowance requests requires explicit expiration and cap enforcement | Low (reduces unlimited approval exposure) |
| NFT Marketplace | Origin verification + new spender holds | Impersonation and lookalike domains target marketplace signatures | Medium (adds latency to first-time approvals) |
| Enterprise Payroll | Time-limited scope + deterministic rule overrides | Predictable spenders require strict deadline and nonce validation | Low (aligns with existing compliance workflows) |
| High-Frequency Trading | Evidence-only AI scoring + fast-path release | Latency sensitivity requires minimal hold friction with post-trade audit | High (requires optimized rule engine and caching) |
Configuration Template
# pre_signature_gate_config.yaml
gate:
version: v1
release_check:
required_fields:
- origin
- spender_address
- approval_scope
- user_visible_reason
ai_scoring:
authority: evidence_only
max_score_threshold: 0.95
fallback_action: hold
rules:
- id: new_spender_unlimited_scope_unverified_origin
conditions:
counterparty_status: new
approval_scope: unlimited
origin_verified: false
decision: hold
warning_template: "New spender requests broad token access from an unverified request path. Reopen from a user-verified entry point or reduce the approval limit."
remediation: reduce_scope_or_verify_origin
- id: expired_permit_replay_attempt
conditions:
deadline_expired: true
signing_method: eth_signTypedData_v4
decision: reject
warning_template: "Permit deadline has expired. Replay protection requires a fresh signature with a valid nonce."
remediation: request_new_signature
- id: bounded_allowance_known_spender
conditions:
counterparty_status: known
approval_scope: bounded
deadline_valid: true
decision: allow
warning_template: null
remediation: null
Quick Start Guide
- Initialize the Verifier: Import the
PreSignatureVerifier class and inject a rule engine instance configured with your organization's risk thresholds and warning templates.
- Hook into Wallet Events: Attach the verifier to signature and approval request listeners. Extract EIP-712, ERC-20/2612, and Permit2 fields before the wallet UI renders the confirmation dialog.
- Evaluate and Gate: Call
evaluate() on the incoming request. If the rule engine returns hold, render the structured warning and block signature release until the user acknowledges the remediation path.
- Enforce Release Check: Before allowing the wallet to sign, run
releaseCheck(). Verify all four mandatory fields are populated. Log the packet to your incident ledger for post-action review.
- Deploy with Feature Flags: Roll out packet gating behind a feature flag. Monitor hold rates, bypass attempts, and user friction metrics. Adjust rule thresholds based on production telemetry before full deployment.