Skip to main content

Green Badges

Companies and student clubs run "green" CSR drives — beach clean-ups, tree-planting, recycling days — but the reward is usually a paper certificate that goes in the bin. This guide builds a small Node.js app that mints a collectible NFT badge to each volunteer: a verifiable, un-fakeable proof of what they contributed that lives in their wallet forever.

The Problem

Recognition for volunteering is forgettable and easy to fake — a PDF anyone can edit, a LinkedIn line nobody can check. A non-fungible token flips that: each badge is unique, owned by the volunteer's wallet, and carries on-chain attributes (trees planted, kg recycled, CO₂ offset) that a recruiter or sponsor can verify independently. It's a reward people actually keep.

What You'll Build

A minimal "green badge" backend that:

  • Creates an NFT contract for your CSR programme once, at setup,
  • Gives each volunteer a wallet to hold badges,
  • Mints a badge NFT with an image and impact attributes,
  • Lets volunteers hold and show their badge collection,
  • Optionally transfers a badge between wallets,
  • Receives the asynchronous result of each mint on a callback URL.

Services Used

Here is the sequence we are building during this tutorial:

Minting is asynchronous: the API returns a pending result with the nft_token_id immediately, then POSTs the final success/failed result to your callback_url. Reads return synchronously.


Preparation

1. Subscribe and get your API keys

In the Enterprise Portal, subscribe to Certificate and Wallet Management, then create an API key for your client_id and client_secret. See Calling APIs and API Keys Generation.

2. Create the Badge NFT Contract

Create an NFT smart contract for your programme — this is the collection your badges belong to. The wallet_address_owner you set is the only wallet allowed to mint, so use a wallet you control. You receive a contract_address.

Create the NFT contract (one-time)
// POST /api/certificate/create-smartcontract
{
"wallet_address": "0x<owner_wallet>", // deploys the contract
"name": "GoGreenBadges", // contract nickname
"field": {
"wallet_address_owner": "0x<owner_wallet>", // ONLY this wallet can mint
"max_supply": 0, // 0 = unlimited
"name": "GoGreen Badge",
"symbol": "GREEN"
},
"callback_url": "https://your.domain/callback"
}

The contract_address is confirmed on the success callback. See Certificate → Create Smart Contract and Smart Contract Creation.

3. Set up the Project

Node.js 18+ (for the built-in fetch, FormData, and Blob). Keep credentials in .env:

.env
MASCHAIN_API_URL=https://service-testnet.maschain.com
MASCHAIN_CLIENT_ID=your_client_id
MASCHAIN_CLIENT_SECRET=your_client_secret

# From step 2:
BADGE_CONTRACT=0x<nft_contract_address>
# The contract owner — the only wallet allowed to mint:
OWNER_WALLET=0x<owner_wallet>
# Where MasChain POSTs async results:
CALLBACK_URL=https://your.domain/callback
npm install express dotenv
Testnet vs Mainnet

Develop on https://service-testnet.maschain.com; switch to https://service.maschain.com for production. View badges at explorer-testnet.maschain.com.


MasChain Client

Reads and the contract call use JSON; minting a badge uploads a file, so it needs a multipart/form-data helper. Note the form helper does not set a content-typefetch sets the multipart boundary automatically:

maschain.js
const fs = require('fs');
const BASE_URL = process.env.MASCHAIN_API_URL;

const AUTH = {
client_id: process.env.MASCHAIN_CLIENT_ID,
client_secret: process.env.MASCHAIN_CLIENT_SECRET,
};

async function post(path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method: 'POST',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.status !== 200) throw new Error(`MasChain error: ${JSON.stringify(json)}`);
return json.result;
}

// multipart/form-data POST — for endpoints that upload a file.
async function postForm(path, fields, filePath) {
const form = new FormData();
for (const [k, v] of Object.entries(fields)) form.set(k, v);
const bytes = fs.readFileSync(filePath);
form.set('file', new Blob([bytes]), filePath.split('/').pop());

const res = await fetch(`${BASE_URL}${path}`, { method: 'POST', headers: AUTH, body: form });
const json = await res.json();
if (json.status !== 200) throw new Error(`MasChain error: ${JSON.stringify(json)}`);
return json.result;
}

