NHBCoin
NHBCoin

Guide: The Loyalty Rewards Engine

The NHBCoin Loyalty Engine lets any business run its own merchant cashback/rewards program natively on-chain. A business registers itself, points a paymaster wallet at itself to fund rewards, registers the merchant addresses customers pay, and creates a reward program (e.g. "5% cashback on purchases at this address, capped per day"). Every one of these steps is a plain, directly-signed on-chain transaction β€” there is no separate gateway or relayer service to integrate with, unlike NHBCoin's escrow module.

Read the loyalty engine docs on GitHub

There Is No Loyalty Gateway

Each of the 8 operations below is signed with the business owner's own private key (or an address holding the ROLE_LOYALTY_ADMIN role) and submitted through the same node RPC every other NHBCoin transaction uses, nhb_sendTransaction. Your business already needs to hold NHB/ZNHB in its own wallet to fund its paymaster, so there's no extra key or gas requirement beyond what you need anyway.

The Model: Business β†’ Paymaster β†’ Merchant β†’ Program

Four concepts, built up in order:

  • Business β€” your top-level registration. The wallet that submits the registration transaction automatically becomes its owner.
  • Paymaster β€” the NHB/ZNHB-holding wallet you point your business at. This is what actually funds customer rewards; nothing more than an ordinary wallet you keep topped up.
  • Merchant β€” an address you register as belonging to your business. It's where customers pay β€” purchases sent to a registered merchant address become eligible for your reward program.
  • Program β€” the reward rule itself (a percentage or fixed reward, with spend/time caps). Programs are created and owned by whichever wallet signs the create transaction, and that wallet must already be a registered merchant of the business β€” a business's own owner is not automatically its own merchant.

Step 1: Register Your Business

Sign and submit a TxTypeCreateLoyaltyBusiness transaction. The sender becomes the owner outright β€” there's no "owner" field to set. Because transactions don't return a value the way an RPC call does, look up the ID the chain assigned you with the read-only loyalty_listBusinesses RPC afterward, passing your own address as owner.

