Skip to content

Project: Build an Airtime Proof-of-Reserve Cycle ​

A complete, runnable project implementing the core proof-of-reserve cycle used throughout Comet Engine: mint a token 1:1 against a real-world deposit, then burn it the moment that value is actually delivered — so on-chain supply never drifts from the real float. See the Tokenization API for the endpoint reference.

▸API ConfigurationNot configured
Every endpoint, payload, and response on this page follows this choice.

What you'll build ​

A cycle.js script that tokenizes a KES 50 airtime deposit into IMC (Celo) or SIMC (Stellar), then burns it back the moment delivery is confirmed — leaving the journal balanced and on-chain supply exactly matching the real airtime float at every point in between, not just at the start and end. Pick a chain above — every step below follows that choice.

Prerequisites ​

  • Node.js 18+ (for global fetch)
  • A running comet-engine (cd comet-engine && go run ./cmd/engine) with a tenant API key/secret — see Authentication

Project setup ​

bash
mkdir airtime-cycle && cd airtime-cycle && npm init -y && npm install dotenv

.env (same for both chains — only chain in the script body changes):

env
COMET_BASE_URL=http://localhost:8100
COMET_API_KEY=your-tenant-api-key
COMET_API_SECRET=your-tenant-api-secret

Create cycle.js with this shared helper at the top — every step below adds to the same file:

js
import 'dotenv/config';

const base = process.env.COMET_BASE_URL;
const headers = {
  'Content-Type': 'application/json',
  'X-API-Key': process.env.COMET_API_KEY,
  'X-API-Secret': process.env.COMET_API_SECRET,
};

async function comet(method, path, body) {
  const res = await fetch(base + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
  const data = await res.json();
  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(data)}`);
  return data;
}

Step 1: Tokenize the deposit ​

The user has already paid KES via M-Pesa (handled by app-core-backend, outside this project). Tokenizing mints the equivalent airtime token to their wallet:

Tokenize (Celo)
curl -X POST "http://localhost:8100/api/v1/tokenize/airtime" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "userId": 24,
  "amountBase": "50000000",
  "externalId": "airtime-tx-001",
  "chain": "celo"
}'
Response200
{
  "status": "success",
  "asset": "IMC",
  "txHash": "0x..."
}

Step 2: Deliver the airtime ​

Handled by your own provider integration (e.g. ImpalaPay via app-core-backend) — outside comet-engine's scope, and outside this project. The next step assumes delivery is confirmed.

Step 3: Burn to remove the on-chain mirror ​

Burn (Celo)
curl -X POST "http://localhost:8100/api/v1/assets/imc/burn-by-holder" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "userId": 24,
  "amountBase": 50000000,
  "externalId": "airtime-tx-001"
}'
Response200
{
  "txHash": "0x...",
  "symbol": "IMC",
  "amount": 50
}

Journal entries:

EntryDebitCreditAmount
Tokenizeairtime_inventory::cometchain::celo::imc+50 IMC
Burnchain::celo::imcairtime_inventory::comet-50 IMC

Net effect either way: journal balanced, on-chain supply unchanged after the full cycle.

Idempotency

Use the same externalId for the tokenize and burn calls of the same transaction. This prevents double-minting or double-burning if a request is retried.

Step 4: Run the whole cycle as one script ​

Add main() to cycle.js — only the asset symbol and endpoint path change per chain, matching the steps above:

js
async function main() {
  const externalId = `airtime-tx-${Date.now()}`;

  // Step 1: mint 50 IMC against a KES deposit already handled upstream
  const minted = await comet('POST', '/api/v1/tokenize/airtime', {
    userId: 24, amountBase: '50000000', externalId, chain: 'celo',
  });
  console.log('minted:', minted.txHash);

  // Step 2 (in a real app): deliver the airtime via your provider here

  // Step 3: burn once delivery is confirmed
  const burned = await comet('POST', '/api/v1/assets/imc/burn-by-holder', {
    userId: 24, amountBase: 50000000, externalId,
  });
  console.log('burned:', burned.txHash);
  console.log('cycle complete — on-chain supply back to where it started');
}

main().catch((e) => { console.error(e); process.exit(1); });
bash
node cycle.js

What you built ​

  • A working mint → burn cycle backed by a real deposit, not a paper balance.
  • A journal that always nets to zero once the cycle completes — on-chain supply provably tracking the real float.
  • The same idempotency guarantee production traffic needs: retried calls with the same externalId never double-mint or double-burn.

Next steps: swap the hardcoded userId/amounts for your own deposit-confirmation webhook, or see Custom Loyalty Token if you want this same mint/burn shape for a token that isn't airtime-backed.

Released under the MIT License.