async function get(path, params = {}) {
const url = new URL(`${BASE_URL}${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: AUTH });
const json = await res.json();
if (json.status !== 200) throw new Error(`MasChain error: ${JSON.stringify(json)}`);
return json.result;
}

module.exports = { post, postForm, get };

1. Give Each Volunteer a Wallet

Each volunteer needs a wallet to receive and hold badges. The returned wallet_address is where their NFTs live:

gogreen.js
const { post, postForm, get } = require('./maschain');

// POST /api/wallet/create-user
async function createVolunteerWallet({ name, email, ic }) {
const result = await post('/api/wallet/create-user', { name, email, ic });
return result.wallet.wallet_address; // 0x...
}

See Wallet Management → Create User Wallet.

2. Mint a Badge

The core action. Mint from the owner wallet to the volunteer's wallet, attaching a badge image and the impact as attributes. Minting is a file upload, so use the postForm helper:

gogreen.js
// POST /api/certificate/mint-certificate  (multipart/form-data)
async function awardBadge({ volunteerWallet, title, description, attributes, imagePath }) {
return postForm('/api/certificate/mint-certificate', {
wallet_address: process.env.OWNER_WALLET, // must be the contract owner
to: volunteerWallet, // volunteer receiving the badge
contract_address: process.env.BADGE_CONTRACT,
name: title, // "Beach Clean-up 2026"
description,
attributes: JSON.stringify(attributes), // impact metrics
callback_url: process.env.CALLBACK_URL,
}, imagePath);
}
Sample result (immediate)
{
"status": 200,
"result": {
"nft_token_id": 17,
"transactionHash": "0xf519ba69ba0e603583e0e885786f5ad1...",
"receiver_wallet_address": "0x147f20a28739da15419AdC0...",
"certificate": "https://storage.maschain.com/.../metadata/17.json",
"certificate_image": "https://storage.maschain.com/.../image/badge.png",
"status": "pending"
}
}

The attributes are what make the badge verifiable — for example:

attributes example
[
{ "trait": "Activity", "value": "Tree Planting" },
{ "trait": "Trees Planted", "value": "5" },
{ "trait": "CO2 Offset (kg)", "value": "110" },
{ "trait": "Date", "value": "2026-07-05" }
]

For larger images, use mint-certificate-small / -medium / -large (up to 10 / 30 / 100 MB). See Certificate → Mint NFT.

3. Show a Volunteer's Badge Collection

Reads are synchronous. Pull every badge a wallet has received:

gogreen.js
// GET /api/certificate/get-certificate
async function getBadges(volunteerWallet) {
return get('/api/certificate/get-certificate', {
to: volunteerWallet,
contract_address: process.env.BADGE_CONTRACT,
status: 'success',
});
}

Each entry includes the nft_token_id, transactionHash, and the token's certificate metadata URL — a public, verifiable record of the volunteer's contribution.

4. Transfer a Badge (optional)

Because it's a real NFT, a badge can move between wallets — handy if a volunteer migrates to a personal wallet:

gogreen.js
// POST /api/certificate/transfer
async function transferBadge({ fromWallet, toWallet, nftId }) {
return post('/api/certificate/transfer', {
wallet_address: fromWallet,
to: toWallet,
nft_id: String(nftId),
contract_address: process.env.BADGE_CONTRACT,
callback_url: process.env.CALLBACK_URL,
});
}

5. Receive Async Result (Callback)

Minting finishes out-of-band. Stand up an endpoint at your CALLBACK_URL to confirm the badge actually minted before telling the volunteer:

callback-server.js
const express = require('express');
const app = express();
app.use(express.json());

app.post('/callback', (req, res) => {
const { status, transactionHash } = req.body.result || req.body;
if (status === 'success') {
console.log(`${transactionHash} confirmed — badge minted`);
} else {
console.log(`${transactionHash} failed: ${req.body.result?.message}`);
}
res.sendStatus(200);
});

app.listen(3000, () => console.log('Listening for MasChain callbacks on :3000'));
Announce a badge only on success

The immediate response is only pending. Wait for the success callback before telling the volunteer their badge is live. On failed, don't announce it — retry.


Putting It Together

demo.js
require('dotenv').config();
const { createVolunteerWallet, awardBadge, getBadges } = require('./gogreen');

(async () => {
// Onboard a volunteer
const wallet = await createVolunteerWallet({
name: 'Amir', email: 'amir@example.com', ic: '030101-01-1234',
});
console.log('Volunteer wallet:', wallet);

// Award a badge for a tree-planting drive
const badge = await awardBadge({
volunteerWallet: wallet,
title: 'Tree Planting Drive 2026',
description: 'Planted 5 trees at the Bukit reforestation site.',
attributes: [
{ trait: 'Activity', value: 'Tree Planting' },
{ trait: 'Trees Planted', value: '5' },
{ trait: 'CO2 Offset (kg)', value: '110' },
],
imagePath: './badge.png',
});
console.log('Minted (pending):', badge.nft_token_id);

// ...after the success callback, show their collection
console.log('Badges:', await getBadges(wallet));
})();

Run it:

node demo.js

Watch each badge mint in the MasChain Explorer, and your callback-server.js log the success result. Volunteers walk away with a collectible they own — not a certificate they'll lose.

Next steps

Sticky Icon