TxTypeCreateLoyaltyBusiness (JavaScript Example)
// Registers a new business. The transaction sender becomes the owner
// outright -- there's no "owner" field in the payload, so you cannot
// register a business on someone else's behalf.
const createBusinessTx = {
  type: 0x42, // TxTypeCreateLoyaltyBusiness
  nonce: ownerAccount.nonce,
  data: JSON.stringify({
    name: "Joe's Coffee"
  }),
  // ...gas, etc.
};
const signedTx = await ownerWallet.signTransaction(createBusinessTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

// Transactions don't return a synchronous result, so there's no businessId
// handed back here. Look up the ID the chain just assigned you instead:
const businesses = await rpc.call('loyalty_listBusinesses', [
  { owner: ownerAddress }
]);
const businessId = businesses[0].id;

Step 2: Point a Paymaster at It

A TxTypeLoyaltySetPaymaster transaction tells the chain which wallet funds this business's programs. Must be signed by the business owner (or a ROLE_LOYALTY_ADMIN holder). Funding that wallet afterward is just a normal NHB/ZNHB transfer to its address.

TxTypeLoyaltySetPaymaster (JavaScript Example)
// Points the business at the wallet that will fund its programs' rewards.
const setPaymasterTx = {
  type: 0x43, // TxTypeLoyaltySetPaymaster
  nonce: ownerAccount.nonce,
  data: JSON.stringify({
    businessId, // 0x... (64 hex chars, from loyalty_listBusinesses)
    paymaster: paymasterAddress // nhb1... -- omit/empty to clear it
  }),
  // ...gas, etc.
};
const signedTx = await ownerWallet.signTransaction(setPaymasterTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

// Funding that wallet is just an ordinary NHB/ZNHB transfer to its
// address -- nothing loyalty-specific about it.

Step 3: Register Your Merchant Addresses

A TxTypeLoyaltyAddMerchant transaction assigns an address as a merchant of your business β€” the place customers actually pay. If you (the business owner) also want to create programs directly in the next step, register your own address as a merchant too; ownership alone doesn't grant that.

TxTypeLoyaltyAddMerchant (JavaScript Example)
// Marks an address as a merchant of the business. Purchases customers
// send to a registered merchant address become eligible for that
// business's reward programs.
const addMerchantTx = {
  type: 0x44, // TxTypeLoyaltyAddMerchant
  nonce: ownerAccount.nonce,
  data: JSON.stringify({ businessId, merchant: merchantAddress }),
  // ...gas, etc.
};
const signedTx = await ownerWallet.signTransaction(addMerchantTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

// To unassign it later, sign the same payload as TxTypeLoyaltyRemoveMerchant (0x45).

Step 4: Create a Reward Program

A TxTypeCreateLoyaltyProgram transaction defines the reward rule: pool (the address rewards are tracked/paid against), tokenSymbol, either accrualBps (percentage mode, e.g. 500 = 5%) or rewardMode: "fixed" with fixedRewardWei, plus optional caps β€” minSpendWei, capPerTx, dailyCapUser, dailyCapProgram, epochCapProgram with epochLengthSeconds, issuanceCapUser β€” and optional startTime/endTime/active. The transaction sender becomes the program's owner and must already be a registered merchant of the business (Step 3), or hold ROLE_LOYALTY_ADMIN.

TxTypeCreateLoyaltyProgram (JavaScript Example)
// Generate your own program ID -- the chain doesn't assign one.
const programId = '0x' + crypto.randomBytes(32).toString('hex');

const createProgramTx = {
  type: 0x46, // TxTypeCreateLoyaltyProgram
  // Must be signed by a wallet that is ALREADY a registered merchant of
  // this business (added via TxTypeLoyaltyAddMerchant above) -- a
  // business's own owner is not automatically its own merchant.
  nonce: merchantAccount.nonce,
  data: JSON.stringify({
    businessId,
    id: programId,
    pool: poolAddress,       // nhb1... rewards are tracked/paid against
    tokenSymbol: 'NHB',
    accrualBps: 500,         // 5% cashback (percentage mode is the default)
    dailyCapProgram: '1000000000000000000000' // required -- see note below
  }),
  // ...gas, etc.
};
const signedTx = await merchantWallet.signTransaction(createProgramTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

Why a Program-Wide Cap Is Mandatory

A program must set dailyCapProgram and/or epochCapProgram above zero β€” the chain rejects a program that only sets per-user caps. This is deliberate: without a program-wide ceiling, someone could split purchases across many wallets, each staying under the per-user cap, to farm effectively unlimited rewards out of your paymaster.

Step 5: Update a Program

TxTypeUpdateLoyaltyProgram is a full replace, not a patch β€” resend every field you want to keep, not just the one you're changing. The program's id and owner can never change; any value you send for them is ignored and the existing ones are preserved. Must be signed by the program's owner or a ROLE_LOYALTY_ADMIN holder.

TxTypeUpdateLoyaltyProgram (JavaScript Example)
// UpdateProgram is a full replace, not a patch -- resend every field
// you want to keep, not just the one you're changing.
const updateProgramTx = {
  type: 0x47, // TxTypeUpdateLoyaltyProgram
  nonce: programOwnerAccount.nonce,
  data: JSON.stringify({
    id: programId,           // the program's id/owner can never change
    pool: poolAddress,
    tokenSymbol: 'NHB',
    accrualBps: 750,         // raised from 5% to 7.5%
    dailyCapProgram: '1000000000000000000000' // must be resent even though it's unchanged
  }),
  // ...gas, etc.
};
const signedTx = await programOwnerWallet.signTransaction(updateProgramTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

Step 6: Pause or Resume a Program

TxTypePauseLoyaltyProgram / TxTypeResumeLoyaltyProgram toggle whether a program is actively accruing rewards, by id. Both are idempotent β€” pausing an already-paused program is a harmless no-op, not an error. Same signer requirement as updating.

TxTypePauseLoyaltyProgram / TxTypeResumeLoyaltyProgram (JavaScript Example)
// Pausing/resuming is idempotent -- pausing an already-paused program
// is a harmless no-op, not an error.
const pauseProgramTx = {
  type: 0x48, // TxTypePauseLoyaltyProgram
  nonce: programOwnerAccount.nonce,
  data: JSON.stringify({ id: programId }),
  // ...gas, etc.
};
const signedTx = await programOwnerWallet.signTransaction(pauseProgramTx);
await rpc.call('nhb_sendTransaction', [signedTx]);

// To resume, sign the same { id } payload as TxTypeResumeLoyaltyProgram (0x49).

A Note on Roles

Every operation above except the initial business registration can also be authorized by any address holding the on-chain ROLE_LOYALTY_ADMIN role, in addition to the business/program's own owner. That's a governance/operator-granted escape hatch for platform operators β€” most integrations only ever need the owner's own key.

The Old loyalty_* Write RPCs Are Permanently Disabled

If you've seen references to calling loyalty_createBusiness, loyalty_setPaymaster, loyalty_addMerchant, loyalty_removeMerchant, loyalty_createProgram, loyalty_updateProgram, loyalty_pauseProgram, or loyalty_resumeProgram as live JSON-RPC methods, that's out of date β€” they now return HTTP 410 Gone. They used to mutate chain state directly outside the normal transaction pipeline; use the signed transaction types on this page instead. Read-only lookups β€” loyalty_getBusiness, loyalty_listBusinesses, loyalty_listPrograms, loyalty_programStats, loyalty_userDaily, loyalty_paymasterBalance β€” are unaffected and still live.