# AIR Kit Developer Dashboard
Source: https://docs.moca.network/airkit/airkit-dashboard
AIR Kit dashboard - manage account and credentials all in one place
The Developer Dashboard is a self-service platform for partners of the Moca Network.
It provides the tools for both developers of AIR Account and AIR Credential and has the following key sections:
* Account
* Issuer
* Verifiers
It is designed to manage the entire lifecycle of AIR Kits, from account creation to issuance to verification.
The dashboard is accessed via a connected EVM wallet, which serves as the primary account identifier, and allows users to access your app's settings.
## Account
The account section contains your partner configuration.
### 1. General Settings
This is where you can get a generated set of IDs that is used for your implementation. Below is an example of what your generated settings might look like, which you can easily copy from the dashboard.
```json theme={null}
{
"partnerId": "1234-5678-9012",
"issuerDid": "did:moca:0xabcdef1234567890abcdef1234567890abcdef",
"verifierDid": "did:moca:0x9876543210fedcba9876543210fedcba987654",
"name": "My Awesome dApp",
"logoUrl": "https://your-cdn.com/logo.png",
"websiteUrl": "https://your-dapp.com",
"jwksUrl": "https://your-auth-provider.com/.well-known/jwks.json"
}
```
| Field | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Partner ID** | Your AIR Kit partner ID, used to identify you across the AIR Kit ecosystem. |
| **Issuer DID** | Your Issuer DID, used to let users and verifiers know who is the issuer of the credentials. |
| **Verifier DID** | Your Verifier DID, used to let users and issuers know who requested usage of the credentials. |
| **Name** | Your App's name, which may be shown to users during various operations such as wallet transactions, credential issuance, or credential verification. |
| **Logo URL** | Your App's logo, which may be shown to users during various operations such as wallet transactions, credential issuance, or credential verification. |
| **Website URL** | Your App's website, which may be shown to users if they want to learn more about your application. |
| **JWKS URL** | Required for Credential Services, or if you utilize the [bring your own auth](/airkit/usage/user-authentication#custom-auth-bring-your-own-auth) feature. See [Authentication (Partner JWT)](/airkit/usage/partner-authentication) for details |
### 2. Allowed Domains
As a security setting, we require partners to set the app domains that are allowed to load AIR Kit. Wildcards are supported (e.g. \*.myapp.example.com)
You can add up to 3 domains or subdomains as you need. But do note that some domains may be blocked for security purposes and if you require these domains to be supported please reach out to our team to further discuss your requirements.
`localhost` is whitelisted for following ports: `3000`, `5173` and `8200`. We do not support adding `localhost` to the allowed domains list.
In addition, our production environment requires all domains to use HTTPS.
## Issuer
The Issuer section is designed for any entity that needs to create and distribute verifiable credentials. Its functionalities are organized into three main areas: Schemas, Credentials, and a monitoring Dashboard.
### 1. Schema Builder
This is the starting point for creating any new credential. A Schema acts as a blueprint, defining the structure, data type, and rules for a specific type of credential.
* **Create Schemas**: Issuers can build custom schemas by defining a title, version, and description.
* **Define Attributes**: Within each schema, issuers add specific data fields (attributes). For each attribute, they define a name, data type (e.g., string, number, boolean), a descriptive title, and can mark it as required.
* **Publish and Store**: Once defined, schemas are published and stored on either a centralized server (OSS) or decentralized storage (dStorage) on Moca Chain to be used for issuing credentials.
Here is an example of a simple schema for a "DAO Membership" credential:
```json theme={null}
{
"title": "DAO Membership",
"version": "1.0",
"description": "Verifies that the holder is a member of a specific DAO.",
"attributes": [
{
"name": "daoName",
"type": "string",
"title": "DAO Name",
"required": true
},
{
"name": "memberSince",
"type": "date",
"title": "Member Since",
"required": true
},
{
"name": "votingPower",
"type": "number",
"title": "Voting Power",
"required": false
}
]
}
```
### 2. Issuance Program
Once a schema is in place, issuers can create and configure the actual credentials that will be distributed to users.
* **Select a Schema**: The process begins by selecting the appropriate schema, which loads its predefined attributes.
* **Set Issuance Rules**: Issuers can configure key parameters for the credential, such as:
* **Accessible Until**: An optional date range during which the credential is valid.
* **Maximum Issuance**: An optional cap on the total number of credentials that can be claimed.
* **Expiration Duration**: The lifespan of the credential after it has been issued to a user (e.g., 90 days, 1 year, permanent).
* **Issue Credential**: After confirming the details, the credential is issued and becomes available for users to claim.
### 3. Issuer Dashboard Monitoring
This section provides a high-level overview and detailed logs of all issuance activity.
* **Key Metrics**: View statistics like the Total Issued Number (the total number of credentials claimed by users) and the Total Credential Number (the number of different credential types created).
* **Claim Records**: See a detailed list of every credential that has been claimed by a user, including the Holder ID, Credential ID, and the Claimed Time. Issuers can also revoke a claimed credential directly from this interface.
## Verifier
The Verifier section is for any application or service that needs to confirm the validity of a user's AIR Credential. It is centered around creating Verification Programs and monitoring their usage.
### 1. Verification Program Management
A Verification Program is a set of rules created by a verifier to check a user's credential for specific attributes.
* **Create Programs**: Verifiers build programs by defining a name and selecting the credential schema they intend to verify.
* **Define Verification Logic**: The core of the program is its logic. Verifiers add specific Operators (e.g., "equals," "greater than," "is a member of") and Attribute Values to create precise verification conditions. For example, a program could verify if a "Country" attribute equals "USA" or if an "Age" attribute is greater than "18".
* **Data Recovery Options**: Programs can be configured to allow verifiers to request access to the underlying data from the user, subject to their explicit consent.
Here is an example of verification logic to check if a user is a member of "Moca DAO":
```json theme={null}
{
"programName": "Moca DAO Member Check",
"credentialSchema": "DAO Membership",
"logic": [
{
"attribute": "daoName",
"operator": "equals",
"value": "Moca DAO"
}
],
"dataRecovery": false
}
```
### 2. Verifier Dashboard Monitoring
This dashboard provides a comprehensive record of all verification activities.
* **Key Metrics**: View the Total Verified Number of credentials and other relevant statistics.
* **Verification Records**: Access a detailed log of all verification attempts, including the Holder ID, the Program Name used, the Verified Time, and the Current Status (e.g., Passed, Verification Failed).
### 3. Settings and Fee Wallet
Both Issuers and Verifiers have access to a settings panel to manage their operations. For Verifiers, this includes a Fee Wallet system. Because issuance and verifications can incur on-chain gas fees, the dashboard provides a dedicated wallet that verifiers can pre-fund with \$MOCA. The system automatically deducts fees from this balance for verification transactions, and the dashboard provides a detailed history of all deposits and charges.
# Development Environments
Source: https://docs.moca.network/airkit/environments
Starting with **AirKit 1.8.0-beta**, the Sandbox environment defaults to **Moca Chain Testnet**.
**Breaking Change: Sandbox Now Defaults to Testnet**
Starting with **AirKit 1.8.0-beta**, the Sandbox environment defaults to **Moca Chain Testnet**.
Make sure you're selecting the correct chain when using different build environment. Otherwise, your app may hit unexpected errors
| Environment | Moca Chain | Purpose | Developer Dashboard |
| ----------------- | ---------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
| Sandbox (Testnet) | Testnet | For development & testing | [https://developers.sandbox.air3.com/dashboard](https://developers.sandbox.air3.com/dashboard) |
> Once Mainnet is available, Production credentials will be validated against Mainnet and hence previously issued credentials on Testnet will need to be re-issued. Issuance Schemas and Programs *may* be migrated. Your Issuer and Verifier DID may also change.
## Sandbox Environment Changes
Starting with **AirKit 1.8.0-beta**, the Sandbox environment defaults to **Moca Chain Testnet** instead of Devnet. Both Testnet and Devnet remain available for development, with Testnet being the recommended choice to align with Production.
### What's Changed
| Before (\< 1.8.0) | After (1.8.0-beta+) |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Sandbox → Devnet (Chain ID: 5151) | Sandbox → Testnet (Chain ID: 222888) |
| Dashboard: [https://developers.sandbox.air3.com](https://developers.sandbox.air3.com) | Dashboard: [https://developers.sandbox.air3.com/testnet](https://developers.sandbox.air3.com/testnet) |
### Impact
* Credentials issued on Devnet are **not** available on Testnet
* Smart Account addresses remain the same across networks
* You will need to re-issue test credentials on Testnet
We recommend migrating to Testnet as soon as possible to align with production.
### Initializing AirService with the correct Environment
```js theme={null}
import { AirService, BUILD_ENV } from "@mocanetwork/airkit";
const airService = new AirService({
partnerId: YOUR_PARTNER_ID
});
await airService.init({
buildEnv: BUILD_ENV.SANDBOX, // BUILD_ENV.PRODUCTION
enableLogging: true
});
```
```dart theme={null}
Future initialize({
YOUR_PARTNER_ID,
navigatorKey,
Environment.sandbox, // Environment.production
true,
})
```
## Chains
| Chain | Chain ID | RPC | Explorer |
| ------- | -------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Testnet | 222888 | [https://testnet-rpc.mocachain.org](https://testnet-rpc.mocachain.org) | [https://testnet-scan.mocachain.org](https://testnet-scan.mocachain.org) |
| Mainnet | 2288 | `coming soon` | `coming soon` |
# Flutter Android setup
Source: https://docs.moca.network/airkit/flutter/android-setup
Configure Android for AIR Kit Flutter: Digital Asset Links, manifest, network security, and build settings.
## Prerequisites
* Android Gradle Plugin and Kotlin versions supported by your Flutter template
* **`minSdk` 26** and **`compileSdk` 34** (required for AIR Kit)
## Asset statements (Digital Asset Links)
Create or edit `android/app/src/main/res/values/strings.xml`:
```xml theme={null}
[
{"include": "https://account.air3.com/.well-known/assetlinks.json"},
{"include": "https://account.sandbox.air3.com/.well-known/assetlinks.json"}
]
```
Reference the resource from `AndroidManifest.xml` inside ``:
```xml theme={null}
```
## App signing and allowlisting
Provide your app’s **SHA-256 signing certificate fingerprint** and **package name** to Moca for allowlisting (debug and release may differ). Use `keytool`:
```bash theme={null}
keytool -list -v -keystore -alias
```
## `minSdk` and `compileSdk`
In your app-level Gradle file (Kotlin DSL or Groovy), set at least:
```kotlin theme={null}
android {
compileSdk = 34
defaultConfig {
minSdk = 26
}
}
```
## Network security config
Optional: add `android/app/src/main/res/xml/network_security_config.xml` for TLS and (if needed) local dev domains:
```xml theme={null}
10.0.2.2
```
Point `` to it:
```xml theme={null}
android:networkSecurityConfig="@xml/network_security_config"
```
## Permissions
Typical requirements:
```xml theme={null}
```
## ProGuard / R8
If you minify release builds, add keep rules for AIR Kit, WebView, and crypto dependencies. **Verify package names** against your resolved dependencies after `flutter pub get`:
```proguard theme={null}
-keepattributes Signature,*Annotation*,EnclosingMethod,InnerClasses
# Adjust package patterns to match your app’s dependencies
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
```
## Verify the build
```bash theme={null}
flutter clean
flutter pub get
flutter build apk
```
## Troubleshooting
| Issue | What to check |
| ----------------------- | -------------------------------------------------------------------- |
| Passkey / domain errors | Asset statements JSON, signing cert vs allowlist, `meta-data` wiring |
| WebView blank / SSL | `networkSecurityConfig`, INTERNET permission |
| Gradle conflicts | `flutter clean`, `./gradlew clean` under `android/` |
More help: [Flutter troubleshooting](/airkit/flutter/troubleshooting) and [SDK issues](/airkit/troubleshooting/sdk-issues).
## Next steps
* [iOS setup](/airkit/flutter/ios-setup)
* [Google Sign-In](/airkit/flutter/google-signin) (optional)
* [SDK initialization](/airkit/usage/initialization)
## Reference
* [Digital Asset Links](https://developers.google.com/digital-asset-links/v1/getting-started)
* [Flutter Android deploy](https://docs.flutter.dev/deployment/android)
# Flutter Google Sign-In
Source: https://docs.moca.network/airkit/flutter/google-signin
AIR Kit can expose **Google** as a login method when your partner configuration enables it. Configure native Google Sign-In on each platform so the SDK can complete the OAuth flow.
## Prerequisites
* [Flutter installation](/airkit/flutter/installation) and platform setup ([Android](/airkit/flutter/android-setup), [iOS](/airkit/flutter/ios-setup))
* Google Cloud / Firebase project with OAuth client IDs for your Android package name and iOS bundle ID
* Google OAuth settings aligned with the [Developer Dashboard](https://developers.sandbox.air3.com/dashboard/general)
## How it connects to AIR Kit
Partner configuration supplies the Google OAuth client behavior expected by AIR Kit. Ensure the **same OAuth clients** you configure in Firebase are the ones you share with Moca for manual dashboard setup (see [Dashboard](#dashboard)).
## iOS
1. In [Firebase Console](https://console.firebase.google.com/), add an iOS app with your bundle ID.
2. Download `GoogleService-Info.plist` and add it to `ios/Runner/` in Xcode (copy if needed).
3. In `Info.plist`, add `CFBundleURLTypes` using the **`REVERSED_CLIENT_ID`** from `GoogleService-Info.plist` as the URL scheme (see Google’s Flutter / iOS Sign-In docs).
## Android
1. In Firebase, add an Android app with your **application ID** (package name).
2. Download `google-services.json` to `android/app/`.
3. In the **project** `android/build.gradle`, include the Google services classpath if required by your template.
4. In the **app** `android/app/build.gradle`, apply the Google Services plugin per [FlutterFire / Google Sign-In setup](https://pub.dev/packages/google_sign_in).
## Dashboard
Partners must provide the **Google OAuth client IDs** for iOS and Android (from Firebase / Google Cloud). The Developer Dashboard does **not** yet let partners enter these values themselves. Moca sets them **manually** for your partner, the same way as other allowlisted values (for example Android signing certificate fingerprints and iOS app identifiers — see [Android setup](/airkit/flutter/android-setup) and [iOS setup](/airkit/flutter/ios-setup)).
Enable Google as a login method in partner configuration once your client IDs are on file. If you need IDs added or updated, use your Moca / partner channel.
## Troubleshooting
| Symptom | Checks |
| ----------------------- | --------------------------------------------------------------------------- |
| `redirect_uri_mismatch` | iOS URL scheme matches `REVERSED_CLIENT_ID`; OAuth client matches bundle ID |
| Google button missing | Partner config; correct `partnerId` and environment |
| Android build errors | `google-services.json` path; package name; Play Services on device |
## Security
* Do not commit `GoogleService-Info.plist` or `google-services.json` to public repos if they contain secrets; use CI secrets or private config distribution.
## Next steps
* [User login & sessions](/airkit/usage/user-authentication)
* [Partner authentication](/airkit/usage/partner-authentication)
* [User management](/airkit/usage/user-management)
## Reference
* [Google Sign-In for Flutter](https://pub.dev/packages/google_sign_in)
* [Google Sign-In iOS](https://developers.google.com/identity/sign-in/ios/start)
* [Google Sign-In Android](https://developers.google.com/identity/sign-in/android/start)
# Flutter installation
Source: https://docs.moca.network/airkit/flutter/installation
Install the AIR Kit Flutter package with OnePub and configure minimum platform versions.
## Prerequisites
* Flutter SDK and Dart toolchain on your PATH
* OnePub access (credentials provided by Moca)
* Android: `compileSdk` 34+, `minSdk` 26+ (see [Android setup](/airkit/flutter/android-setup))
* iOS: platform 14.0+ in `Podfile` (see [iOS setup](/airkit/flutter/ios-setup))
## OnePub CLI
```bash theme={null}
dart pub global activate onepub
onepub login
```
If `onepub` is not found, add Pub’s global bin directory to your PATH, for example: `export PATH=$PATH:$HOME/.pub-cache/bin`
## Add the dependency
**Option A — `pubspec.yaml`**
```yaml theme={null}
dependencies:
airkit:
hosted: https://onepub.dev/api/sxhddavuhn/
version: ^1.6.0-beta.1
```
Use the version your team recommends; check [Release notes](/airkit/release-notes) for current Flutter SDK versions.
**Option B — CLI**
```bash theme={null}
onepub pub add airkit
```
Then run:
```bash theme={null}
flutter pub get
```
## Import
```dart theme={null}
import 'package:airkit/airkit.dart';
```
## Next steps
1. [Android setup](/airkit/flutter/android-setup) — asset statements, manifest, network security
2. [iOS setup](/airkit/flutter/ios-setup) — associated domains, `Podfile`
3. [Initialization](/airkit/usage/initialization) — `AirService` and `navigatorKey`
4. [Google Sign-In](/airkit/flutter/google-signin) if you use Google login
# Flutter iOS setup
Source: https://docs.moca.network/airkit/flutter/ios-setup
Configure iOS for AIR Kit Flutter: Associated Domains, Podfile, and passkey-related settings.
## Prerequisites
* Xcode and CocoaPods
* **iOS deployment target 14.0+** in `Podfile` (required for AIR Kit)
## Associated Domains
1. Open `ios/Runner.xcworkspace` in Xcode.
2. Select the **Runner** target → **Signing & Capabilities**.
3. Add **Associated Domains**.
4. Add the domains you use:
```
webcredentials:account.air3.com
webcredentials:account.sandbox.air3.com
```
Optional for non-production testing:
```
webcredentials:account.staging.air3.com
webcredentials:account.uat.air3.com
```
Include only environments you actually use. Production and sandbox are the usual minimum.
## App ID allowlisting
Provide Moca with your **Apple Team ID** and **bundle identifier** in the form `TEAMID.com.example.app`.
## Podfile
Set the platform version:
```ruby theme={null}
platform :ios, '14.0'
```
Then:
```bash theme={null}
cd ios
pod install
```
## Info.plist (optional)
If you need ATS exceptions for specific domains, configure them narrowly in `Info.plist`. Prefer default secure TLS where possible.
Optional usage strings if you add camera / photo features:
```xml theme={null}
NSCameraUsageDescription
Required for features that use the camera.
```
## Testing
* Passkeys and full WebAuthn behavior require a **physical device** in many cases.
* Build with `flutter build ios` and run from Xcode or `flutter run`.
## Troubleshooting
| Issue | What to check |
| ---------------------------- | --------------------------------------------------------------------------------- |
| Domain verification failures | Capability enabled, correct team, provisioning profile, domains spelled correctly |
| `pod install` errors | `rm -rf Pods Podfile.lock`, `pod repo update`, `pod install` |
| WebView / network | Associated Domains, device network, ATS settings |
See also [Flutter troubleshooting](/airkit/flutter/troubleshooting).
## Next steps
* [Google Sign-In](/airkit/flutter/google-signin) (optional)
* [SDK initialization](/airkit/usage/initialization)
* [User login & sessions](/airkit/usage/user-authentication)
## Reference
* [Associated Domains](https://developer.apple.com/documentation/xcode/supporting-associated-domains)
* [Flutter iOS deploy](https://docs.flutter.dev/deployment/ios)
# Flutter SDK overview
Source: https://docs.moca.network/airkit/flutter/overview
Integrate AIR Kit in Flutter apps for account services, wallet features, and native mobile flows.
## What to read first
1. [Flutter installation](/airkit/flutter/installation) — OnePub setup and `pubspec.yaml`
2. [Android setup](/airkit/flutter/android-setup) and [iOS setup](/airkit/flutter/ios-setup) — passkeys, asset links, associated domains
3. [SDK initialization](/airkit/usage/initialization) (Flutter tab) and [Reference](/airkit/usage/reference) (Flutter tab) for `AirService` APIs
## Capability summary
See the [platform support matrix](/airkit/platform-matrix) for supported platforms, browsers, and package installs.
Credential **issuance** and **verification** in the client SDK are available on Flutter. Start with [Issue credentials](/airkit/quickstart/issue-credentials), [Verify credentials](/airkit/quickstart/verify-credentials), [Issuing credentials](/airkit/usage/credential/issuing-credentials), and [Verify](/airkit/usage/credential/verify). Server-side [Issue on Behalf](/airkit/usage/credential/issue-on-behalf-api) works from any backend.
## Authentication on Flutter
On Flutter, sign-in uses `login` with a **Partner JWT** (`authToken`) after `initialize()`. See [User login & sessions](/airkit/usage/user-authentication) (Flutter tab) and [Partner authentication](/airkit/usage/partner-authentication).
## Environment and dashboard
Use the [Developer Dashboard](https://developers.sandbox.air3.com/dashboard) for partner ID, programs, and configuration. Environment values match [Environments](/airkit/environments) (`Environment.sandbox`, `production`, etc.).
## Related guides
* [Google Sign-In](/airkit/flutter/google-signin) — Firebase / platform OAuth setup
* [Wallet operations](/airkit/flutter/wallet-operations) — balances, transactions, signing
* [Provider functions](/airkit/flutter/provider-functions) — JSON-RPC via `AirService`
* [UI components](/airkit/flutter/ui-components) — login, swap, on-ramp, MFA
* [Troubleshooting](/airkit/flutter/troubleshooting) — common Flutter issues
# Flutter provider functions
Source: https://docs.moca.network/airkit/flutter/provider-functions
Call Ethereum JSON-RPC methods through AirService on Flutter.
On Flutter, `AirService.sendEthereumRpcRequest` exposes an **EIP-1193-style** surface for common RPC methods. Use it directly or wrap it for libraries like **web3dart**.
**wagmi** is for web React apps. For wagmi, see [Wagmi integration](/recipes/wagmi-integration) and the Web SDK [provider](/airkit/usage/account/provider-functions) docs.
## Request and response shapes
See [Reference](/airkit/usage/reference) (Flutter tab) for `EthereumRpcRequest` and `EthereumRpcSuccessResponse`.
## Examples
### `eth_accounts` and `eth_chainId`
```dart theme={null}
final accountsRes = await airService.sendEthereumRpcRequest(
EthereumRpcRequest(method: 'eth_accounts', params: []),
);
final chainRes = await airService.sendEthereumRpcRequest(
EthereumRpcRequest(method: 'eth_chainId', params: []),
);
```
### `eth_getBalance`
```dart theme={null}
final address = await airService.getAbstractAccountAddress();
if (address != null) {
final res = await airService.sendEthereumRpcRequest(
EthereumRpcRequest(
method: 'eth_getBalance',
params: [address, 'latest'],
),
);
}
```
### Switch or add chain
```dart theme={null}
await airService.sendEthereumRpcRequest(
EthereumRpcRequest(
method: 'wallet_switchEthereumChain',
params: [
{'chainId': '0x1'},
],
),
);
```
## web3dart
You can implement a thin adapter that forwards `JsonRpc` calls to `sendEthereumRpcRequest`. Keep the adapter in your app repo and test against your SDK version.
## Next steps
* [Wallet operations](/airkit/flutter/wallet-operations)
* [Supported chains](/airkit/usage/account/supported-chains)
# Flutter troubleshooting
Source: https://docs.moca.network/airkit/flutter/troubleshooting
Common fixes for AIR Kit Flutter integration: install, platform config, login, and WebView issues.
## Installation and OnePub
| Issue | What to try |
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `onepub` command not found | Add `$HOME/.pub-cache/bin` to PATH |
| Package resolve failures | `onepub login`, verify hosted URL and version in [Flutter installation](/airkit/flutter/installation) |
## Android
| Issue | What to try |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Passkey / asset links | [Android setup](/airkit/flutter/android-setup): `strings.xml`, manifest `meta-data`, signing cert allowlisting |
| `minSdk` / manifest merge errors | Set `minSdk` 26+, `compileSdk` 34+ |
## iOS
| Issue | What to try |
| ------------------ | ----------------------------------------------------------------------------------------- |
| Associated Domains | [iOS setup](/airkit/flutter/ios-setup): capability, exact domain entries, physical device |
| CocoaPods | `cd ios && rm -rf Pods Podfile.lock && pod install` |
## Runtime
| Issue | What to try |
| ---------------------------- | -------------------------------------------------------------------- |
| Login UI does not appear | `navigatorKey` passed to `MaterialApp` and `initialize()` |
| `Service is not initialized` | Await `initialize()` before other calls |
| WebView blank | Platform network config, ATS (iOS), Associated Domains / asset links |
## Credentials on Flutter
Credential **issuance** and **verification** are available on Flutter. Use the [Issue credentials](/airkit/quickstart/issue-credentials) and [Verify credentials](/airkit/quickstart/verify-credentials) quickstarts, plus [Issuing credentials](/airkit/usage/credential/issuing-credentials) and [Verify](/airkit/usage/credential/verify).
If calls fail or payloads are rejected, see [Credential issues](/airkit/troubleshooting/credential-issues). For Google OAuth client IDs and dashboard-side setup, see [Google Sign-In — Dashboard](/airkit/flutter/google-signin#dashboard).
## More help
* [SDK issues](/airkit/troubleshooting/sdk-issues) — includes Flutter-specific notes
* [Common errors](/airkit/troubleshooting/common-errors)
* Support via your Moca / partner channel
# Flutter UI components
Source: https://docs.moca.network/airkit/flutter/ui-components
Built-in AIR Kit UI on Flutter: login, swap, on-ramp, and MFA.
AIR Kit exposes **native Flutter UI** for login and several **hosted UI flows** through `AirService`. APIs below match the [Reference](/airkit/usage/reference) Flutter tab.
## Prerequisites
* [Initialization](/airkit/usage/initialization) with a valid `GlobalKey` on `MaterialApp`
* For **Swap**, **On-ramp**, and **MFA** below: an authenticated session after `login`
* Optional: `preloadWallet()` before opening wallet UIs for smoother first open
## Login
`login` presents **native Flutter UI** for the configured login methods. After `initialize()`, call `login` with your **Partner JWT** (`authToken`) as described in [User login & sessions](/airkit/usage/user-authentication) (Flutter tab).
## Swap UI
```dart theme={null}
await airService.showSwapUi();
```
## On-ramp UI
```dart theme={null}
await airService.showOnRampUi(
displayCurrencyCode: 'USD',
targetCurrencyCode: 'ETH', // optional
);
```
## MFA setup
```dart theme={null}
await airService.setupOrUpdateMfa();
```
Check status via `getUserInfo()` → `user.isMFASetup` (see Reference models).
## Theming, locale, and currency
Web SDK uses `sessionConfig` / `updateSessionConfig` on `init`. For Flutter, confirm behavior in your SDK version and [Release notes](/airkit/release-notes). Dashboard and partner configuration also affect login and widget appearance — see [Theming](/airkit/usage/config-theming) and [Language & currency](/airkit/usage/config-language) for product-level options.
## Web-first built-in UI
Some Account Services UIs documented under [Built-in UI](/airkit/usage/account/builtin-ui) are Web-first; Flutter parity varies by release. Confirm APIs and behavior in the [Reference](/airkit/usage/reference) Flutter tab and [Release notes](/airkit/release-notes).
## Next steps
* [Wallet operations](/airkit/flutter/wallet-operations)
* [User login & sessions](/airkit/usage/user-authentication)
# Flutter wallet operations
Source: https://docs.moca.network/airkit/flutter/wallet-operations
After [initialization](/airkit/usage/initialization) and login, use `AirService` for embedded wallet operations. Method names match the [Reference](/airkit/usage/reference) Flutter tab.
## Prerequisites
* `await airService.initialize(...)` completed
* User authenticated (see [User login & sessions](/airkit/usage/user-authentication))
* Optional: `await airService.preloadWallet()` before first wallet UI or heavy RPC use
## Accounts and address
```dart theme={null}
final accounts = await airService.getAccounts();
final address = await airService.getAbstractAccountAddress();
```
## Balances
```dart theme={null}
final address = await airService.getAbstractAccountAddress();
if (address != null) {
final balanceWei = await airService.getBalance(address);
// Format for display (example: 18 decimals)
}
```
## Sign messages
```dart theme={null}
final signature = await airService.signMessage('Hello from AIR Kit');
```
## Contract reads (`call`)
```dart theme={null}
final result = await airService.call(
'0xContractAddress',
'balanceOf',
[/* parameters */],
erc20AbiJsonString,
);
```
## Transactions (`sendTransaction`)
Use `web3dart` `Transaction` / `Transaction.callContract` types as in your SDK version:
```dart theme={null}
import 'package:web3dart/web3dart.dart';
// Example shape — adjust gas, chain, and contract types to your app
final txHash = await airService.sendTransaction(transaction);
```
## Smart account deployment
```dart theme={null}
final deployed = await airService.isSmartAccountDeployed();
if (!deployed) {
final txHash = await airService.deploySmartAccount();
}
```
## Built-in wallet UI
* Swap: `await airService.showSwapUi();`
* On-ramp: `await airService.showOnRampUi(displayCurrencyCode: 'USD');`
See [UI components](/airkit/flutter/ui-components).
## Error handling
Catch `AirKitException` and inspect `type` (`client`, `sdk`, `server`, `unknown`) for logging and user messaging.
## Next steps
* [Provider functions](/airkit/flutter/provider-functions) — raw JSON-RPC
* [Account Services](/airkit/usage/account/smart-account) — product concepts (web-centric sections may still apply conceptually)
* [Paymaster](/airkit/usage/account/paymaster) — gas sponsorship where supported
# AIR for Advertising & Audiences
Source: https://docs.moca.network/airkit/guides/air-for-advertising
Build verified, consent-based audience segments without third-party cookies. Let advertisers buy access to cryptographically proven user attributes — no raw data leaves user accounts.
Post-cookie advertising is broken in one specific way: every replacement (contextual, cohort, data clean rooms) still requires someone to hold raw user data and vouch for it. AIR Kit takes a different approach — **users hold their own verified attribute credentials**, and publishers verify them at ad-serve time via ZK proof. The advertiser gets a signal they can trust. No data broker in the middle. No raw data leaves the user's account.
## What You Can Build
* **Verified audience segments** — Issue demographic and interest credentials on profile completion or KYC; advertisers target against cryptographic proof, not self-reported fields
* **Consent-portable targeting** — User consents once; that consent credential is verifiable by any publisher in the ecosystem without a consent management platform re-prompt
* **Age-verified ad targeting** — Gate alcohol, gambling, and adult ad categories behind a ZK age proof — no DOB ever transmitted to the ad server
* **Cross-publisher identity** — A user's verified attributes follow them across publisher sites without third-party cookies or device fingerprinting
* **Bot and fraud resistance** — Issue a "verified human" credential after KYC or passkey authentication; DSPs and SSPs can filter on-credential instead of on-signal
* **Brand safety compliance** — Verify user age and location at serve time without storing the data, satisfying COPPA, GDPR Article 8, and regional ad regulations
## Architecture
```mermaid theme={null}
graph TD
A["User completes profile / KYC\non your platform"] --> B[Your Backend]
B -->|Issue on Behalf API| C[AIR Kit / Moca Chain]
C --> D["Audience Credentials\nin User's AIR Account"]
D --> E{Ad serve request}
E -->|Publisher verifies| F["ZK proof: age ✓ location ✓\ninterest segment ✓"]
F --> G[DSP receives verified segment signal]
G --> H[Targeted ad served]
D --> I{Cross-publisher visit}
I -->|Any partner publisher| J["Same credentials verified\nno cookie / fingerprint needed"]
```
## Recommended Schemas
### Verified Audience Segment
```json theme={null}
{
"title": "Audience Segment",
"description": "Publisher-verified user audience attributes — no raw PII included",
"properties": {
"publisherId": {
"type": "string",
"description": "Issuing publisher identifier"
},
"ageRange": {
"type": "string",
"enum": ["18-24", "25-34", "35-44", "45-54", "55+"],
"description": "Verified age bracket — never the actual age or DOB"
},
"isOver18": {
"type": "boolean"
},
"isOver21": {
"type": "boolean"
},
"countryCode": {
"type": "string",
"description": "ISO 3166-1 alpha-2 — verified registration country"
},
"interestSegments": {
"type": "array",
"items": { "type": "string" },
"description": "IAB content categories — e.g. ['IAB19', 'IAB9'] — derived from behaviour, not declared"
},
"verifiedAt": {
"type": "string",
"format": "date-time"
}
},
"required": ["publisherId", "isOver18", "countryCode", "verifiedAt"]
}
```
### Consent Credential
```json theme={null}
{
"title": "Ad Consent",
"description": "Verifiable record of user consent for personalised advertising",
"properties": {
"publisherId": { "type": "string" },
"consentedTo": {
"type": "array",
"items": {
"type": "string",
"enum": ["personalised_ads", "cross_site_tracking", "interest_inference"]
}
},
"consentVersion": {
"type": "string",
"description": "CMP policy version the user consented to"
},
"consentedAt": {
"type": "string",
"format": "date-time"
},
"expiresAt": {
"type": "string",
"format": "date-time",
"description": "Consent expiry — re-issue when user re-confirms"
}
},
"required": ["publisherId", "consentedTo", "consentVersion", "consentedAt"]
}
```
Never include name, email address, IP address, device ID, or precise location in `credentialSubject`. Use only **derived, aggregated attributes** — `ageRange`, `countryCode`, `interestSegments`. The ZK proof lets the ad server confirm `isOver18 === true` without ever receiving the subscriber's date of birth.
## Implementation
### Step 1 — Issue audience credentials on profile completion
```javascript theme={null}
// profile-complete-handler.js
const { getPartnerJwt } = require('./lib/jwt');
const BASE_URL = 'https://api.sandbox.mocachain.org/v1';
async function issueAudienceCredential({ userEmail, profile }) {
const token = await getPartnerJwt(userEmail);
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,
credentialId: process.env.AUDIENCE_CREDENTIAL_ID,
credentialSubject: {
publisherId: process.env.PUBLISHER_ID,
ageRange: getAgeRange(profile.birthYear), // "25-34" — NOT the DOB
isOver18: profile.age >= 18,
isOver21: profile.age >= 21,
countryCode: profile.countryCode,
interestSegments: profile.iabSegments, // e.g. ['IAB19', 'IAB9-30']
verifiedAt: new Date().toISOString(),
},
onDuplicate: 'revoke', // refresh when profile or interests update
}),
});
if (!res.ok) throw new Error(`Audience credential issuance failed: ${res.status}`);
return res.json();
}
function getAgeRange(birthYear) {
const age = new Date().getFullYear() - birthYear;
if (age < 25) return '18-24';
if (age < 35) return '25-34';
if (age < 45) return '35-44';
if (age < 55) return '45-54';
return '55+';
}
// Fire on profile completion and on interest segment updates
userEvents.on('profile:completed', ({ userEmail, profile }) =>
issueAudienceCredential({ userEmail, profile })
);
userEvents.on('interests:updated', ({ userEmail, profile }) =>
issueAudienceCredential({ userEmail, profile })
);
```
### Step 2 — Issue consent credential when user accepts CMP
```javascript theme={null}
// consent-handler.js
async function issueConsentCredential({ userEmail, consentRecord }) {
const token = await getPartnerJwt(userEmail);
const expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1); // 1-year consent window
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,
credentialId: process.env.CONSENT_CREDENTIAL_ID,
credentialSubject: {
publisherId: process.env.PUBLISHER_ID,
consentedTo: consentRecord.purposes, // ["personalised_ads", ...]
consentVersion: consentRecord.policyVersion,
consentedAt: new Date().toISOString(),
expiresAt: expiresAt.toISOString(),
},
onDuplicate: 'revoke', // replace on re-consent or withdrawal
}),
});
}
// Wire into your CMP callback
cmp.on('consent:granted', ({ userEmail, purposes, policyVersion }) =>
issueConsentCredential({
userEmail,
consentRecord: { purposes, policyVersion },
})
);
// Revoke consent credential on withdrawal
cmp.on('consent:withdrawn', async ({ userEmail }) => {
// Issue credential with empty consentedTo to effectively invalidate
// Or call the revoke endpoint directly — see Issue on Behalf API reference
});
```
### Step 3 — Verify audience attributes at ad-serve time
```javascript theme={null}
// ad-server-verify.js (publisher ad server / prebid adapter)
import { AirService } from '@mocanetwork/airkit';
import { AirService, BUILD_ENV } from "@mocanetwork/airkit";
const airService = new AirService({ partnerId: process.env.PUBLISHER_PARTNER_ID });
await airService.init({ buildEnv: BUILD_ENV.PRODUCTION });
async function getVerifiedAudienceSignal() {
// Check age compliance for restricted category
const ageResult = await airService.verifyCredential({
programId: process.env.AGE_18_VERIFY_PROGRAM_ID,
// Program rule: isOver18 === true
// Verifier receives only: COMPLIANT / NON_COMPLIANT
});
// Check consent for personalised ads
const consentResult = await airService.verifyCredential({
programId: process.env.CONSENT_VERIFY_PROGRAM_ID,
// Program rule: "personalised_ads" in consentedTo AND expiresAt > now
});
return {
ageVerified: ageResult.status === 'COMPLIANT',
consentVerified: consentResult.status === 'COMPLIANT',
// Pass these as verified signals to your DSP bid request
// No raw age, no raw consent record — just provable booleans
};
}
// Enrich bid request with verified signals
async function buildBidRequest(adSlot, user) {
const signals = await getVerifiedAudienceSignal();
return {
...adSlot,
user: {
...user,
ext: {
airkit: {
ageVerified: signals.ageVerified,
consentVerified: signals.consentVerified,
// DSP can bid higher on verified signals vs. unverified
},
},
},
};
}
```
### Step 4 — Cross-publisher audience verification (partner publisher)
Partner publishers integrate AIR Kit as a verifier. One SDK call replaces the cookie sync / data clean room round-trip.
```javascript theme={null}
// partner-publisher.js
import { AirService } from '@mocanetwork/airkit';
// The PARTNER publisher uses their own Partner ID
const airService = new AirService({ partnerId: 'PARTNER_PUBLISHER_PARTNER_ID' });
await airService.init({ buildEnv: BUILD_ENV.PRODUCTION });
async function verifyAudienceForPartner() {
const result = await airService.verifyCredential({
programId: process.env.CROSS_PUB_AUDIENCE_VERIFY_PROGRAM_ID,
// Program checks: isOver18 === true AND countryCode in allowed list
// The partner publisher never receives the user's raw profile data
// They get: COMPLIANT / NON_COMPLIANT — that's it
});
return result.status === 'COMPLIANT';
}
```
## Key Patterns
| Pattern | `onDuplicate` | When to Use |
| --------------------------- | :------------: | ----------------------------------------- |
| Initial audience credential | `"ignore"` | Issue once on first profile completion |
| Interest segment refresh | `"revoke"` | Monthly or on significant interest drift |
| Consent granted | `"revoke"` | Always replace with latest consent record |
| Consent withdrawn | Revoke via API | Immediately invalidate consent credential |
| KYC-backed audience | `"revoke"` | Re-issue when KYC result updates |
## Privacy Guarantee
No raw user data — no DOB, no email, no IP, no browsing history — is transmitted during ad verification. The ZK proof flow means the verifier (DSP, SSP, partner publisher) receives only boolean results.
| What the Verifier Sees | What Stays Private |
| --------------------------- | ----------------------------- |
| `COMPLIANT / NON_COMPLIANT` | Date of birth, exact age |
| `ageRange` (`25-34`) | Full name, email address |
| `isOver18: true` | IP address, device ID |
| `countryCode` (`AU`) | Precise location |
| IAB segment IDs | Browsing history, clickstream |
| Consent expiry date | Declared interests |
## Regulatory Alignment
* **GDPR Article 8** — Age verification without storing DOB satisfies child protection obligations
* **CCPA / CPRA** — Consent credential provides an auditable, user-owned record of opt-in; revocation is instant and on-chain
* **COPPA** — ZK age gate (isOver18 === false blocks serve) without collecting minor data
* **TCF 2.2** — Consent credentials can encode TCF purpose IDs; verifiers check compliance without a centralised CMP dependency
## Examples
The repo uses a venue check-in and merch store; the same event-triggered pattern applies to ad engagement (publisher issues, ad server verifies). Adapt via schema and branding — see the README's "Adapting to Your Vertical" section.
Venue check-in app: issues attendance credential when the fan scans in at the event.
Merch store app: verifies attendance and unlocks rewards (e.g. discount, exclusive offer).
## Next Steps
Server-side issuance without user presence.
Full endpoint reference with error codes.
KYC portability and age verification patterns.
More credential schema examples.
# AIR for Fintech & Payments
Source: https://docs.moca.network/airkit/guides/air-for-fintech
Issue KYC credentials, income proofs, and compliance attestations without storing PII. Enable privacy-preserving financial verification with AIR Kit.
Traditional KYC and compliance verification requires every platform to collect, store, and protect sensitive personal data — creating liability, compliance risk, and repeated friction for users. AIR Kit lets you **issue a KYC credential once**, and any partner platform can verify it via zero-knowledge proof without ever accessing raw PII.
## What You Can Build
* **KYC credentials** — Issue a verified identity attestation the moment a user passes KYC; partner platforms accept it without re-running the check
* **Income / accreditation proofs** — Issue an "Accredited Investor" credential backed by income verification, provable via ZK proof
* **Compliance gating** — Gate DeFi pools, financial products, or high-value transactions behind credential checks
* **Age verification** — Prove a user is 18+ or 21+ without revealing their birthdate to the verifier
* **Cross-border compliance** — One credential, accepted at every partner service in the ecosystem
## Architecture
```mermaid theme={null}
graph TD
A["User completes KYC\nwith your KYC provider"] --> B["KYC Provider webhook / callback"]
B --> C[Your Backend]
C -->|Issue on Behalf API| D[AIR Kit / Moca Chain]
D --> E[KYC Credential in User's AIR Account]
E --> F{User accesses financial product}
F -->|Your platform| G["Verify KYC credential\nNo PII stored or transferred"]
F -->|Partner DeFi protocol| H["ZK proof: user is KYC'd"]
F -->|Cross-border service| I[Compliance check passed]
```
## Recommended Schema
### KYC Attestation
```json theme={null}
{
"title": "KYC Attestation",
"description": "Identity verification attestation — no raw PII included",
"properties": {
"kycLevel": {
"type": "string",
"enum": ["basic", "enhanced", "institutional"],
"description": "Level of KYC verification completed"
},
"kycProvider": {
"type": "string",
"description": "Name of the KYC provider (e.g. 'Jumio', 'Onfido')"
},
"verifiedAt": {
"type": "string",
"format": "date-time"
},
"countryCode": {
"type": "string",
"description": "ISO 3166-1 alpha-2 country code"
},
"isOver18": {
"type": "boolean"
},
"isOver21": {
"type": "boolean"
}
},
"required": ["kycLevel", "kycProvider", "verifiedAt", "isOver18"]
}
```
Never include raw PII — full name, passport number, date of birth, address — in `credentialSubject`. Store only **attestations and derived facts**. ZK proofs let verifiers confirm `isOver18 === true` without seeing any underlying data.
## Implementation
### Step 1 — Issue KYC credential from your KYC webhook
```javascript theme={null}
// kyc-webhook.js — called when your KYC provider sends a completion event
const { getPartnerJwt } = require('./lib/jwt');
async function issueKycCredential({ userEmail, kycResult }) {
if (kycResult.status !== 'APPROVED') return; // only issue on success
const token = await getPartnerJwt(userEmail);
const res = await fetch('https://api.sandbox.mocachain.org/v1/credentials/issue-on-behalf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-partner-auth': token,
},
body: JSON.stringify({
issuerDid: process.env.ISSUER_DID,
credentialId: process.env.KYC_CREDENTIAL_ID,
credentialSubject: {
kycLevel: kycResult.level, // "basic" | "enhanced" | "institutional"
kycProvider: 'YourKYCProvider',
verifiedAt: new Date().toISOString(),
countryCode: kycResult.countryCode, // e.g. "US", "GB"
isOver18: kycResult.age >= 18,
isOver21: kycResult.age >= 21,
},
onDuplicate: 'revoke', // re-issue if KYC level upgrades
}),
});
if (!res.ok) throw new Error(`KYC credential issuance failed: ${res.status}`);
return res.json();
}
// Express webhook endpoint
app.post('/webhooks/kyc', async (req, res) => {
const { userEmail, kycResult } = req.body;
await issueKycCredential({ userEmail, kycResult });
res.json({ ok: true });
});
```
### Step 2 — Gate financial products with credential verification
```javascript theme={null}
// fintech-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 });
async function requireKyc() {
const result = await airService.verifyCredential({
programId: process.env.KYC_VERIFY_PROGRAM_ID,
// Verifier program rule: kycLevel is "enhanced" or "institutional"
});
if (result.status !== 'COMPLIANT') {
throw new Error('KYC_REQUIRED');
}
return result;
}
// Gate a high-value transaction
try {
await requireKyc();
proceedWithTransaction();
} catch (err) {
if (err.message === 'KYC_REQUIRED') showKycOnboarding();
}
```
### Step 3 — Age verification gate (no birthdate exposed)
```javascript theme={null}
// age-gate.js (frontend)
async function requireAgeVerification() {
const result = await airService.verifyCredential({
programId: process.env.AGE_18_VERIFY_PROGRAM_ID,
// Program checks: isOver18 === true — verifier never sees the actual age/DOB
});
return result.status === 'COMPLIANT';
}
```
## Privacy Guarantee
The ZK proof flow means the verifier receives **only a boolean result**. No raw KYC data, no PII, no liability for the verifier to store sensitive documents.
| What the Verifier Sees | What Stays Private |
| -------------------------------- | ------------------------------- |
| `COMPLIANT / NON_COMPLIANT` | Full name, passport / ID number |
| Credential expiry date | Date of birth |
| KYC level (`basic` / `enhanced`) | Residential address |
| Issuer DID | Income figures, net worth |
| `isOver18: true` | Actual age |
## Examples
KYC provider app: issues credentials once; user carries the proof to other platforms.
Lending platform app: verifies the credential and accepts the proof without re-running KYC.
Identity provider app: issues age credential; user proves 18+ without revealing date of birth.
iGaming platform app: verifies age-gate (e.g. 18+) via ZK proof; no PII received.
## Next Steps
Server-side issuance without user presence.
ZK proof flow end-to-end.
JWT signing setup for your backend.
SDK verification reference.
# AIR for Gaming & NFTs
Source: https://docs.moca.network/airkit/guides/air-for-gaming
Port player identities, achievements, and badges across games and chains. Issue cross-game credentials that follow players wherever they play.
In gaming, players rebuild their reputation from zero every time they switch titles. AIR Kit lets you **make achievements, badges, and identities portable** — a player's rank in your game becomes a credential they carry to partner games, tournaments, communities, and airdrops.
## What You Can Build
* **Achievement badges** — Issue on-chain proof that a player reached Diamond rank, completed a raid, or earned a title
* **Cross-game identity** — Let players carry their AIR Account identity and credentials into any game in the Moca ecosystem
* **Anti-cheat / bot gating** — Verify a player holds a "verified human" or KYC credential before allowing tournament entry
* **Airdrop gating** — Reward only players who hold specific credentials (e.g. "played > 100 hrs", "held NFT for 90 days")
* **Guild / DAO membership** — Issue guild membership credentials that gate Discord roles, in-game benefits, and governance votes
## Architecture
```mermaid theme={null}
graph TD
A["In-game event\ne.g. rank achieved, raid completed"] --> B[Game Server]
B -->|Issue on Behalf API| C[AIR Kit / Moca Chain]
C --> D[Achievement Credential in Player's AIR Account]
D --> E{Player uses credential}
E --> F[Display badge in game profile]
E --> G[Enter partner game tournament]
E --> H[Claim ecosystem airdrop]
E --> I[Access guild Discord role]
```
## Recommended Schema
```json theme={null}
{
"title": "Game Achievement",
"description": "On-chain proof of a player achievement or rank",
"properties": {
"gameId": {
"type": "string",
"description": "Unique identifier of the game"
},
"achievementId": {
"type": "string",
"description": "Unique identifier of the achievement"
},
"achievementName": {
"type": "string",
"description": "Display name — e.g. 'Diamond Rank Season 3'"
},
"tier": {
"type": "string",
"enum": ["Bronze", "Silver", "Gold", "Platinum", "Diamond"],
"description": "Achievement tier"
},
"earnedAt": {
"type": "string",
"format": "date-time"
},
"season": {
"type": "string",
"description": "Game season, e.g. 'Season 3'"
}
},
"required": ["gameId", "achievementId", "achievementName", "earnedAt"]
}
```
## Implementation
### Step 1 — Issue an achievement credential on rank-up
```javascript theme={null}
// game-server.js
const { getPartnerJwt } = require('./lib/jwt');
async function issueAchievement({ playerEmail, achievement }) {
const token = await getPartnerJwt(playerEmail);
const res = await fetch('https://api.sandbox.mocachain.org/v1/credentials/issue-on-behalf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-partner-auth': token,
},
body: JSON.stringify({
issuerDid: process.env.ISSUER_DID,
credentialId: process.env.ACHIEVEMENT_CREDENTIAL_ID,
credentialSubject: {
gameId: process.env.GAME_ID,
achievementId: achievement.id,
achievementName: achievement.name,
tier: achievement.tier,
earnedAt: new Date().toISOString(),
season: 'Season 3',
},
onDuplicate: 'ignore', // achievements are one-time — don't re-issue
}),
});
if (!res.ok) throw new Error(`Achievement issuance failed: ${res.status}`);
return res.json();
}
// Wire into your game's rank-up event system
gameEvents.on('player:rank_up', async ({ playerEmail, newRank }) => {
await issueAchievement({
playerEmail,
achievement: {
id: `rank-${newRank.toLowerCase()}`,
name: `${newRank} Rank`,
tier: newRank,
},
});
console.log(`Achievement issued to ${playerEmail}: ${newRank}`);
});
```
### Step 2 — Gate tournament entry with credential check
```javascript theme={null}
// tournament-gate.js (frontend or server)
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 });
async function canJoinTournament() {
const result = await airService.verifyCredential({
programId: process.env.TOURNAMENT_VERIFY_PROGRAM_ID,
// Verifier program rule: tier === "Diamond" AND gameId === process.env.GAME_ID
});
return result.status === 'COMPLIANT';
}
const eligible = await canJoinTournament();
if (!eligible) {
showMessage('Reach Diamond rank to enter this tournament.');
}
```
### Step 3 — Cross-game SSO with AIR Account
Partner games authenticate using the same AIR Account — no new signup required.
```javascript theme={null}
// partner-game.js (any game integrating AIR Kit)
import { AirService } from '@mocanetwork/airkit';
const airService = new AirService({ partnerId: 'PARTNER_GAME_PARTNER_ID' });
await airService.init({ buildEnv: BUILD_ENV.SANDBOX });
await airService.login(); // player logs in with existing AIR Account
const user = await airService.getUserInfo();
// user.email is the same across all games — portable, unified identity
```
### Step 4 — Bot-resistant airdrop gating
Only distribute tokens to players who provably hold a real game achievement — filtered on-chain.
```javascript theme={null}
// airdrop.js (server-side)
async function getEligiblePlayers(allPlayers) {
const eligible = [];
for (const { email } of allPlayers) {
const token = await getPartnerJwt(email);
// Use your verifier program to check credential status per player
// In production, batch this with your own DB query first to reduce API calls
const res = await fetch(
`https://api.sandbox.mocachain.org/v1/credentials/status?...`,
{ headers: { 'x-partner-auth': token } }
);
const { vcStatus } = await res.json();
if (vcStatus === 'ONCHAIN') eligible.push(email);
}
return eligible;
}
```
## Examples
Identity provider app: issues age credential; user proves 18+ without revealing date of birth.
iGaming or age-gated app: verifies age via ZK proof and grants access; no PII received.
## Next Steps
Full endpoint reference and error codes.
User-initiated issuance for in-game claim flows.
More schema examples including event pass.
Attendance credentials for live events.
# AIR for Loyalty & Rewards
Source: https://docs.moca.network/airkit/guides/air-for-loyalty
Build portable, verifiable loyalty programs using AIR Kit credential services. Issue tier badges, points milestones, and membership credentials that travel with users across platforms.
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
```mermaid theme={null}
graph TD
A["User Action\ne.g. purchase, referral, check-in"] --> B[Your Loyalty Engine]
B -->|Milestone reached| C[Issue on Behalf API]
C --> D[AIR Kit / Moca Chain]
D --> E[Loyalty Credential in User's AIR Account]
E --> F{User presents credential}
F -->|At your app| G[Tier unlock / reward]
F -->|At partner app| H[Cross-brand benefit]
F -->|At any verifier| I[Gated access / discount]
```
## Recommended Schema
Create this schema in the [AIR Kit Dashboard](https://developers.sandbox.air3.com/dashboard) under **Issuer → Schema Builder**.
```json theme={null}
{
"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.
```javascript theme={null}
// 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.
```javascript theme={null}
// 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` value | When 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.
```javascript theme={null}
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
Airline loyalty app: issues tier credential; user carries status to partner brands.
Hotel chain app: verifies tier and grants equivalent perks (e.g. room upgrade, lounge).
## Next Steps
Understand when and why to use server-side issuance.
Full endpoint reference with error codes.
More schema examples including membership and event pass.
SDK verification flow reference.
# AIR for Telco & Subscriptions
Source: https://docs.moca.network/airkit/guides/air-for-telco
Issue subscriber credentials at activation, reuse KYC across carrier partners, and build loyalty tiers that reduce churn — without storing subscriber PII.
Carriers run identity verification for every new subscriber, but that verified status stays locked inside each operator's silo. A prepaid user who passes KYC at one carrier starts from zero at a roaming partner. AIR Kit lets you **issue a subscriber credential at activation** — so your KYC investment travels with the user across carriers, MVNOs, and partner services.
## What You Can Build
* **Subscriber credentials** — Issue a verified subscriber attestation the moment a user activates; roaming and partner carriers accept it without re-running identity checks
* **Plan tier credentials** — Issue a "Premium" or "Business" tier badge that unlocks partner benefits automatically, with no bilateral API required
* **Device ownership proofs** — Issue a verified device credential (IMEI-bound) for device insurance, warranty, and repair partner integrations
* **Churn intervention** — Trigger a "Loyalty Reward" credential when a subscriber hits a retention milestone; partner services can verify and honour the benefit
* **Roaming partner verification** — Let partner carriers verify a subscriber's KYC status via ZK proof; no PII crosses operator boundaries
* **Age-gated service access** — Gate adult content, gambling, or age-restricted add-ons behind an age credential without storing birthdates
## Architecture
```mermaid theme={null}
graph TD
A["User activates SIM / subscription"] --> B[Your Activation Backend]
B -->|KYC pass + activation event| C[Issue on Behalf API]
C --> D[Subscriber Credential in User's AIR Account]
D --> E{User accesses partner service}
E -->|Roaming carrier| F["Verify subscriber KYC\nNo PII transferred"]
E -->|MVNO partner| G["Tier unlock\nno API integration required"]
E -->|Device insurer| H["Device ownership proof"]
E -->|Your retention offer| I["Loyalty reward gated by tenure credential"]
```
## Recommended Schemas
### Subscriber Credential
```json theme={null}
{
"title": "Subscriber Credential",
"description": "Verified mobile subscriber attestation — no raw PII included",
"properties": {
"carrierId": {
"type": "string",
"description": "Issuing carrier identifier"
},
"planType": {
"type": "string",
"enum": ["prepaid", "postpaid", "business", "iot"],
"description": "Subscription plan category"
},
"kycLevel": {
"type": "string",
"enum": ["basic", "enhanced", "full"],
"description": "Level of identity verification completed at activation"
},
"activatedAt": {
"type": "string",
"format": "date-time"
},
"isOver18": {
"type": "boolean"
},
"countryCode": {
"type": "string",
"description": "ISO 3166-1 alpha-2 — subscriber's registration country"
}
},
"required": ["carrierId", "planType", "kycLevel", "activatedAt", "isOver18"]
}
```
### Subscriber Loyalty Tier
```json theme={null}
{
"title": "Subscriber Loyalty Tier",
"description": "Carrier loyalty tier based on tenure and spend",
"properties": {
"carrierId": { "type": "string" },
"tier": {
"type": "string",
"enum": ["Standard", "Silver", "Gold", "Platinum"],
"description": "Current loyalty tier"
},
"tenureMonths": {
"type": "number",
"description": "Months as active subscriber"
},
"memberSince": {
"type": "string",
"format": "date"
}
},
"required": ["carrierId", "tier", "tenureMonths"]
}
```
Never include MSISDN (phone number), IMSI, full name, or address in `credentialSubject`. Store only **attestations and derived facts** — `isOver18`, `kycLevel`, `planType`. ZK proofs let partner services confirm subscriber attributes without receiving any underlying data.
## Implementation
### Step 1 — Issue subscriber credential at activation
Your activation system already fires an event when a subscriber passes KYC and activates. Add one Issue on Behalf call to that event handler.
```javascript theme={null}
// activation-handler.js
const { getPartnerJwt } = require('./lib/jwt');
const BASE_URL = 'https://api.sandbox.mocachain.org/v1';
async function issueSubscriberCredential({ userEmail, activation }) {
if (activation.kycStatus !== 'APPROVED') return;
const token = await getPartnerJwt(userEmail);
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,
credentialId: process.env.SUBSCRIBER_CREDENTIAL_ID,
credentialSubject: {
carrierId: process.env.CARRIER_ID,
planType: activation.planType, // "prepaid" | "postpaid" | "business"
kycLevel: activation.kycLevel, // "basic" | "enhanced" | "full"
activatedAt: new Date().toISOString(),
isOver18: activation.age >= 18,
countryCode: activation.countryCode, // e.g. "AU", "SG"
},
onDuplicate: 'revoke', // re-issue if subscriber upgrades plan / re-verifies
}),
});
if (!res.ok) throw new Error(`Subscriber credential issuance failed: ${res.status}`);
return res.json();
}
// Hook into your existing activation pipeline
activationPipeline.on('subscriber:activated', async ({ userEmail, activationData }) => {
await issueSubscriberCredential({ userEmail, activation: activationData });
});
```
### Step 2 — Issue loyalty tier on tenure milestone
```javascript theme={null}
// churn-prevention.js — runs nightly or on billing cycle
async function issueLoyaltyTierIfEligible({ userEmail, tenureMonths }) {
const tier =
tenureMonths >= 36 ? 'Platinum' :
tenureMonths >= 24 ? 'Gold' :
tenureMonths >= 12 ? 'Silver' : 'Standard';
const token = await getPartnerJwt(userEmail);
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,
credentialId: process.env.LOYALTY_TIER_CREDENTIAL_ID,
credentialSubject: {
carrierId: process.env.CARRIER_ID,
tier,
tenureMonths,
memberSince: getMemberSinceDate(userEmail),
},
onDuplicate: 'revoke', // always replace with updated tier
}),
});
}
// Run on each billing cycle to keep tiers current
billingCycle.on('invoice:settled', async ({ userEmail, tenureMonths }) => {
await issueLoyaltyTierIfEligible({ userEmail, tenureMonths });
});
```
### Step 3 — Verify subscriber KYC at a roaming partner
The roaming partner integrates AIR Kit as a verifier. They call one SDK method — they never receive subscriber PII, only a COMPLIANT/NON\_COMPLIANT result plus the KYC level.
```javascript theme={null}
// roaming-partner.js (frontend — partner carrier's app)
import { AirService } from '@mocanetwork/airkit';
import { AirService, BUILD_ENV } from "@mocanetwork/airkit";
const airService = new AirService({ partnerId: process.env.ROAMING_PARTNER_ID });
await airService.init({ buildEnv: BUILD_ENV.PRODUCTION });
async function verifyRoamingSubscriber() {
const result = await airService.verifyCredential({
programId: process.env.ROAMING_VERIFY_PROGRAM_ID,
// Verifier program rule: kycLevel === "enhanced" OR "full"
// The verifier sees only: COMPLIANT / NON_COMPLIANT
// No MSISDN, no name, no raw subscriber data
});
if (result.status !== 'COMPLIANT') {
throw new Error('SUBSCRIBER_KYC_REQUIRED');
}
return result;
}
```
### Step 4 — Gate age-restricted services
```javascript theme={null}
// age-restricted-service.js (frontend)
async function requireAgeVerification() {
const result = await airService.verifyCredential({
programId: process.env.AGE_18_VERIFY_PROGRAM_ID,
// Program checks: isOver18 === true — subscriber's birthdate never exposed
});
return result.status === 'COMPLIANT';
}
const allowed = await requireAgeVerification();
if (!allowed) showAgeVerificationPrompt();
```
## Key Patterns
| Pattern | `onDuplicate` | When to Use |
| --------------------- | :-----------: | ----------------------------------------------------------------- |
| Initial activation | `"ignore"` | Issue once at first activation; don't re-issue if user reinstalls |
| Plan upgrade / re-KYC | `"revoke"` | Always reissue with updated `kycLevel` or `planType` |
| Loyalty tier update | `"revoke"` | Monthly billing cycle refresh |
| Device credential | `"ignore"` | One credential per device — immutable proof of ownership |
## Privacy Guarantee
The roaming partner or MVNO receives **only a boolean result** from the ZK proof. No MSISDN, no IMSI, no subscriber identity document data crosses operator boundaries.
| What the Verifier Sees | What Stays Private |
| ------------------------------------------ | ------------------------ |
| `COMPLIANT / NON_COMPLIANT` | MSISDN / phone number |
| `kycLevel` (`basic` / `enhanced` / `full`) | IMSI / SIM data |
| `isOver18: true` | Full name, date of birth |
| `planType` | Address, national ID |
| Credential expiry | Billing history |
## Examples
The repo uses fintech and loyalty app names (KYC provider, lending platform, airline, hotel). The same code adapts to telco (carrier, roaming partner) via schema and branding — see each example's `schema.json` and the README's "Adapting to Your Vertical" section.
KYC provider app: issues credentials once; user carries the proof to other platforms.
Lending platform app: verifies the credential and accepts the proof without re-running KYC.
Airline loyalty app: issues tier credential; user carries status to partner brands.
Hotel chain app: verifies tier and grants equivalent perks (e.g. room upgrade, lounge).
## Next Steps
Server-side issuance without user presence.
Full endpoint reference with error codes.
KYC portability patterns used in financial services.
ZK proof flow end-to-end.
# AIR for Ticketing & Events
Source: https://docs.moca.network/airkit/guides/air-for-ticketing
Issue tamper-proof attendance credentials at venue check-in. Let fans carry verified event history as portable proofs for loyalty rewards, presales, and exclusive access.
Traditional event tickets are one-time-use and forgotten after the show. AIR Kit lets you transform every venue scan into a **permanent, portable attendance credential** — proof the fan was there, reusable for loyalty rewards, presale access, partner discounts, and cross-brand partnerships.
## What You Can Build
* **Attendance credentials** — Issue a verifiable "I attended X event" badge at check-in
* **Fan loyalty tiers** — Auto-upgrade fans after 5 attended events to "Loyal Fan" credential
* **Exclusive access gating** — Gate VIP lounges, artist meet-and-greets, and presales to verified attendees
* **Cross-brand fan credentials** — Share attendance proof with partner brands (merchandise, hotels, streaming)
* **Anti-scalping** — Gate secondary ticket access behind an attendance credential from a prior event
## Architecture
```mermaid theme={null}
graph TD
A[Fan arrives at venue] --> B["Venue scanner / check-in system"]
B -->|"Scan detects fan's AIR Account email"| C[Issue on Behalf API]
C --> D["Attendance Credential\nin Fan's AIR Account"]
D --> E{Fan uses credential later}
E --> F[Claim loyalty reward]
E --> G[Access VIP presale]
E --> H[Redeem partner discount]
E --> I[Prove attendance history]
```
## Recommended Schema
```json theme={null}
{
"title": "Event Attendance",
"description": "Verifiable proof of physical attendance at a live event",
"properties": {
"eventId": {
"type": "string",
"description": "Unique event identifier"
},
"eventName": {
"type": "string",
"description": "Display name — e.g. 'Coachella 2025 Day 1'"
},
"venue": {
"type": "string",
"description": "Venue name and city"
},
"attendedAt": {
"type": "string",
"format": "date-time",
"description": "Timestamp of check-in scan"
},
"ticketTier": {
"type": "string",
"enum": ["General", "VIP", "Backstage", "Artist"],
"description": "Ticket category"
},
"organizerId": {
"type": "string",
"description": "Issuing organizer / promoter identifier"
}
},
"required": ["eventId", "eventName", "attendedAt", "organizerId"]
}
```
## Implementation
### Step 1 — Issue attendance credential at check-in
Your venue scanner calls your backend on each valid scan. The backend issues via Issue on Behalf — the fan does nothing, no wallet open, no UX friction at the door.
```javascript theme={null}
// venue-checkin.js
const { getPartnerJwt } = require('./lib/jwt');
async function issueAttendanceCredential({ fanEmail, eventDetails }) {
const token = await getPartnerJwt(fanEmail);
const res = await fetch('https://api.sandbox.mocachain.org/v1/credentials/issue-on-behalf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-partner-auth': token,
},
body: JSON.stringify({
issuerDid: process.env.ISSUER_DID,
credentialId: process.env.ATTENDANCE_CREDENTIAL_ID,
credentialSubject: {
eventId: eventDetails.id,
eventName: eventDetails.name,
venue: eventDetails.venue,
attendedAt: new Date().toISOString(),
ticketTier: eventDetails.ticketTier, // "General" | "VIP" | "Backstage"
organizerId: process.env.ORGANIZER_ID,
},
onDuplicate: 'ignore', // one credential per event per fan
}),
});
if (!res.ok) throw new Error(`Attendance issuance failed: ${res.status}`);
const { coreClaimHash } = await res.json();
return coreClaimHash;
}
// Called by your ticketing system on each valid scan
app.post('/api/checkin', async (req, res) => {
const { fanEmail, eventId } = req.body;
const event = await getEventDetails(eventId);
const hash = await issueAttendanceCredential({ fanEmail, eventDetails: event });
res.json({ success: true, coreClaimHash: hash });
});
```
### Step 2 — Gate VIP presale to verified attendees
```javascript theme={null}
// presale-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 });
async function canAccessPresale() {
const result = await airService.verifyCredential({
programId: process.env.PRESALE_VERIFY_PROGRAM_ID,
// Verifier program rule: organizerId === process.env.ORGANIZER_ID
// (attended at least 1 prior event by this organizer)
});
return result.status === 'COMPLIANT';
}
const isVerifiedFan = await canAccessPresale();
if (isVerifiedFan) {
showPresaleRegistration();
} else {
showMessage('Presale access requires verified attendance at a prior event.');
}
```
### Step 3 — Auto-upgrade fans to loyalty tiers after N events
```javascript theme={null}
// loyalty-tier-check.js — called after each attendance credential issuance
async function checkAndIssueLoyaltyTier(fanEmail) {
const attendedCount = await getAttendanceCount(fanEmail); // your DB
if (attendedCount === 5) await issueLoyaltyTier({ fanEmail, tier: 'Loyal Fan' });
if (attendedCount === 20) await issueLoyaltyTier({ fanEmail, tier: 'Super Fan' });
}
// issueLoyaltyTier uses the same Issue on Behalf pattern — see AIR for Loyalty guide
```
## Zero-Friction Fan UX
The key advantage for events: **fans never interact with the credential system at the door**. They scan their ticket as normal, the credential lands in their AIR Account in the background, and they use it the next time they want a reward or presale.
No app to open, no wallet prompt. Exactly the same experience as today.
Issue on Behalf fires silently. The fan's AIR Account receives the credential on-chain.
Sees a notification: "You earned a Verified Attendee credential for Coachella 2025."
One tap to register for the presale, claim a merchandise discount, or prove their fan tier to a partner brand.
## Examples
Venue check-in app: issues attendance credential when the fan scans in at the event.
Merch store app: verifies attendance and unlocks rewards (e.g. discount, exclusive offer).
Airline loyalty app: issues tier credential; user carries status to partner brands.
Hotel chain app: verifies tier and grants equivalent perks (e.g. room upgrade, lounge).
## Next Steps
Build tier-based loyalty on top of attendance credentials.
Full endpoint reference and error handling.
When to use server-side vs. user-initiated issuance.
End-to-end system overview.
# Architecture & Data Flow
Source: https://docs.moca.network/airkit/guides/overview
End-to-end technical architecture of AIR Kit — SDK services, Moca Chain, credential lifecycle, and how data moves through the system.
AIR Kit is a modular SDK that sits between your application and Moca Chain. It exposes two independent service surfaces — **Account Services** and **Credential Services** — that can be used together or independently.
## System Overview
```mermaid theme={null}
graph TD
subgraph YourApp["Your Application"]
A[Frontend / Mobile App]
B[Backend Server]
end
subgraph SDK["AIR Kit SDK"]
C[Account Services]
D[Credential Services]
end
subgraph Chain["Moca Chain"]
E[Identity Oracle]
F[EVM Layer]
G[Decentralized Storage - MCSP]
end
A -->|Web SDK / Flutter SDK| C
A -->|Web SDK / Flutter SDK| D
B -->|Partner JWT + REST API| D
C -->|Wallet & session management| F
D -->|Issue / Verify credentials| E
E -->|Anchors credential hash| F
F -->|Payload stored| G
```
## Account Services Flow
Account Services handles user authentication, smart account creation, and wallet operations.
```mermaid theme={null}
sequenceDiagram
participant U as User
participant App as Your App
participant SDK as AIR Kit SDK
participant Chain as Moca Chain
U->>App: Click "Login"
App->>SDK: airService.login()
SDK->>U: Show login UI (email / social / wallet)
U->>SDK: Authenticate
SDK->>Chain: Create / fetch smart account
Chain-->>SDK: Account address + session
SDK-->>App: User session + wallet provider
App-->>U: Logged in ✓
```
## Credential Issuance Flow (User-Initiated)
The standard issuance path — the user is present and triggers the credential claim.
```mermaid theme={null}
sequenceDiagram
participant U as User
participant App as Your App (Issuer)
participant SDK as AIR Kit SDK
participant Chain as Moca Chain
U->>App: Triggers credential claim
App->>SDK: airService.issueCredential(credentialId, subject)
SDK->>U: Prompt user to sign consent
U->>SDK: Signs with AIR Account
SDK->>Chain: Submit issuance transaction
Chain->>Chain: Identity Oracle verifies & anchors hash
Chain-->>SDK: coreClaimHash
SDK-->>App: Issuance confirmed
App-->>U: Credential delivered to AIR Account
```
## Credential Verification Flow
```mermaid theme={null}
sequenceDiagram
participant U as User (Holder)
participant App as Your App (Verifier)
participant SDK as AIR Kit SDK
participant Chain as Moca Chain
U->>App: Requests access / action
App->>SDK: airService.verifyCredential(programId)
SDK->>U: Prompt user to select credential
U->>SDK: Presents credential (ZK proof generated client-side)
SDK->>Chain: Submit ZK proof for verification
Chain->>Chain: Oracle validates proof on-chain
Chain-->>SDK: COMPLIANT / NON_COMPLIANT
SDK-->>App: Verification result
App-->>U: Access granted or denied
```
## Issue on Behalf Flow (Server-Side, No User Session)
Use this path when a **backend event** should trigger issuance without the user being present.
```mermaid theme={null}
sequenceDiagram
participant Event as Backend Event
participant Server as Your Server
participant API as AIR Kit API
participant Chain as Moca Chain
Event->>Server: KYC passed / purchase confirmed / check-in scanned
Server->>Server: Sign Partner JWT (scope:"issue on-behalf", email: user)
Server->>API: POST /v1/credentials/issue-on-behalf
API->>Chain: Queue credential for on-chain processing
Chain-->>API: coreClaimHash
API-->>Server: { coreClaimHash }
Server->>API: GET /v1/credentials/status?coreClaimHash=...
API-->>Server: { vcStatus: "ONCHAIN" }
Note over Chain: User receives credential in AIR Account silently
```
## Multi-Vendor Stack Integration
AIR Kit is **additive** — it does not replace your existing infrastructure. The diagram below shows how it slots into a typical enterprise stack alongside loyalty platforms, KYC providers, ticketing systems, and payment processors.
```mermaid theme={null}
graph LR
subgraph Existing["Your Existing Stack"]
L[Loyalty Platform]
K[KYC Provider]
T[Ticketing System]
P[Payment Processor]
end
subgraph AIR["AIR Kit Integration Layer"]
IB[Issue on Behalf API]
SDK2[Web / Flutter SDK]
end
subgraph MocaChain["Moca Chain"]
ORC[Identity Oracle]
STORE[Decentralized Storage]
end
L -->|Milestone event| IB
K -->|Verification pass| IB
T -->|Scan / check-in| IB
P -->|Transaction event| IB
IB --> ORC
ORC --> STORE
SDK2 -->|Verify credential| ORC
```
## Service Independence
Account Services and Credential Services are decoupled — integrate one, both, or mix server-side and client-side patterns.
| Feature | Account Services | Credential Services | Requires Both |
| ---------------------------------- | :--------------: | :-----------------: | :----------------------: |
| User login / SSO | ✓ | — | — |
| Smart account / wallet | ✓ | — | — |
| Gas sponsorship (Paymaster) | ✓ | — | — |
| Wagmi connector | ✓ | — | — |
| Issue credentials (user-initiated) | — | ✓ | ✓ user must be logged in |
| Issue on Behalf (server-side) | — | ✓ | — |
| Verify credentials | — | ✓ | — |
## Next Steps
Install and initialize the SDK in minutes.
Vertical-specific guides with code examples for Loyalty, Fintech, Gaming, and Ticketing.
Step-by-step credential issuance walkthrough.
Server-side issuance without user presence.
# AIR Kit Documentation
Source: https://docs.moca.network/airkit/how-to-use
AIR Kit developer documentation for accounts, credentials, and full-stack integration.
# How to Read This Documentation
This documentation is designed to guide developers through using **AIR Kit** efficiently. It is organized to serve multiple personas and use cases.
## 1. Start with Your Persona
Identify which persona matches your use case to choose the appropriate Quickstart:
| Persona | Suggested Start |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Account Integrators** | [Quickstart 1: Account Integration & User Onboarding](/airkit/quickstart) |
| **Credential Integrators** | [Quickstart 2: Issue Credentials](/airkit/quickstart/issue-credentials) & [Quickstart 3: Verify Credentials](/airkit/quickstart/verify-credentials) |
Each Quickstart contains step-by-step instructions and example code to get started quickly.
## 2. Core Concepts
Before building, familiarize yourself with the following concepts:
* **AIR Service Overview** – [What AIR Kit is and how it works](/airkit/)
* **Installation & Setup** – [Creating your partner account and initializing the SDK](/airkit/usage/getting-started)
* **Configuration Options** – [Customize AIR Kit behavior for your app](/airkit/usage/config-theming)
* **Authentication & Sessions** – [Partner JWT, login flows, and session management](/airkit/usage/partner-authentication)
* **Verifiable Credentials** – [Issuance, verification, storage, and ecosystem interoperability](/learn/airkit/credentials)
* **Security & Privacy Principles** – Best practices for data handling and privacy
Each concept links to detailed guides and examples for implementation.
## 3. Using AIR Kit
The functional documentation is organized by capabilities:
1. **General Usage**
* Initialize SDK, configure authentication, and user management
2. **General Configuration**
* Theming, languages, and login options
3. **Account Service**
* Wallet functions and provider
* Integrations with Wagmi Connector and other functions
4. **Credential Service**
* Issue and verify credentials
## 4. Advanced Topics (Coming Soon)
Optional sections for experienced developers:
* **API Reference** – All classes, methods, and SDK options
* **Plug & Play modules** – Coming soon
* **Migration & Compatibility** – Guidelines for upgrading or integrating with existing systems
Advanced topics are not required for standard usage.
## 5. Support & Maintenance
Use this section for:
* Troubleshooting issues
* Reviewing release notes
* Providing feedback or contributing to AIR Kit
## What's next?
Each Quickstart provides:
* Step-by-step instructions you can copy and run
* Code examples in TypeScript for direct use
* Deep links to reference docs for SDK methods and configuration options
👉 Start with the Quickstart that matches your role, then refer back to others as your app grows.
# What is AIR Kit?
Source: https://docs.moca.network/airkit/index
AIR Kit is the SDK for integrating portable credentials, smart accounts, and privacy-preserving identity into your application.
# AIR Kit Overview
AIR Kit is the comprehensive developer toolkit for integrating secure, decentralized identity features into your applications. It simplifies complex concepts like **Web3 login**, **user onboarding**, and **zero-knowledge proofs**, making them accessible for all developers and users.
AIR Kit provides two powerful services:
1. **AIR Account Services**
* Manage user login and Single Sign-On (SSO)
* Wallet management and services
2. **AIR Credential Services**
* Issue, verify, and consume **zero-knowledge credentials**
* Portable credentials that users can carry anywhere
### Platform support
AIR Kit integrates easily with your **Web App** or **Mobile App**.
By embedding our **Web SDK (Javascript)** on the frontend of your web application, or integrating our **Flutter SDK** on your Mobile app, you can easily access our Account Services and Credential Services.
Note: Credential Services coming soon to Flutter SDK
## What is AIR?
AIR stands for **Account Identity Reputation**, representing a foundational shift in how users interact with the internet. AIR is designed to create a **user-centric digital experience**, where individuals truly own their **assets, identity, and data**.
Key principles of AIR:
* **User Ownership:** Users control their digital assets, identity, and personal data.
* **Common Language of the Internet:** Identity and user data become a shared standard for seamless interaction between applications.
* **True Network Effects:** Applications leveraging the user-as-root model can provide more valuable experiences.
* **Shared Economics:** Incentivizes collaboration among apps to benefit the user.
*Moca Network = AIR Kit + Moca Chain*
## Who Is This For?
AIR Kit is suitable for any platform where **trust, privacy, and user context** matter:
* Founders building consumer apps who want to skip traditional KYC
* DAOs needing pseudonymous membership tools
* Game developers porting player identities and badges across worlds
* Fintech teams verifying income, age, or credentials **without storing PII**
* AI agents requiring verifiable access and behavioral reputation
## The Big Idea: Identity as a Primitive
By making identity **modular, portable, and verifiable**, AIR Kit enables new types of applications:
* Reward **real participation** with airdrops
* Launch apps **without starting from zero users**
* Grow communities **without re-KYCing everyone**
> With AIR Kit, identity becomes a core building block rather than an afterthought. Plug in the SDK, remix credentials, and design new experiences.
## Key Features & Benefits
* **Single Sign-on**: Users sign on with the same account across the Moca Network ecosystem
* **Embedded Account:** Wallet capabilities without the web3 learning curve
* **Decentralized & Secure:** Users maintain control of their identity and credentials
* **Zero-Knowledge Proofs:** Privacy-preserving verification for sensitive data
* **Cross-Application Portability:** Credentials and identity travel across platforms
* **Ecosystem Growth:** Shared identity enables cooperation and network effects
## Getting Started
### Quickstart Guide
* **Account Integrators:** Start with **Login & User Onboarding**
* **Credential Integrators:** Start with **Issuance & Verification**
* **Full Stack Builders:** Follow **Combined Flow Guide**
Each Quickstart provides **step-by-step instructions and code examples** to get up and running fast.
### Flutter apps
If you build with **Flutter**, start with the [Flutter SDK overview](/airkit/flutter/overview) (installation, Android/iOS setup, wallet, and troubleshooting).
## Next Steps
1. Explore **Core Concepts**: Authentication, sessions, and credentials
2. Install **AIR Kit**: Account and Credential services
3. Implement **Combined Flows**: Login + Issue + Verify for end-to-end integration
4. Check **Advanced Topics**: API references, compatibility, and plug & play modules
5. Refer to **Support & Maintenance**: Troubleshooting, release notes, and feedback
# Integrate Using AI
Source: https://docs.moca.network/airkit/integrating-using-ai
Connect Moca Network documentation to AI tools to accelerate AIR Kit development with real-time access to documentation, API references, and code examples.
Connect the Moca Network documentation MCP server to your IDE or AI assistant for real-time access to AIR Kit documentation, API references, and code examples. When you connect the MCP server, AI assistants can search the documentation directly to help you build faster and more accurately.
## Connect your AI tools
Connect the Moca Network documentation MCP server to your preferred AI tool:
Your MCP server URL is: `https://docs.moca.network/mcp`
1. Navigate to the [Connectors](https://claude.ai/settings/connectors) page in the Claude settings.
2. Select **Add custom connector**.
3. Add the Moca Network MCP server:
* Name: `MocaNetwork`
* URL: `https://docs.moca.network/mcp`
4. Select **Add**.
1. When using Claude, select the attachments button (the plus icon).
2. Select the MocaNetwork MCP server.
3. Ask Claude a question about Moca Network or AIR Kit.
See the [Model Context Protocol documentation](https://modelcontextprotocol.io/docs/tutorials/use-remote-mcp-server#connecting-to-a-remote-mcp-server) for more details.
Your MCP server URL is: `https://docs.moca.network/mcp`
Run the following command:
```bash theme={null}
claude mcp add --transport http MocaNetwork https://docs.moca.network/mcp
```
Verify the connection by running:
```bash theme={null}
claude mcp list
```
See the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/mcp#installing-mcp-servers) for more details.
Your MCP server URL is: `https://docs.moca.network/mcp`
1. Use Command + Shift + P (Ctrl + Shift + P on Windows) to open the command palette.
2. Search for "Open MCP settings".
3. Select **Add custom MCP**. This opens the `mcp.json` file.
In `mcp.json`, add:
```json theme={null}
{
"mcpServers": {
"MocaNetwork": {
"url": "https://docs.moca.network/mcp"
}
}
}
```
In Cursor's chat, ask "What tools do you have available?" Cursor should show the MocaNetwork MCP server as an available tool.
See the [Cursor documentation](https://docs.cursor.com/en/context/mcp#installing-mcp-servers) for more details.
Your MCP server URL is: `https://docs.moca.network/mcp`
Create a `.vscode/mcp.json` file in your project root.
In `.vscode/mcp.json`, add:
```json theme={null}
{
"servers": {
"MocaNetwork": {
"type": "http",
"url": "https://docs.moca.network/mcp"
}
}
}
```
See the [VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more details.
You can also use the contextual menu on any page of the documentation to copy the MCP server URL or install it directly into Cursor or VS Code.
## Setup with other agents
The Moca skill works with any tool that supports the [Agent Skills](https://agentskills.io/home) standard, including Cursor, VS Code Copilot, Gemini CLI, OpenAI Codex, and more.
Run:
```bash theme={null}
npx skills add https://docs.moca.network
```
The CLI detects your installed agents and places the skill in the correct directory for each one.
## What the skill provides
| Feature | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Quick references | SDK install (`@mocanetwork/airkit`), platform matrix, provider and wallet APIs, and REST credential endpoints |
| Decision guidance | Account-only vs credential flows, embedded AIR login vs bring-your-own auth, and client vs server signing with Partner JWT |
| Workflows | Step-by-step sequences for onboarding, issuance, verification, Issue on Behalf, and advanced flows like paymaster or session keys where relevant |
| Common gotchas | Initialization and iframe setup, JWKS and JWT errors, polling issuance until credentials are on-chain, and schema or verification pitfalls |
| Verification checklist | Partner ID, environment (sandbox vs production), chain allowlists, auth headers, and error handling before you ship |
## Static docs files
If your AI tool does not support MCP yet, you can use one of Moca Network's static documentation files instead.
Static files are snapshots and may not include the latest updates. Use MCP when possible for always-current docs.
## Which file should I use?
* **`llms.txt`** — A curated overview of Moca Network. Use it to route an agent to the right docs, understand product structure, and choose the right integration path.
* **`llms-full.txt`** — A full static snapshot of the documentation. Use it when your tool needs more exhaustive reference material in one file.
## Setup with Cursor
Go to **Settings** → **Features** → **Docs**.
Click **Add new doc** and paste: `https://docs.moca.network/llms.txt`
If your agent needs deeper static reference coverage, add: `https://docs.moca.network/llms-full.txt`
Use **@docs** → **Moca** in your AI chat to reference the documentation.
## Setup with Claude Desktop
Download the curated overview from: [https://docs.moca.network/llms.txt](https://docs.moca.network/llms.txt)
Download the full documentation snapshot from: [https://docs.moca.network/llms-full.txt](https://docs.moca.network/llms-full.txt)
Save one or both files in your project directory or another known location on your system.
Drag and drop the file into your Claude Desktop chat, or use the attachment button to upload it. Claude will then have access to the uploaded Moca Network documentation context for that conversation.
# SDK Platform Support Matrix
Source: https://docs.moca.network/airkit/platform-matrix
Supported platforms, frameworks, browsers, and module formats across AIR Kit SDKs.
## SDK Packages
| SDK | Package | Install |
| ------------- | --------------------- | ------------------------------------------------------------------------ |
| Web (JS / TS) | `@mocanetwork/airkit` | `npm install @mocanetwork/airkit` |
| Flutter | `airkit` (Dart) | OnePub hosted — see [Flutter installation](/airkit/flutter/installation) |
For the latest version numbers see [Release Notes](/airkit/release-notes).
## Platform Support
| Platform | Web SDK | Flutter SDK |
| ---------------- | :--------------------: | :---------: |
| Web (Browser) | ✓ | — |
| iOS (native) | — | ✓ |
| Android (native) | — | ✓ |
| React Native | Partial — WebView only | — |
| Electron | ✓ | — |
## Browser Support (Web SDK)
| Browser | Minimum Version | Notes |
| ----------------- | :-------------: | ------------------------------- |
| Chrome / Chromium | 126+ | Full support (passkey required) |
| Firefox | 97+ | Full support (passkey required) |
| Safari | 16+ | Full support (passkey required) |
| Edge | 126+ | Full support (Chromium-based) |
| Opera | 112+ | Full support (Chromium-based) |
| Samsung Internet | 14+ | Full support |
| IE 11 | ✗ | Not supported |
## Framework Compatibility (Web SDK)
| Framework | Status | Notes |
| ------------------------- | :----: | ---------------------------------------------------------- |
| React 17+ | ✓ | Full support |
| React 16 | ✓ | Full support |
| Next.js 13+ (App Router) | ✓ | Add `'use client'` directive to the initializing component |
| Next.js 12 (Pages Router) | ✓ | Full support |
| Vue 3 | ✓ | Full support |
| Vue 2 | ✓ | Full support |
| Nuxt 3 | ✓ | Full support |
| SvelteKit | ✓ | Full support |
| Angular 14+ | ✓ | Full support |
| Vanilla JS | ✓ | UMD / ESM builds available |
## Module Formats (Web SDK)
| Format | Import | Best For |
| -------- | ------------------------------------------------------- | -------------------------------- |
| ESM | `import { AirService } from '@mocanetwork/airkit'` | Vite, Webpack 5, modern bundlers |
| CommonJS | `const { AirService } = require('@mocanetwork/airkit')` | Node.js, older bundlers |
| UMD | `