here’s the super-tight wiring so it actually unlocks “no attribution” (or grants a pack) end-to-end.

1) Stripe (what the price should carry)

Pick one of these patterns for the price’s metadata (you can support both):

Global unlock (no attribution anywhere):

license_type=icons_no_attr

(optional) title=Icons — No Attribution

Icon pack (own specific icons):

license_type=icon_pack

pack_id=modern_starter ← your slug

(optional) icons_granted=20

Keep the lookup key handy (e.g. icons_no_attr_499 or icon_pack_modern_199).

2) Checkout route (no changes needed)

Your cart already supports { kind: "price", lookupKey }. Just add a UI button that pushes that:

// in Icons UI, next to "Download Free (Attribution)"
addItem({
  kind: "price",
  lookupKey: "icons_no_attr_499",
  name: "Icons — No Attribution",
  qty: 1,
});
open(); // open cart


(For a pack, use the pack lookup key.)

3) Webhook: grant entitlements on purchase

Drop this into your existing checkout.session.completed handler (after you get session):

const full = await stripe.checkout.sessions.retrieve(session.id, {
  expand: ['line_items.data.price.product'],
});
const userId = session.metadata?.userId || 'anon';

for (const li of full.line_items.data) {
  const price = li.price;
  const meta = price?.metadata || {};

  if (meta.license_type === 'icons_no_attr') {
    await Entitlements.grantIconsNoAttribution(userId);
    console.log('✅ Icons: no-attribution unlocked', { userId });
  }

  if (meta.license_type === 'icon_pack' && meta.pack_id) {
    await Entitlements.grantIconPack(userId, meta.pack_id, Number(meta.icons_granted || 0));
    console.log('✅ Icons: pack granted', { userId, pack: meta.pack_id });
  }
}


Minimal entitlements service:

// services/entitlements.js
const db = require('../db'); // however you persist

async function grantIconsNoAttribution(userId) {
  await db.entitlements.upsert(userId, { iconsNoAttr: true, updatedAt: Date.now() });
}

async function grantIconPack(userId, packId, count = 0) {
  await db.entitlements.addPack(userId, packId, count);
  // optionally also mark each icon in pack as licensed if you maintain a mapping
}

async function userHasIconsNoAttribution(userId) {
  const e = await db.entitlements.get(userId);
  return !!e?.iconsNoAttr;
}

async function userOwnsIcon(userId, iconId) {
  const e = await db.entitlements.get(userId);
  return e?.iconIds?.includes(iconId) || false;
}

module.exports = {
  grantIconsNoAttribution,
  grantIconPack,
  userHasIconsNoAttribution,
  userOwnsIcon,
};

4) Icons entitlement check (backend)

Update your /api/icons/:id/entitlement to treat “no attribution” as licensed:

const { userHasIconsNoAttribution, userOwnsIcon } = require('../services/entitlements');

router.get('/:id/entitlement', async (req, res) => {
  const userId = req.user?.id || null;
  const id = req.params.id;
  const global = userId ? await userHasIconsNoAttribution(userId) : false;
  const owns = userId ? await userOwnsIcon(userId, id) : false;
  const licensed = !!(global || owns);

  res.json({ licensed, requiresAttribution: !licensed, canDownload: true });
});


Your existing download route will then serve raw file when licensed, and ZIP + CREDIT.txt otherwise—no other change needed.

5) Frontend nudge (button)

Only for icons that are not licensed, show an unlock button:

{!ent.loading && !ent.licensed && (
  <button
    onClick={() => addItem({ kind: 'price', lookupKey: 'icons_no_attr_499', name: 'Icons — No Attribution', qty: 1 })}
    className="px-3 py-1 bg-black text-white rounded text-sm"
  >
    Unlock (no attribution) – $4.99
  </button>
)}


(If you also sell packs, add a second button that uses the pack’s lookup key.)

That’s it

Free users keep Download Free (Attribution) (ZIP + CREDIT.txt).

Buyers of your icons package get No attribution required everywhere.

If you sell packs, they get licensed for those icons (or your pack slug).