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 GitHubThere 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.