Skip to main content

Code Streak

Every day you code, you post #100DaysOfCode on X — but anyone can fake a streak by backdating a pile of posts the night before. This guide builds a tiny Node.js app that logs each day's progress to an on-chain audit trail: tamper-proof, timestamped, and impossible to fake after the fact. At the end you get a "verify my streak" script that proves every entry is real and unchanged.

The Problem

A streak only means something if it's honest. Screenshots get edited, dates get faked, and "trust me, I coded every day" is worth nothing to a recruiter looking at your GitHub. What if each day's entry carried a cryptographic timestamp nobody — not even you — could alter later?

That's exactly what an audit trail gives you. Each entry stores a hash of your data on-chain with the exact time it was recorded. Change a single character afterwards and the hash no longer matches — tampering is detectable by anyone.

What You'll Build

A minimal "streak logger" backend that:

  • Creates an audit trail contract once, at setup,
  • Gives you a wallet that signs your entries,
  • Logs one tamper-proof record per day (what you built, your repo link),
  • Organises entries with a category and tags (e.g. frontend, shipped),
  • Lists your whole streak on demand,
  • Verifies any entry's data against its on-chain hash — the honesty check,
  • Receives the asynchronous result of each write on a callback URL.

Services Used

  • Audit Trail — record each day's progress as a tamper-proof, timestamped entry on-chain.
  • Wallet Management — create the wallet that signs your audit entries.

Here is the sequence we are building during this tutorial:

Writes (logging a day) are asynchronous: the API immediately returns a pending transaction hash plus the metadatahash, then POSTs the final success/failed result to your callback_url. Reads (listing, verifying) return synchronously.


Preparation

1. Subscribe and get your API keys

In the Enterprise Portal, subscribe to Audit Trail and Wallet Management, then create an API key. You'll receive a client_id and client_secret — these authenticate every request. See Calling APIs and API Keys Generation.

2. Create the Audit Trail Contract

Create an Audit Trail smart contract — this is the ledger your daily entries are written to. Leave encrypt_data as false so entries stay publicly readable (the whole point is that anyone can verify them). You receive a contract_address.

You can do this in the portal, or via the API:

Create the audit contract (one-time)
// POST /api/audit/contracts
{
"name": "100DaysOfCode",
"field": { "encrypt_data": false },
"callback_url": "https://your.domain/callback"
}

The response is pending; the contract_address is confirmed on the success callback. See Audit Trail → Create Smart Contract and Smart Contract Creation.

3. Set up the Project

You need Node.js 18+ (for the built-in fetch) and one small library to verify hashes later. Put your credentials in an .env file — never hard-code them:

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

# From step 2:
AUDIT_CONTRACT=0x<audit_contract_address>
# The wallet that signs your entries (created in step 1 of the guide):
LOGGER_WALLET=0x<your_wallet_address>
# Where MasChain POSTs async results:
CALLBACK_URL=https://your.domain/callback
npm install express dotenv bcryptjs
Testnet vs Mainnet

Use https://service-testnet.maschain.com while developing. Switch to https://service.maschain.com for production. Explore your entries at explorer-testnet.maschain.com.


MasChain Client

Every call hits the same base URL with the same auth headers, so wrap that once and reuse it:

maschain.js
const BASE_URL = process.env.MASCHAIN_API_URL;

const HEADERS = {
client_id: process.env.MASCHAIN_CLIENT_ID,
client_secret: process.env.MASCHAIN_CLIENT_SECRET,
'content-type': 'application/json',
};

// POST helper — returns `result`, throws on any non-200 status.
async function post(path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(body),
});
const json = await res.json();
if (json.status !== 200) throw new Error(`MasChain error: ${JSON.stringify(json)}`);
return json.result;
}

// GET helper — appends query params.
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: HEADERS });
const json = await res.json();
if (json.status !== 200) throw new Error(`MasChain error: ${JSON.stringify(json)}`);
return json.result;
}

module.exports = { post, get };

1. Create Your Logger Wallet

Create one wallet — it signs every entry you log. The returned wallet_address is what appears as the author (from) of each record:

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

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

Save the returned address as LOGGER_WALLET in your .env. See Wallet Management → Create User Wallet.

2. Organise with a Category and Tags (optional, one-time)

Categories and tags make your streak easy to slice later — "show me every day I shipped something" or "every day I fought a bug". Create them once and reuse the returned IDs:

streak.js
// POST /api/audit/category  → { id, name, ... }
async function createCategory(name) {
const result = await post('/api/audit/category', { name });
return result.id;
}

// POST /api/audit/tag → { id, name, ... }
async function createTag(name) {
const result = await post('/api/audit/tag', { name });
return result.id;
}

For example: one category 100DaysOfCode, and tags like frontend, backend, bug, shipped. See Audit Category and Audit Tags.

3. Log Today's Progress (create audit trail)

The heart of the app. Each day, write one record. The metadata is whatever you want to prove — keep it as a JSON string so it hashes deterministically. The response is pending and already includes the metadatahash; the final outcome arrives at your callback:

