Documentation Index Fetch the complete documentation index at: https://docs.moca.network/llms.txt
Use this file to discover all available pages before exploring further.
Loyalty programs today are siloed — a user’s Gold status with one brand is invisible to another. AIR Kit lets you issue verifiable loyalty credentials that users carry in their AIR Account and present anywhere in the ecosystem, across brands, apps, and chains.
What You Can Build
Tier credentials — Issue a “Gold Member” or “VIP” badge that upgrades or revokes automatically as status changes
Milestone badges — Issue a credential when a user reaches 1,000 points, 10 purchases, or any event-driven threshold
Cross-platform loyalty — Allow partner apps to accept your loyalty credentials as proof of status, no API integration required
Zero-friction issuance — Trigger credential issuance from your loyalty engine backend; the user doesn’t need to open a wallet or take any action
Architecture
Recommended Schema
Create this schema in the AIR Kit Dashboard under Issuer → Schema Builder .
{
"title" : "Loyalty Tier" ,
"description" : "Brand loyalty membership tier credential" ,
"properties" : {
"tier" : {
"type" : "string" ,
"enum" : [ "Bronze" , "Silver" , "Gold" , "Platinum" ],
"description" : "Current loyalty tier"
},
"totalPoints" : {
"type" : "number" ,
"description" : "Lifetime points accumulated"
},
"memberSince" : {
"type" : "string" ,
"format" : "date" ,
"description" : "Date of first loyalty enrollment"
},
"brandId" : {
"type" : "string" ,
"description" : "Issuing brand identifier"
}
},
"required" : [ "tier" , "totalPoints" , "brandId" ]
}
Implementation
Step 1 — Issue a loyalty credential on milestone
Use Issue on Behalf to issue silently when a backend event fires. The user’s session is not required.
// loyalty-engine.js
const { getPartnerJwt } = require ( './lib/jwt' ); // see Partner Authentication
const BASE_URL = 'https://api.sandbox.mocachain.org/v1' ; // swap for prod URL on launch
async function issueLoyaltyCredential ({ userEmail , tier , totalPoints , memberSince }) {
const token = await getPartnerJwt ( userEmail ); // JWT must include scope: "issue on-behalf"
const res = await fetch ( ` ${ BASE_URL } /credentials/issue-on-behalf` , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'x-partner-auth' : token ,
},
body: JSON . stringify ({
issuerDid: process . env . ISSUER_DID , // Dashboard → Accounts → General
credentialId: process . env . LOYALTY_CRED_ID , // Dashboard → Issuer → Programs
credentialSubject: { tier , totalPoints , memberSince , brandId: process . env . BRAND_ID },
onDuplicate: 'revoke' , // upgrade tier by revoking old credential, issuing new one
}),
});
if ( ! res . ok ) throw new Error ( `Issuance failed: ${ res . status } ` );
const { coreClaimHash } = await res . json ();
return coreClaimHash ;
}
// Hook into your existing loyalty event pipeline
loyaltyEngine . on ( 'tier:upgrade' , async ({ userEmail , newTier , totalPoints }) => {
const hash = await issueLoyaltyCredential ({
userEmail ,
tier: newTier ,
totalPoints ,
memberSince: new Date (). toISOString (). split ( 'T' )[ 0 ],
});
console . log ( `Loyalty credential issued: ${ hash } ` );
});
Step 2 — Verify loyalty tier at point of benefit
On your frontend, verify the user holds the required tier before granting access or rewards.
// loyalty-gate.js (frontend)
import { AirService } from '@mocanetwork/airkit' ;
import { AirService , BUILD_ENV } from "@mocanetwork/airkit" ;
const airService = new AirService ({ partnerId: process . env . PARTNER_ID });
await airService . init ({ buildEnv: BUILD_ENV . SANDBOX }); // or BUILD_ENV.PRODUCTION
async function checkLoyaltyTier () {
const result = await airService . verifyCredential ({
programId: process . env . LOYALTY_VERIFY_PROGRAM_ID , // Dashboard → Verifier → Programs
});
// Your verifier program rule: tier === "Gold" (or higher)
return result . status === 'COMPLIANT' ;
}
const hasGoldStatus = await checkLoyaltyTier ();
if ( hasGoldStatus ) {
unlockPremiumContent ();
} else {
showUpgradePrompt ();
}
Key Patterns
Pattern onDuplicate valueWhen to Use Tier upgrade "revoke"Always revoke old credential and issue new one One-time milestone badge "ignore"Don’t re-issue if credential already held Time-boxed VIP Set expirationDate in subject Seasonal or campaign-based access Retroactive bulk issuance Loop issueLoyaltyCredential Migrating existing loyalty members
Status Polling (optional)
Issuance is asynchronous. Poll until ONCHAIN before showing a confirmation to the user.
async function waitForOnchain ( userEmail , coreClaimHash , maxAttempts = 10 ) {
const token = await getPartnerJwt ( userEmail );
for ( let i = 0 ; i < maxAttempts ; i ++ ) {
const res = await fetch (
` ${ BASE_URL } /credentials/status?coreClaimHash= ${ encodeURIComponent ( coreClaimHash ) } ` ,
{ headers: { 'x-partner-auth' : token } }
);
const { vcStatus } = await res . json ();
if ( vcStatus === 'ONCHAIN' ) return true ;
await new Promise ( r => setTimeout ( r , 2000 )); // wait 2 s between polls
}
return false ;
}
Examples
VIP Status Portability — Issuer Airline loyalty app: issues tier credential; user carries status to partner brands.
VIP Status Portability — Verifier Hotel chain app: verifies tier and grants equivalent perks (e.g. room upgrade, lounge).
Next Steps
Issue on Behalf — Concepts Understand when and why to use server-side issuance.
Issue on Behalf — API Full endpoint reference with error codes.
Schema Use Cases More schema examples including membership and event pass.
Verifying Credentials SDK verification flow reference.