@agnic/mandate-verifier
v0.1.0
Published
Offline verifier for Agnic Mandate IntentMandateTemplate and IntentMandateDerivation credentials (SD-JWT-VC, W3C Bitstring Status List 2023, did:web).
Maintainers
Readme
@agnic/mandate-verifier
Offline verifier for Agnic Mandate credentials — the WebAuthn-signed AP2-compatible IntentMandate that proves a human user authorized an AI agent's purchase.
MIT-licensed. Zero Agnic API calls at verify time. Ships as a tiny TypeScript package with one dependency (jose).
What you're verifying
When an Agnic-enabled agent places an order on your site, the request carries two SD-JWT-VC credentials:
| Credential | Validity | Signed by | Contains |
|---|---|---|---|
| IntentMandateTemplate | 30 days | User's device passkey (WebAuthn) + Agnic | The user's standing scope: categories, max/tx, daily limit, merchant whitelist |
| IntentMandateDerivation | 5 min | Agnic (scope-contained against template) | One specific transaction: merchant, cart hash, amount, the user's prompt |
As a merchant, you care about proving:
- The user's device really signed the template (legal non-repudiation)
- The derivation is a legitimate narrowing of the template (scope containment)
- The derivation matches the order you're about to fulfill
This library does all three in one call.
Install
npm install @agnic/mandate-verifierQuick start
import { verifyIntentBundle } from '@agnic/mandate-verifier/intent';
// In your /api/order (or /checkout) handler:
const header = req.headers.get('x-intent-mandate');
if (!header) return reject('missing mandate');
const [templateSdJwt, derivationSdJwt] = header.split('~~');
const result = await verifyIntentBundle(templateSdJwt, derivationSdJwt, {
expectedMerchant: 'your-merchant-id',
expectedAmount: { value: '7.35', currency: 'CAD' },
expectedCategories: ['food_beverage'],
});
if (!result.valid) {
console.warn('intent verification failed', result.reasons);
// Decide: reject the order, or accept advisorily and log for disputes.
}
// Store these on your order row for chargeback defense:
const { template_jti, derivation_jti, template_claims, derivation_claims } = result;What gets checked
verifyIntentBundle runs every check a merchant needs, and reports each one:
{
valid: boolean,
template_jti: string,
derivation_jti: string,
checks: {
template_signature: boolean, // issuer signature verified via did:web
derivation_signature: boolean, // issuer signature verified via did:web
parent_match: boolean, // derivation.parent_jti === template.jti
not_expired: boolean, // neither credential past its exp
amount_in_scope: boolean, // derivation.amount ≤ template.max_per_tx AND matches expectedAmount
merchant_in_scope: boolean, // derivation.merchant allowed by template.merchant_whitelist AND matches expectedMerchant
categories_in_scope: boolean, // derivation.categories ⊆ template.categories AND covers expectedCategories
},
reasons: string[], // human-readable failure reasons, empty when valid
template_claims: {...}, // raw claims, useful for chargeback bundle
derivation_claims: {...},
}Step-up approvals
If the user faced an out-of-scope request, they complete a fresh WebAuthn ceremony and Agnic issues a derivation with step_up: true + step_up_evidence. For these, scope-containment against the parent template is skipped — the fresh assertion is stronger evidence than the template's prior grant. The verifier accepts this automatically; the step_up_evidence claim on the derivation (a WebAuthn assertion over sha256(canonicalize(approval_context))) is the authorization.
Just verifying a single credential
If you only need to verify one SD-JWT-VC (e.g. to inspect claims without the bundle context):
import { verifyCredential } from '@agnic/mandate-verifier';
const res = await verifyCredential(sdJwt);
if (res.valid) {
console.log(res.claims, res.issuerDid, res.holderDid, res.vct);
}Offline & cacheable
The verifier makes two HTTP calls, both to public W3C-standard documents on the issuer domain:
GET https://{issuer}/.well-known/did.json— issuer public key (DID Core)GET {statusListCredential}— revocation bit vector (W3C Bitstring Status List 2023)
Both are static, cacheable, and served by the issuer. No authenticated Agnic API call happens at verification time. Agnic downtime does not block verification.
Cache both for 5 minutes and you pay roughly one HTTP round-trip per minute of throughput, regardless of mandate volume.
Spec compliance
- SD-JWT-VC — draft-ietf-oauth-sd-jwt-vc
- did:web — W3C DID Core + did:web spec
- Revocation — W3C Bitstring Status List 2023 (canonical); the older IETF SD-JWT-VC token-status-list draft shape is accepted as a legacy fallback for in-flight credentials
- Signature algorithm — ES256 (P-256 ECDSA) with raw R||S signature bytes (IEEE P1363), JWS-compatible
Tested interop: Agnic's kya-service issuer, as of 2026-04-23.
Advanced options
await verifyCredential(sdJwt, {
issuerPublicKeyJwk: { /* JWK */ }, // skip did:web resolution
skipStatusCheck: true, // skip revocation list
skipExpiryCheck: true, // for chargeback analysis of expired creds
fetch: customFetch, // for testing or HTTP-proxy scenarios
});License
MIT. Copyright © 2026 Agnic Labs.
Links
- Product: https://agnic.ai/mandate
- Issuer source: https://github.com/agnicpay/kya-service (issuance is closed-source; verification is MIT and lives here)
- Bugs / PRs: https://github.com/agnicpay/mandate-verifier-js
