MMOLove Docs
Referral KitSDK guides

Node / JS

A complete, copy-paste Node.js integration for the Referral Kit — one module using fetch + node:crypto, with signing, error handling, and a full Express example.

A complete Node.js integration: capture the mmref token, mint the anchor with registered, credit the referrer with qualified — all correctly signed.

The fastest path is the official @mmolove/referral package — it does the signing, the canonical body serialisation, and the POST for you:

npm i @mmolove/referral
import { ReferralClient } from "@mmolove/referral";

const client = new ReferralClient({
  serverId: process.env.MMOLOVE_SERVER_ID,
  secret: process.env.MMOLOVE_REFERRAL_SECRET, // Referrals tab
  // endpoint defaults to https://mmolove.gg/api/referral/events
});

// When a referred player signs up (refereeIdentity is REQUIRED — the first-touch anchor):
await client.registered({ token, refereeIdentity: "player-7842", serverEventId: "reg-7842" });

// When they hit your "qualified" bar:
const res = await client.qualified({ token, serverEventId: "qual-7842" });
if (!res.ok) console.error(res.status, res.body);

registered / qualified return { status, ok, body }; a 200 covers both new events and idempotent duplicates (same serverEventId), so retries are safe. Need to sign without sending (queue it, POST elsewhere)? Use client.sign(...) or the pure signReferralBody(...) helper. Requires Node ≥ 18 (global fetch).

The rest of this page is the from-scratch path — a single dependency-free module if you'd rather not add the package. It's exactly what the SDK wraps.

What you need

  • Your signing secret and server id from the Referrals tab.
  • A registration route that can read a query param / cookie.
  • A milestone hook (where you decide a player has "qualified").
// config.js — load from env; NEVER hardcode secrets.
export const MMOLOVE_ENDPOINT = "https://mmolove.com/api/referral/events";
export const MMOLOVE_SECRET = process.env.MMOLOVE_REFERRAL_SECRET; // Referrals tab
export const MMOLOVE_SERVER_ID = process.env.MMOLOVE_SERVER_ID;    // your server id

The module

mmolove-referral.js — the whole client. Build the body once, sign those exact bytes, send those exact bytes.

import crypto from "node:crypto";

const ENDPOINT = "https://mmolove.com/api/referral/events";

/**
 * Report a referral lifecycle event to MMOLove with an HMAC-signed body.
 * @returns {Promise<{status:number, body:string}>}
 */
export async function mmoloveSendEvent(secret, serverId, event, extra) {
  // Build the body ONCE. Sign these exact bytes; send these exact bytes.
  const payload = {
    event,                                  // "registered" | "qualified"
    server_id: serverId,
    ts: Math.floor(Date.now() / 1000),      // your event time
    ...extra,
  };
  const raw = JSON.stringify(payload);

  // HMAC-SHA256 over "<t>.<rawBody>" (timestamp, a dot, then the raw body).
  const t = Math.floor(Date.now() / 1000);
  const mac = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
  const sig = `t=${t},v1=sha256=${mac}`;

  try {
    const res = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-MMOLove-Signature": sig,
      },
      body: raw,                            // <-- the SAME bytes we signed
    });
    return { status: res.status, body: await res.text() };
  } catch (err) {
    // Transport failure (DNS/timeout/network). Treat like a 5xx: retry w/ backoff.
    return { status: 0, body: String(err) };
  }
}

1. Capture the mmref token at registration

MMOLove redirects referred players to your registration page with ?mmref=<token> (and a first-party mmref cookie as fallback). Read it and persist it on the new account.

// Express-style handler, BEFORE creating the account.
function captureMmref(req) {
  // Query first, cookie fallback (needs cookie-parser for req.cookies).
  return req.query.mmref ?? req.cookies?.mmref ?? null;
}

app.post("/register", async (req, res) => {
  const mmref = captureMmref(req);
  const account = await createAccount({ ...req.body, mmref });   // store it
  // ... continue to step 2
});

2. Report registered — mint the anchor

Right after the account exists, fire registered. This binds the referrer to this referee (first-touch). Use a stable identity (internal account id), not a renamable name.

import { mmoloveSendEvent } from "./mmolove-referral.js";
import { MMOLOVE_SECRET, MMOLOVE_SERVER_ID } from "./config.js";

if (account.mmref) {
  const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
    token: account.mmref,                 // the captured token
    referee_identity: String(account.id), // STABLE id
    server_event_id: `reg-${account.id}`, // idempotency key
  });
  handleResult(res);                      // see error handling below
}

3. Report qualified at your milestone

When the referee reaches your milestone, fire qualified with the same token and a new server_event_id. Then grant the in-game reward.

async function onPlayerQualified(player) {
  if (!player.mmref) return;              // not a referred player

  const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "qualified", {
    token: player.mmref,                  // SAME token as register
    server_event_id: `qual-${player.id}`, // NEW idempotency key
  });

  if (handleResult(res)) {
    // MMOLove credited the referrer. Now YOU grant the reward in-game.
    await grantReferrerReward(player);
  }
}

Error handling

/** @returns {boolean} true if the event was accepted (any 200). */
function handleResult({ status, body }) {
  let data = null;
  try { data = JSON.parse(body); } catch { /* non-JSON */ }

  if (status === 200) {
    // {ok:true,...} — advanced, duplicate, first_touch_conflict, or test.
    // All terminal; do NOT retry.
    return true;
  }

  if (status === 0 || status >= 500) {
    enqueueRetry({ status, body });       // backoff; same server_event_id
    return false;
  }

  // 4xx — a request bug; retrying won't help.
  console.error(`[mmolove] event rejected (${status}):`, data?.error ?? body);
  return false;
}
You getMeaningDo
200Accepted (incl. duplicate / first-touch / test)Stop. On qualified, grant the reward.
400Malformed requestFix the payload — Errors.
401Bad signature or stale tUsually the raw-body bug; or wrong/old secret; or clock drift.
404Unknown server/token, or referrals offCheck server_id, enable referrals, verify the token.
422Invalid transition (e.g. qualified-first)Send registered first.
0 / 5xxTransport / server errorRetry with backoff (idempotent via server_event_id).

Test it first

const res = await mmoloveSendEvent(MMOLOVE_SECRET, MMOLOVE_SERVER_ID, "registered", {
  token: "<mmref>",
  referee_identity: "<player>",
  server_event_id: "test-1",
  test: true,                            // dry run — verified, not written
});
// Expect: { status: 200, body: '{"ok":true,"test":true}' }

See Testing & sandbox for the full sequence and the dashboard Send test event button.

MMOLOVE_SECRET is server-side only. Never bundle it into client/browser code, and never commit it. If it leaks, rotate it on the Referrals tab.

Next

On this page