I’ve got you a drop-in pricing module + route example so you can add markup in minutes.

What this gives you

Flat and/or percent markup (stackable)

Optional per-TLD overrides (e.g., .ai gets a bigger markup)

Clean utility you can reuse anywhere (search results, checkout)

1) Add this file to your backend (e.g., server/lib/markup.js)
// server/lib/markup.js

/**
 * Pricing/markup utilities for domain sales.
 * - Supports flat + percent markup
 * - Per-TLD overrides (e.g., .ai, .com, etc.)
 * - Rounds to 2 decimals
 */

const DEFAULTS = {
  flat: Number(process.env.MARKUP_FLAT || 3.00),      // $3 flat by default
  percent: Number(process.env.MARKUP_PERCENT || 0),   // 0% default
  // Per-TLD overrides (optional). Keys are lowercase, include the dot.
  perTld: {
    // ".com": { flat: 5, percent: 0 },         // example
    // ".ai":  { flat: 20, percent: 10 },       // example
  },
};

function round2(n) {
  return Math.round((Number(n) + Number.EPSILON) * 100) / 100;
}

function applyMarkup({ wholesale, tld, config = {} }) {
  const cfg = {
    flat: DEFAULTS.flat,
    percent: DEFAULTS.percent,
    perTld: { ...DEFAULTS.perTld, ...(config.perTld || {}) },
  };

  const key = (tld || "").toLowerCase();
  const ovr = cfg.perTld[key] || {};
  const flat = (ovr.flat ?? cfg.flat);
  const percent = (ovr.percent ?? cfg.percent);

  let retail = Number(wholesale);
  if (!Number.isFinite(retail)) throw new Error("Invalid wholesale price");

  // percent first, then flat (order doesn’t matter much, but keeps it consistent)
  retail = retail * (1 + (Number(percent) / 100));
  retail = retail + Number(flat);

  return round2(retail);
}

module.exports = { applyMarkup };

2) Use it in your availability route (example routes/domains.js)
// routes/domains.js

const express = require("express");
const router = express.Router();
const fetch = require("node-fetch"); // or your OpenSRS client
const { applyMarkup } = require("../lib/markup");

// Helper to normalize TLD from a domain, e.g., "example.com" -> ".com"
function extractTld(domain) {
  const m = String(domain).toLowerCase().match(/(\.[a-z0-9-]+)$/i);
  return m ? m[1] : "";
}

// Example: call OpenSRS Lookup API (pseudo; replace with your client call)
async function lookupWholesalePrice(domain) {
  // You should already have this implemented. This is just a stub:
  // Return an object with wholesale prices you care about.
  return {
    domain,
    tld: extractTld(domain),
    registration: 14.5,   // <- from OpenSRS response
    renewal: 14.5,        // <- from OpenSRS response
    transfer: 0,          // optional
  };
}

router.get("/availability", async (req, res) => {
  try {
    const { q } = req.query; // domain name
    if (!q) return res.status(400).json({ error: "Missing ?q=domain" });

    // If OpenSRS auth is failing, short-circuit to your demo logic here.
    const wholesale = await lookupWholesalePrice(q);

    const registrationRetail = applyMarkup({
      wholesale: wholesale.registration,
      tld: wholesale.tld,
      // Optional: pass per-TLD overrides here
      // config: { perTld: { ".ai": { flat: 25, percent: 15 } } }
    });

    const renewalRetail = applyMarkup({
      wholesale: wholesale.renewal,
      tld: wholesale.tld,
    });

    return res.json({
      domain: wholesale.domain,
      tld: wholesale.tld,
      available: true, // or use actual OpenSRS availability flag when auth works
      pricing: {
        wholesale: {
          registration: wholesale.registration,
          renewal: wholesale.renewal,
        },
        retail: {
          registration: registrationRetail,
          renewal: renewalRetail,
        },
        currency: "USD",
      },
    });
  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: "availability_failed" });
  }
});

module.exports = router;

3) Frontend (what to display)

Where you render the search results, show:

pricing.retail.registration to the user

Keep pricing.wholesale.registration hidden (or for admin view)

Example display:

const price = data?.pricing?.retail?.registration; // e.g., 19.5
// Render as `$19.50/yr`

4) Stripe checkout (charge the retail)

When you create the Stripe Payment/Session, pass the retail number you computed above. If you use Price objects, you can create one-time Prices dynamically, or use Payment Links with custom amounts. For simple MVP, many teams use a paymentIntent with amount = Math.round(retail * 100) and currency = 'usd'.

Env controls you can set now

MARKUP_FLAT=5 (adds $5)

MARKUP_PERCENT=10 (adds 10%)

Per-TLD overrides are inside markup.js (or inject via a JSON config if you prefer).

If you paste these two files/blocks in the right spots, you’ll have real markup flowing today. Want me to tailor the per-TLD table for your exact pricing strategy (.com, .net, .org, .ai, etc.)?