sellfolk
v0.1.1
Published
Sellfolk SDK — track signups, trials, and conversions from your referral sellers.
Downloads
176
Readme
sellfolk
Two-line tracking SDK for Sellfolk. Attribute signups, trials and paid conversions back to the seller who sent the visitor.
- ✅ 2 KB minified browser bundle (sub-1 KB gzipped)
- ✅ Zero dependencies on the server SDK (Node 20+ native fetch)
- ✅
keepalive: true— fire-and-forget event delivery, survives page unload - ✅
localStoragewith 60-day ref expiry - ✅ Never throws — silent failure path. Your SaaS keeps running.
- ESM + CJS + minified CDN bundle
Install
npm install sellfolk
pnpm add sellfolk
yarn add sellfolkOr drop the CDN bundle into your HTML (see Plain HTML below).
Quickstart — browser
import sellfolk from 'sellfolk'
// One-line init. Do this at the root of your app.
sellfolk.init('sk_live_acme_a8e92f...')
// Track any event. sellfolk auto-attaches the ref_id captured from ?ref= on init.
sellfolk.track('signup', { email: '[email protected]' })
// Fire when real money moves.
sellfolk.conversion({ amount: 199.00, email: '[email protected]' })The first time a visitor lands on your site via https://yoursite.com?ref=jane123-acme, sellfolk captures jane123-acme and stores it in localStorage for 60 days. Every subsequent track() or conversion() call automatically attaches that ref id — even on different pages, days later, after sign-in.
Framework recipes
Vue 3 / Nuxt
// plugins/sellfolk.client.ts
import sellfolk from 'sellfolk'
export default defineNuxtPlugin(() => {
sellfolk.init('sk_live_acme_a8e92f...')
})
// Then anywhere in your components:
import sellfolk from 'sellfolk'
sellfolk.track('signup', { email: user.value.email })React / Next.js
// app/providers.tsx (Next 13+) or a top-level layout
'use client'
import { useEffect } from 'react'
import sellfolk from 'sellfolk'
export function SaaSBoostProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
sellfolk.init('sk_live_acme_a8e92f...')
}, [])
return children
}
// In a component:
import sellfolk from 'sellfolk'
function SignupForm() {
async function onSubmit(email: string) {
await fetch('/api/signup', { method: 'POST', body: JSON.stringify({ email }) })
sellfolk.track('signup', { email })
}
...
}Plain HTML
<script src="https://cdn.jsdelivr.net/npm/sellfolk/dist/sellfolk.min.js"></script>
<script>
sellfolk.init('sk_live_acme_a8e92f...')
sellfolk.track('signup', { email: '[email protected]' })
</script>The IIFE bundle exposes a global sellfolk.
Server SDK
Use the server SDK from inside your billing webhook so conversions are reported the moment money moves — they don't depend on a browser session.
import { SellfolkServer } from 'sellfolk/server'
const sb = new SellfolkServer(process.env.SELLFOLK_KEY!)
// Inside your Stripe / Polar / Lemonsqueezy webhook handler:
export async function handleOrderPaid(order) {
await sb.conversion({
email: order.customer_email,
amount: order.amount_total / 100,
currency: order.currency,
})
}The server SDK uses Node 20+'s native fetch — zero runtime deps.
By default it never throws. If you want errors to propagate (e.g. so your queue retries):
const sb = new SellfolkServer(key, { throwOnError: true })API reference
Browser
sellfolk.init(apiKey, options?)
Initialize the SDK. Captures ?ref= from the URL if present (otherwise reads the stored ref) and prepares the network client.
| Option | Type | Default | Description |
| --------------------- | -------- | -------------------------------- | ------------------------------------------------- |
| apiBase | string | https://api.sellfolk.com | Override (testing / self-hosted backends). |
| forceClearOnEmptyUrl| boolean | false | When true and URL has no ?ref=, wipe stored ref.|
| now | function | Date.now | Deterministic clock (mostly for tests). |
sellfolk.track(event, payload?)
Send a non-conversion event. Common values: 'signup', 'trial', 'demo', 'newsletter'. Custom names are allowed and recorded as type='custom' with customName=<event>.
sellfolk.conversion(payload)
Fire a conversion. amount is required. The backend attributes this to the stored ref id.
| Field | Type | Required | Notes |
| ---------- | -------- | -------- | ---------------------------------------------- |
| amount | number | ✅ | In your store's currency. |
| email | string | optional | Privacy-truncated server-side before display. |
| currency | string | optional | 3-letter ISO code. Defaults to USD server-side.|
sellfolk.getRef() / sellfolk.clearRef()
Read or clear the currently-stored ref id. Useful for "Was this user referred?" UI or to drop attribution on logout.
Server
new SellfolkServer(apiKey, options?)
| Option | Type | Default |
| -------------- | --------- | ----------------------------- |
| apiBase | string | https://api.sellfolk.com |
| throwOnError | boolean | false |
| fetch | function | globalThis.fetch |
sb.track(event, payload?) / sb.conversion(payload)
Same shape as the browser SDK. Both are async.
How attribution works
- A seller shares
https://sellfolk.com/r/jane123-acme - The visitor clicks. Sellfolk logs the click and 302-redirects to
https://yoursite.com?ref=jane123-acme - Your site loads.
sellfolk.init()capturesjane123-acmeand stores it inlocalStoragefor 60 days. - The visitor browses, signs up, starts a trial, eventually pays.
- Each event (
signup,trial,conversion) is reported with the stored ref id. - Sellfolk attributes the conversion to
janeand pays her per your listing's terms.
If the visitor returns from a different referral link later, the most recent click wins. If they wipe their browser storage, the chain is broken — that's by design (and matches every other attribution tool).
License
MIT
