> ## 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.

# End-to-End Loyalty Credential Flow

> Issue tiered loyalty credentials from your backend when users cross spend or activity thresholds — portable Gold and Platinum tiers across partner platforms.

This recipe walks through issuing a portable loyalty credential that users carry across your partner ecosystem. When a user crosses a tier threshold (e.g. Gold, Platinum), your backend issues a new credential automatically.

## What you'll build

1. A credential schema that encodes loyalty tier, lifetime points, and membership date.
2. A backend function that issues (or upgrades) the credential when a user crosses a tier boundary.
3. Issuer-signed, holder-encrypted credentials stored in DStorage.

## Prerequisites

* [Direct Issuance](/airkit/usage/credential/direct-issuance) enabled for your partner account
* A published issuance program with a loyalty schema
* [Partner JWT](/airkit/usage/partner-authentication) signing configured
* Issuer signing keys available to your backend

## Step 1: Design the credential schema

Create a schema in the Developer Dashboard (Issuer > Schemas) with fields like:

| Field            | Type    | Description                                     |
| ---------------- | ------- | ----------------------------------------------- |
| `tier`           | string  | Loyalty tier name (e.g. `"Gold"`, `"Platinum"`) |
| `lifetimePoints` | integer | Total points accumulated                        |
| `memberSince`    | string  | ISO date when membership started                |
| `upgradedAt`     | string  | ISO date of the most recent tier upgrade        |

See [Schema Creation](/airkit/usage/credential/schema-creation) for full setup instructions.

## Step 2: Issue on tier upgrade

When your loyalty engine determines a user has crossed a tier boundary, resolve the recipient, then build, sign, encrypt, and store a fresh tier credential. Dedupe by recipient email + program ID and revoke the previous tier credential before reissuing.

<Warning>
  The recipient's email (passed to `initialize-user`) is the routing key that determines which AIR Account the credential lands in. Resolve it from the triggering event, and never reuse a partner, service, admin, or static email across recipients — every credential issued against that email lands in the same account.
</Warning>

```js theme={null}
const jwt = require("jsonwebtoken");
const fs = require("fs");
// issuer-controlled helpers backed by your signing keys
const { buildVc, signVc, encryptToHolder } = require("./lib/credential");

const privateKey = fs.readFileSync("path/to/private.key");
const BASE_URL = process.env.API_BASE_URL || "https://api.sandbox.mocachain.org/v1";

function getPartnerJwt() {
  const now = Math.floor(Date.now() / 1000);
  return jwt.sign(
    // No email claim — the recipient is passed to initialize-user, not the JWT
    { partnerId: process.env.PARTNER_ID, scope: "issue", iat: now, exp: now + 300 },
    privateKey,
    { algorithm: "RS256", header: { kid: process.env.KEY_ID, typ: "JWT" } }
  );
}

async function issueLoyaltyCredential(recipientEmail, tierData) {
  const token = getPartnerJwt();

  // 1. Resolve or create the recipient's AIR Account
  const initRes = await fetch(`${BASE_URL}/auth/initialize-user`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-partner-auth": token },
    body: JSON.stringify({ email: recipientEmail }),
  });
  if (!initRes.ok) throw new Error(`initialize-user failed: ${initRes.status}`);
  const { did, publicKey } = await initRes.json();

  // 2. Build, sign (BJJ_SIG_2021), and encrypt the credential to the holder
  const schemaId = process.env.LOYALTY_CREDENTIAL_ID;
  const vc = buildVc({
    holderDid: did,
    schemaId,
    credentialSubject: {
      tier: tierData.tier,
      lifetimePoints: tierData.lifetimePoints,
      memberSince: tierData.memberSince,
      upgradedAt: new Date().toISOString(),
    },
  });
  const encrypted = encryptToHolder(signVc(vc), publicKey);

  // 3. Store the encrypted envelope in DStorage
  const storeRes = await fetch(`${BASE_URL}/dstorage/vcs`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-partner-auth": token },
    body: JSON.stringify({
      holderDid: did,
      schemaId,
      expiresAt: vc.expirationDate,
      data: encrypted.encryptedData,
      iv: encrypted.iv,
      authTag: encrypted.authTag,
      encryptedKey: encrypted.dataEncPublicKey,
      externalId: vc.id,
    }),
  });
  if (!storeRes.ok) throw new Error(`dstorage/vcs failed: ${storeRes.status}`);
  return storeRes.json(); // { storagePath }
}
```

## Step 3: Trigger on purchase events

Wire the issuance into your purchase or activity pipeline:

```js theme={null}
async function onPurchaseComplete(userEmail, purchaseAmount) {
  const user = await getUserProfile(userEmail);
  const newPoints = user.lifetimePoints + calculatePoints(purchaseAmount);
  const newTier = resolveTier(newPoints);

  if (newTier !== user.currentTier) {
    const result = await issueLoyaltyCredential(userEmail, {
      tier: newTier,
      lifetimePoints: newPoints,
      memberSince: user.memberSince,
    });
    console.log(`Upgraded ${userEmail} to ${newTier}, storagePath: ${result.storagePath}`);
  }

  await updateUserProfile(userEmail, { lifetimePoints: newPoints, currentTier: newTier });
}
```

## Step 4: Let verifiers read the credential

Any partner in the ecosystem can verify the user's tier using [Credential Verification](/airkit/usage/credential/verify). The user presents the credential (via AIR Kit SDK), and the verifier confirms tier status without accessing underlying purchase data.

## Duplicate handling

Dedupe by recipient email + program ID before issuing so an event replay can't fan out duplicate credentials. When a user upgrades tiers, revoke the previous tier credential before storing the new one. For one-time membership issuance, skip reissuance if a credential already exists for the user + schema.

## Next steps

* [AIR for Loyalty](/airkit/guides/air-for-loyalty) for a broader integration guide
* [Issuance API Reference](/airkit/usage/credential/issuance-api) for endpoint details
* [Credential Verification](/airkit/usage/credential/verify) to set up cross-partner verification
