Skip to content

Tokenization API ​

Comet Engine tokenizes real-world assets by minting/burning on-chain tokens backed by real inventory. The engine enforces proof-of-reserve: airtime-backed tokens can only be minted/burned against registered airtime assets, and every operation is double-entry journaled.

Tokenizing something tenant-specific?

Security and utility tokens here use one fixed field schema for every tenant. If you need your own fields, unit of account, and verification workflow (a shipping container, a vault lot, an invoice — anything), see Receipt Tokenization below instead — it's schema-first and fully self-service.

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

How Tokenization Works ​

Real World                    On Chain
──────────                    ────────
User pays KES  ──────────►  Mint IMC (1:1 with KES)
    │                              │
    │   ImpalaPay delivers         │   On-chain supply = real float
    │   airtime to phone           │
    │                              │
    ▼                              ▼
Airtime received  ◄────────  Burn IMC (release backing)

Key invariant: on-chain IMC supply = real airtime float remaining

When IMC is minted, the engine debits the airtime inventory account. When IMC is burned, it credits it. The journal always balances.


Tokenize Airtime ​

Mints the chain's airtime-backed asset (IMC on Celo) to a user's custodial wallet, drawing from the platform's real airtime inventory (ImpalaPay float).

Tokenize Airtime (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": 12345,
  "amountBase": "10000000",
  "externalId": "airtime-001",
  "chain": "celo"
}'
Response200
{
  "status": "success",
  "asset": "IMC",
  "type": "airtime",
  "chain": "celo",
  "userId": 12345,
  "to": "0xe3f53fe54f8e1b5e8c86f070e4c9c2c7fa641d0d",
  "amountBase": "10000000",
  "txHash": "0x..."
}

Airtime-typed assets aren't chain-bound

The endpoint itself is chain-parameterized like every other dynamic-asset endpoint — whichever asset on the target chain is registered with type: airtime/backing: airtime gets minted. IMC is Celo's; a Stellar deployment needs its own airtime-typed asset registered there first (see Register an Airtime Asset below) — SIMC above is illustrative, not a fixed symbol.

Request Fields:

FieldTypeRequiredDescription
userIdnumberYesRecipient user
amountBasestringYesAmount in base units (6 decimals: 10000000 = 10 IMC)
externalIdstringYesIdempotency key (prevents double-mint)
chainstringNoTarget chain (defaults to celo)

Proof-of-reserve

The caller (typically app-core-backend) must verify the ImpalaPay float balance before calling this endpoint. Comet Engine only mints assets registered with type: airtime or backing: airtime.


Burn on Redemption ​

When airtime is actually delivered, burn the on-chain mirror to keep supply pegged:

Burn IMC (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": 12345,
  "amountBase": 10000000,
  "externalId": "airtime-delivery-001"
}'
Response200
{
  "txHash": "0x...",
  "chain": "celo",
  "symbol": "IMC",
  "amount": 10
}

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.


Check Status ​

Check Status
curl -X GET "http://localhost:8100/api/v1/tokenize/status/airtime-001" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "id": "airtime-001",
  "status": "confirmed",
  "txHash": "0x...",
  "amount": 10000000
}

Delegated Endpoints ​

EndpointStatusNotes
POST /api/v1/tokenize/depositDelegatesHandled by app-core-backend
GET /api/v1/tokenize/status/:idDelegatesProxied to app-core-backend

Full worked examples

Step-by-step walkthroughs using these endpoints now live in Examples: Airtime Tokenization, AMM Trading, Cross-Border Payment, and Custom Loyalty Token.

Registering an Airtime Asset ​

Set env vars:

env
CELO_TESTNET_IMC_CONTRACT=0x1D1aA006f66A191fc04e7d1cab8D62B0c81d9221
CELO_IMC_TYPE=airtime
CELO_IMC_BACKING=airtime

Or register at runtime:

Register Airtime Asset (Celo)

🔒 Admin operation — always requires a Service Token, regardless of the panel above.

curl -X POST "http://localhost:8100/api/v1/admin/assets/IMC/register?chain=celo" \
  -H "Content-Type: application/json" \
  -H "X-Service-Token: YOUR_SERVICE_TOKEN" \
  -d '{
  "contract": "0x1D1aA006f66A191fc04e7d1cab8D62B0c81d9221",
  "decimals": 6,
  "name": "ImpalaCoin",
  "type": "airtime",
  "backing": "airtime"
}'
Response200
{
  "message": "asset IMC registered on celo"
}

Receipt Tokenization ​