streak.js
// POST /api/audit/audit
async function logDay({ day, summary, repo, categoryId, tagIds }) {
// Serialise once so the exact same string is what gets hashed and stored.
const metadata = JSON.stringify({ day, summary, repo });

return post('/api/audit/audit', {
wallet_address: process.env.LOGGER_WALLET,
contract_address: process.env.AUDIT_CONTRACT,
metadata,
category_id: categoryId ? [categoryId] : [],
tag_id: tagIds || [],
callback_url: process.env.CALLBACK_URL,
});
}
Sample result (immediate)
{
"status": 200,
"result": {
"transactionHash": "0x76578bb22a17d1fa06165570...",
"nonce": 175,
"status": "pending",
"metadatahash": "$2y$12$JCdgqkB1QKI5cRHTVaQXqu2JZPMj5MH8qT6GU7vb0NR4ONjgR1i62",
"metadata": "{\"day\":7,\"summary\":\"built the login page\",\"repo\":\"...\"}",
"from": "0x44Ce5799F1d0672e6577..."
}
}

That metadatahash is the magic — a salted hash of your exact data, anchored on-chain. Hold onto the entry until the callback confirms it.

4. Show Your Streak (list entries)

Reads return synchronously — no callback needed. List every confirmed entry your wallet has written:

streak.js
// GET /api/audit/get-audit-transaction
async function getStreak() {
return get('/api/audit/get-audit-transaction', {
wallet_address: process.env.LOGGER_WALLET,
contract_address: process.env.AUDIT_CONTRACT,
filter: 'from', // entries authored by you
status: 'success', // only confirmed days count
});
}
Sample result
{
"status": 200,
"result": [
{
"from": "0x44Ce5799F1d0672e6577C4F0038729177dB65FD7",
"status": "success",
"blockNumber": 8717499,
"transactionHash": "0x899bcb62f70b9ac51da499f0bcdebbf4ec528ed37cc00596c4598b1e0ec42d6c",
"method": "mint",
"timestamp": "2025-09-11T04:16:05.000000Z",
"metadata": "{\"day\":7,\"summary\":\"built the login page\",\"repo\":\"...\"}"
}
]
}

result.length is your streak. Each timestamp is the on-chain proof of when you logged that day — no backdating possible.

5. Verify Honesty (the tamper check)

This is the payoff. Fetch any single entry and check its stored metadata against the on-chain metadatahash. Because the hash is anchored on-chain, a match proves the data is exactly what you committed that day — and a mismatch proves someone edited it:

streak.js
const bcrypt = require('bcryptjs');

// GET /api/audit/audit/{id}
async function verifyEntry(id) {
const entry = await get(`/api/audit/audit/${id}`);
const untampered = bcrypt.compareSync(entry.metadata, entry.metadatahash);
return { id, untampered, metadata: entry.metadata };
}
Verify output
{ id: 439, untampered: true, metadata: '{"day":7,"summary":"built the login page","repo":"..."}' }

Try it: change one character of the stored metadata and re-run — untampered flips to false. That's a proof anyone can reproduce, which is what makes the streak trustworthy.

6. Receive Async Result (Callback)

Logging a day finishes out-of-band. Stand up an endpoint at your CALLBACK_URL to record the outcome — this is where you confirm the day was actually written before counting it toward your streak:

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

// MasChain POSTs the final transaction result here.
app.post('/callback', (req, res) => {
const { status, transactionHash } = req.body;

if (status === 'success') {
// Mark the day as settled, increment the streak, celebrate.
console.log(`${transactionHash} confirmed — streak +1`);
} else {
// status === 'failed' — the day didn't record; prompt a retry.
console.log(`${transactionHash} failed: ${req.body.message}`);
}

res.sendStatus(200); // acknowledge receipt
});

app.listen(3000, () => console.log('Listening for MasChain callbacks on :3000'));
Sample callback (success)
{
"status": "success",
"from": "0x1a0BA2b4d8830496Beb8469...",
"nonce": 129,
"transactionHash": "0xf519ba69ba0e603583e0e885786f5ad1...",
"receipt": { }
}
Count a day only on success

The immediate API response is only pending. Wait for a success callback before adding the day to your streak. On failed, don't count it — surface the error and retry. See the callback shapes in the Audit Trail reference.


Putting It Together

demo.js
require('dotenv').config();
const {
createCategory, createTag, logDay, getStreak, verifyEntry,
} = require('./streak');

(async () => {
// One-time setup (do this once, then hard-code the IDs)
const categoryId = await createCategory('100DaysOfCode');
const shipped = await createTag('shipped');

// Log today
const entry = await logDay({
day: 7,
summary: 'Built the login page and wired up auth',
repo: 'https://github.com/you/your-project',
categoryId,
tagIds: [shipped],
});
console.log('Logged (pending):', entry.transactionHash);

// ...after the success callback, check your streak
const streak = await getStreak();
console.log(`🔥 Current streak: ${streak.length} days`);

// Prove day 7 is honest
console.log(await verifyEntry(439));
})();

Run it:

node demo.js

Watch each entry confirm in the MasChain Explorer, and your callback-server.js log the success result. When you hit day 100, your streak isn't a claim — it's a chain of proofs.

Next steps

Sticky Icon