Skip to content

Project: Build a Tokenized Commodity Marketplace ​

A complete, runnable project modeled on Bekana Traders Ltd, who run a marketplace for tokenized avocado oil containers built entirely on the Receipt Tokenization API — self-service, using only Bekana's own tenant credentials, no platform operator involvement past the initial tenant setup. Every container is its own receipt; Bekana's model has two distinct markets over the same token:

  • Primary market — a verified 24-ton container becomes exactly one minted lot of 24 tokens (1 token = 1 ton), landing entirely in the exporter's wallet the moment it's verified.
  • Secondary market — anyone can then buy, sell, or use as collateral any fraction of that lot — a buyer who only wants 6 tons never needs to touch the other 18. No separate mechanism for this: it's the same /assets/:symbol/send every asset already uses.
▸API ConfigurationNot configured
Every endpoint, payload, and response on this page follows this choice.

One container, one chain, start to finish

Bekana fixes chain on the Standard itself (Step 1), so every receipt minted under it lives on one chain for its whole lifecycle — pick Celo or Stellar below and the rest of the project follows that same container.

What you'll build ​

A marketplace.js script covering Bekana's whole lifecycle: define a schema once, tokenize a verified container as a primary-market lot, trade a partial position on the secondary market, and redeem it for physical delivery.

Prerequisites ​

  • Node.js 18+ (for global fetch)
  • Bekana's own tenant API key/secret — created once via POST /api/v1/admin/tenants by the platform operator, after which Bekana never needs the platform's own service token for anything in this project

Project setup ​

bash
mkdir bekana-marketplace && cd bekana-marketplace && npm init -y && npm install dotenv

.env (Bekana's own tenant credentials, not the platform operator's):

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

Create marketplace.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: Define the standard (once) ​

Define 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
    },
    {
      "name": "qualityGrade",
      "type": "string",
      "required": false
    }
  ]
}'
Response201
{
  "code": "BEKANA_AVOCADO_CONTAINER",
  "chain": "celo",
  "status": "active"
}

Step 2: Primary market — the exporter tokenizes a container ​

Container BEK-CTR-0001, deposited then verified:

Deposit (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",
    "qualityGrade": "Extra Virgin, Grade A"
  }
}'
Response201
{
  "id": "6a9e80a7f1770a34291a0259",
  "symbol": "BEKAD75E1B56",
  "status": "pending_verification"
}
Verify (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",
  "mintedQuantity": 24,
  "to": "0xcf0981b26e0e022f4323da24e092f30d151bf4e8",
  "txHash": "0x35ed18b655cfc1b8d773b785276a1d5d36546e2a91b78a401c45530ff24ba629"
}

This is the whole primary market: one verified container, one lot of 24 tokens, one transaction, landing entirely with the exporter. Confirmed on Celo Sepolia — eth_getTransactionReceipt shows a Transfer event of 24000000 (24 × 10⁶, matching the standard's decimals: 6) from the zero address to the exporter's wallet.

Step 3: Secondary market — a buyer takes a quarter-container position ​

Trade 6 tons (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,
  "txHash": "0xca80211115aa22608bf79f5771ed4a80dbff2973b77cbf3c2ae9712a05b722af"
}

The buyer now holds a genuine, independently tradable claim on 6 of the 24 tons — confirmed via balance check on both sides:

Exporter's remaining balance
curl -X GET "http://localhost:8100/api/v1/assets/BEKAD75E1B56/balance?chain=celo&externalUserId=bekana-exporter-1" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "balance": "18000000",
  "humanAmount": 18
}
Buyer's new balance
curl -X GET "http://localhost:8100/api/v1/assets/BEKAD75E1B56/balance?chain=celo&externalUserId=bekana-buyer-1" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "balance": "6000000",
  "humanAmount": 6
}

The buyer never had to purchase, hold, or even be capable of holding the whole 24-ton container — that's exactly the point of a secondary market for a physical asset this large. The same pattern extends to using a partial position as loan collateral, splitting it further among sub-buyers, or redeeming just that portion when the buyer takes physical delivery — all without any container-specific code beyond the standard Bekana defined once at the start.

Step 4: Settlement — redeeming for physical delivery ​

When a holder collects their share of the physical cargo, they redeem their on-chain position, permanently reducing the receipt's outstanding balance:

Redeem 6 tons (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-buyer-1",
  "amount": 6,
  "externalId": "buyer-1-redeem"
}'
Response200
{
  "redeemedAmount": 6,
  "outstanding": 18,
  "receiptStatus": "partially_redeemed"
}

Once every ton across every holder has been redeemed, receiptStatus reaches redeemed and the ledger's receipt_inventory:bekana_avocado_container account nets back to zero — the on-chain supply and the real, physically redeemed cargo are back in balance.

Step 5: Run the whole lifecycle as one script ​

Add main() to marketplace.js:

js
async function main() {
  // 1. Define the standard (once)
  await comet('POST', '/api/v1/tokenize/receipt/standards', {
    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' },
    ],
  });
  console.log('standard defined');

  // 2. Primary market: tokenize a verified container
  const deposit = await comet('POST', '/api/v1/tokenize/receipt/deposit', {
    externalUserId: 'bekana-exporter-1', standardCode: 'BEKANA_AVOCADO_CONTAINER',
    attributes: { containerId: 'BEK-CTR-0001', commodityType: 'avocado oil', netWeightTons: 24, warehouseLocation: 'Mombasa Bonded Warehouse, Kenya' },
  });
  const verified = await comet('POST', `/api/v1/tokenize/receipt/${deposit.id}/verify`, { verifiedBy: 'SGS Kenya Surveyors' });
  console.log(`minted ${verified.mintedQuantity} tons as ${deposit.symbol} to`, verified.to);

  // 3. Secondary market: sell 6 of the 24 tons
  const trade = await comet('POST', `/api/v1/assets/${deposit.symbol}/send?chain=celo`, {
    externalUserId: 'bekana-exporter-1', to: '0x176d431dbd00639e13177ef0c397a9fa2eb77a7b', amountBase: 6000000,
  });
  console.log('traded 6 tons:', trade.txHash);

  // 4. Settlement: buyer redeems their 6 tons for physical delivery
  const redeemed = await comet('POST', `/api/v1/tokenize/receipt/${deposit.id}/redeem`, {
    externalUserId: 'bekana-buyer-1', amount: 6, externalId: 'buyer-1-redeem',
  });
  console.log(`redeemed 6 tons, ${redeemed.outstanding} outstanding, status: ${redeemed.receiptStatus}`);
}

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

What you built ​

  • A tenant-owned, self-service marketplace — no platform-operator involvement past the initial tenant setup.
  • A real primary/secondary market split over one physical asset: one verified lot, infinitely divisible trading, redemption that always nets the ledger back to zero.

Next steps: add more standards for other commodities, or put Approvals in front of the verify step if Bekana wants a second surveyor to co-sign before a container's tokens are minted.

Released under the MIT License.