Tokenize any real-world claim your tenant deals in — a shipping container, a vault lot of gold bars, a prepaid voucher, an invoice receivable — on your own terms. Unlike the fixed fields of Airtime Tokenization above, a receipt is an instance of a Standard you define yourself: your own field list, your own unit of account, your own decimals, your own choice of whether a verification step gates minting. This is entirely self-service — every endpoint below accepts your normal X-API-Key/X-API-Secret (or X-Service-Token), the same credentials you already use for wallets and assets. No elevated access is required.

How Receipt Tokenization Works ​

1. Define a Standard          2. Deposit a claim         3. Verify (or reject)
   (once, per product line)      under that standard         → mints on-chain,
   fields, unit, decimals,       free-form attributes,          exactly once
   verification requirement      validated against the
                                 standard's own schema

4. Trade                      5. Redeem
   plain /assets/:symbol/send    burns back on settlement,
   — no receipt-specific code    tracked per receipt
   needed, fractional amounts
   move like any other asset

Works on both Stellar and EVM chains (Celo, Base, Arbitrum, …) — the chain you set on your Standard decides which. On Stellar, the on-chain asset is auto-provisioned (no deploy step exists there). On an EVM chain with a token factory configured, verifying auto-deploys a dedicated ERC-20 capped at exactly your receipt's quantity; otherwise pass an explicit contract.


Define a Standard ​

One receipt, followed end to end, per chain

The six sections below (Define → Deposit → Verify → Trade → Redeem → Check Status) all follow the same one receipt — its id and symbol carry through every step. Because of that, the chain toggle here switches the entire workflow at once rather than each block independently — a Celo-deposited receipt can't suddenly be verified on Stellar. Pick a chain once at the top and read straight down.

Create Standard (Celo)
curl -X POST "http://localhost:8100/api/v1/tokenize/receipt/standards" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "code": "BEKANA_AVOCADO_CONTAINER",
  "name": "Bekana Avocado Oil Container Receipt",
  "symbolPrefix": "BEKA",
  "quantityUnit": "tons",
  "decimals": 6,
  "chain": "celo",
  "requireVerification": true,
  "fields": [
    {
      "name": "containerId",
      "type": "string",
      "required": true,
      "role": "identifier"
    },
    {
      "name": "commodityType",
      "type": "string",
      "required": true
    },
    {
      "name": "netWeightTons",
      "type": "number",
      "required": true,
      "role": "quantity"
    },
    {
      "name": "warehouseLocation",
      "type": "string",
      "required": false
    }
  ]
}'
Response201
{ "id": "6a9e...", "tenantId": 6, "code": "BEKANA_AVOCADO_CONTAINER", "symbolPrefix": "BEKA", "quantityUnit": "tons", "decimals": 6, "chain": "celo", "requireVerification": true, "fields": [ ... ], "status": "active" }

Deposit a Claim (Celo) ​

Deposit Receipt (Celo)
curl -X POST "http://localhost:8100/api/v1/tokenize/receipt/deposit" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "externalUserId": "bekana-exporter-1",
  "standardCode": "BEKANA_AVOCADO_CONTAINER",
  "attributes": {
    "containerId": "BEK-CTR-0001",
    "commodityType": "avocado oil",
    "netWeightTons": 24,
    "warehouseLocation": "Mombasa Bonded Warehouse, Kenya"
  }
}'
Response201
{
  "id": "6a9e80a7f1770a34291a0259",
  "symbol": "BEKAD75E1B56",
  "chain": "celo",
  "status": "pending_verification",
  "message": "receipt recorded (BEK-CTR-0001: 24 tons) under standard BEKANA_AVOCADO_CONTAINER — pending verification before minting"
}

Verify (Mint) (Celo) ​

Verify Receipt (Celo)
curl -X POST "http://localhost:8100/api/v1/tokenize/receipt/6a9e80a7f1770a34291a0259/verify" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "verifiedBy": "SGS Kenya Surveyors"
}'
Response200
{
  "status": "success",
  "id": "6a9e80a7f1770a34291a0259",
  "symbol": "BEKAD75E1B56",
  "chain": "celo",
  "mintedQuantity": 24,
  "quantityUnit": "tons",
  "to": "0xcf0981b26e0e022f4323da24e092f30d151bf4e8",
  "txHash": "0x35ed18b655cfc1b8d773b785276a1d5d36546e2a91b78a401c45530ff24ba629"
}

Trade (Celo) ​

Trade Partial Cargo (Celo)
curl -X POST "http://localhost:8100/api/v1/assets/BEKAD75E1B56/send?chain=celo" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "externalUserId": "bekana-exporter-1",
  "to": "0x176d431dbd00639e13177ef0c397a9fa2eb77a7b",
  "amountBase": 6000000
}'
Response200
{
  "amount": 6,
  "chain": "celo",
  "symbol": "BEKAD75E1B56",
  "txHash": "0xca80211115aa22608bf79f5771ed4a80dbff2973b77cbf3c2ae9712a05b722af"
}

