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.
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).
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"
}'{
"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:
| Field | Type | Required | Description |
|---|---|---|---|
userId | number | Yes | Recipient user |
amountBase | string | Yes | Amount in base units (6 decimals: 10000000 = 10 IMC) |
externalId | string | Yes | Idempotency key (prevents double-mint) |
chain | string | No | Target 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:
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"
}'{
"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
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"{
"id": "airtime-001",
"status": "confirmed",
"txHash": "0x...",
"amount": 10000000
}Delegated Endpoints
| Endpoint | Status | Notes |
|---|---|---|
POST /api/v1/tokenize/deposit | Delegates | Handled by app-core-backend |
GET /api/v1/tokenize/status/:id | Delegates | Proxied 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:
CELO_TESTNET_IMC_CONTRACT=0x1D1aA006f66A191fc04e7d1cab8D62B0c81d9221
CELO_IMC_TYPE=airtime
CELO_IMC_BACKING=airtimeOr register at runtime:
🔒 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"
}'{
"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 assetWorks 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.
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
}
]
}'{ "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)
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"
}
}'{
"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)
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"
}'{
"status": "success",
"id": "6a9e80a7f1770a34291a0259",
"symbol": "BEKAD75E1B56",
"chain": "celo",
"mintedQuantity": 24,
"quantityUnit": "tons",
"to": "0xcf0981b26e0e022f4323da24e092f30d151bf4e8",
"txHash": "0x35ed18b655cfc1b8d773b785276a1d5d36546e2a91b78a401c45530ff24ba629"
}Trade (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
}'{
"amount": 6,
"chain": "celo",
"symbol": "BEKAD75E1B56",
"txHash": "0xca80211115aa22608bf79f5771ed4a80dbff2973b77cbf3c2ae9712a05b722af"
}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"
}'{
"status": "success",
"id": "6a9e80a7f1770a34291a0259",
"symbol": "BEKAD75E1B56",
"redeemedAmount": 5,
"quantityUnit": "tons",
"outstanding": 13,
"receiptStatus": "partially_redeemed",
"txHash": "0x..."
}Check Status (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"{
"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:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Your own identifier for this standard, e.g. AVOCADO_OIL_CONTAINER. Unique per tenant. |
name | string | Yes | Human-readable name. |
symbolPrefix | string | Yes | Seeds every receipt's on-chain asset code under this standard, e.g. RCPT, GOLD, INVC. |
quantityUnit | string | Yes | Whatever unit you deal in: tons, bars, USD, bags. |
decimals | number | No (default 6) | Token precision. Use 0 for whole units only (e.g. bars). |
chain | string | No | Fixes every receipt under this standard to one chain. Omit to choose per-deposit. |
requireVerification | boolean | No (default true) | false mints immediately on deposit — use when your own upstream process has already confirmed the claim. |
fields | array | Yes | Your schema — see below. |
Each field in fields:
| Field | Type | Description |
|---|---|---|
name | string | Key used in attributes at deposit time. |
type | string | string, number, date, or bool. |
required | boolean | Rejects a deposit missing this field. |
role | string | Optional. 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:
attributesis 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"). IfrequireVerificationisfalseon the standard, deposit mints immediately instead — the response shows"status": "active"with atxHash, 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/rejectwith 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/sendevery other asset uses (including partial amounts) is the whole mechanism. - Redeem:
receiptStatusmovesactive→partially_redeemed→redeemedas 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.