Redeem (Celo) ​

Redeem (Celo)
curl -X POST "http://localhost:8100/api/v1/tokenize/receipt/6a9e80a7f1770a34291a0259/redeem" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "externalUserId": "bekana-exporter-1",
  "amount": 5,
  "externalId": "redeem-001"
}'
Response200
{
  "status": "success",
  "id": "6a9e80a7f1770a34291a0259",
  "symbol": "BEKAD75E1B56",
  "redeemedAmount": 5,
  "quantityUnit": "tons",
  "outstanding": 13,
  "receiptStatus": "partially_redeemed",
  "txHash": "0x..."
}

Check Status (Celo) ​

Get Receipt (Celo)
curl -X GET "http://localhost:8100/api/v1/tokenize/receipt/6a9e80a7f1770a34291a0259" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "id": "6a9e80a7f1770a34291a0259",
  "standardCode": "BEKANA_AVOCADO_CONTAINER",
  "symbol": "BEKAD75E1B56",
  "chain": "celo",
  "identifier": "BEK-CTR-0001",
  "attributes": {
    "containerId": "BEK-CTR-0001",
    "commodityType": "avocado oil",
    "netWeightTons": 24
  },
  "quantity": 24,
  "quantityUnit": "tons",
  "redeemedQuantity": 5,
  "status": "partially_redeemed",
  "mintTxHash": "0x35ed18b6...",
  "verifiedBy": "SGS Kenya Surveyors"
}

Fields:

FieldTypeRequiredDescription
codestringYesYour own identifier for this standard, e.g. AVOCADO_OIL_CONTAINER. Unique per tenant.
namestringYesHuman-readable name.
symbolPrefixstringYesSeeds every receipt's on-chain asset code under this standard, e.g. RCPT, GOLD, INVC.
quantityUnitstringYesWhatever unit you deal in: tons, bars, USD, bags.
decimalsnumberNo (default 6)Token precision. Use 0 for whole units only (e.g. bars).
chainstringNoFixes every receipt under this standard to one chain. Omit to choose per-deposit.
requireVerificationbooleanNo (default true)false mints immediately on deposit — use when your own upstream process has already confirmed the claim.
fieldsarrayYesYour schema — see below.

Each field in fields:

FieldTypeDescription
namestringKey used in attributes at deposit time.
typestringstring, number, date, or bool.
requiredbooleanRejects a deposit missing this field.
rolestringOptional. Exactly one field must be "identifier" (type string, the uniqueness key) and exactly one must be "quantity" (type number, the mintable amount).

One engine, many shapes

Nothing about tons, containers, or any specific commodity is hardcoded. A gold vault tenant defines {vaultLotId: identifier, barCount: quantity} with decimals: 0; an invoice financier defines {invoiceNumber: identifier, faceValue: quantity} with quantityUnit: "USD". Same four endpoints below serve all of them.

Also available: GET /api/v1/tokenize/receipt/standards (list yours), GET /api/v1/tokenize/receipt/standards/:code (one), and DELETE /api/v1/tokenize/receipt/standards/:code (archive — existing receipts under it are unaffected, no new deposit can reference it).


Notes on the receipt workflow above ​

  • Deposit: attributes is validated against your standard's own field list — missing a required field is rejected before anything touches the chain (400 receipts: attributes do not satisfy the standard: missing required field "netWeightTons"). If requireVerification is false on the standard, deposit mints immediately instead — the response shows "status": "active" with a txHash, no separate verify call needed.
  • Verify: the trust checkpoint — nothing is minted until this is called. On a non-Stellar chain with no token factory configured, pass an explicit contract (a pre-deployed token address) in the body, same requirement the general Tokenization API has. Didn't check out? POST /api/v1/tokenize/receipt/:id/reject with a {"reason": "..."} body marks the claim rejected — nothing is ever minted for it.
  • Trade: no receipt-specific endpoint exists on purpose — once minted, a receipt is an ordinary registered asset, so the same /assets/:symbol/send every other asset uses (including partial amounts) is the whole mechanism.
  • Redeem: receiptStatus moves active → partially_redeemed → redeemed as the outstanding balance shrinks to zero. Over-redeeming is rejected before any chain call (400 amount 999 tons exceeds outstanding balance 13).
  • Check Status: GET /api/v1/tokenize/receipt?status=&standardCode= lists your receipts, newest first, filterable by either.

Full worked example

See Examples: Receipt Tokenization — Bekana's Marketplace for a complete end-to-end walkthrough: defining a standard, primary-market tokenization of a 24-ton container, secondary-market partial-cargo trading, and redemption — with real captured requests and on-chain transaction hashes from both Stellar and Celo.

Released under the MIT License.