Fuul SDK
The typed, versioned @fuul/sdk client every Fuul product surface uses to reach the backend — owned end to end, from endpoint to UI, across the affiliate, claims, and incentive features.
- Role
- Primary Maintainer — @fuul/sdk
- Year
- Dec 2024 — Present
- Company
- Kuyen Labs / Fuul
- Status
- Shipped

@fuul/sdk is the typed TypeScript client that every Fuul product surface — the project-owner admin console, the affiliate self-service portal, and white-label hosted dashboards — uses to talk to the backend. It is a browser client (built with Vite, shipped as UMD/ESM with type declarations) that mirrors the server's REST contract. All business logic stays on the server; the SDK is the typed boundary every frontend imports instead of writing raw HTTP.
That makes the SDK the connective tissue of the whole product. When a backend capability lands, it isn't usable by any UI until it exists as a typed method in the SDK and is released as a new version. I became the primary maintainer of that layer while Fuul's affiliate, claims, and tiering products were being built out — so the SDK was less a library I tended on the side and more the spine that carried my own feature work from the backend to the screen.
Why this project matters
A shared, typed client is what keeps a multi-surface product coherent. Without one, every frontend reinvents request and response shapes, and the moment the server changes a field, several apps break silently. The protocol spans multiple chains and identifier types — EVM, Solana, Sui, XRPL, email, UUID — so a loose contract is a steady source of runtime bugs.
The SDK is also a published, multi-consumer package. The admin app, the affiliate portal, the hosted dashboard, and external partner deployments each pin their own version, so the contract has to evolve without stranding the slower movers. Getting that right is the difference between building a new screen by calling a typed method and building it by auditing the network tab.
My role
I was the primary maintainer of @fuul/sdk — the top contributor to the package overall and its dominant author through the v7.x era, from December 2024 onward. My ownership was domain-scoped to the surface the product needed: affiliate analytics, claims and claim-checks, referrals, incentives, tiers, and the cross-cutting platform concerns (chain-ID typing, source detection, dependency security) that touch every method.
I want to be precise about the full-stack framing. On the backend I was a feature-scoped contributor — I added the endpoints my frontend features required, not the core backend architecture. On the SDK I was the maintainer who defined and shipped the typed contract over those endpoints. On the frontend I was the consumer. The value is that I owned the seam between those layers end to end, so a feature rarely stalled on a handoff.
The problem
Three problems converge on a layer like this:
- Untyped drift. Without a shared, typed client, response shapes get reinvented per app and server changes surface as production incidents instead of compile errors.
- Coordination cost. A single product feature — an affiliate bonus campaign, say — needs a backend endpoint, an SDK method, a version bump, and frontend consumption, in that dependency order. Get the order or the typing wrong and you ship breakage to a published, multi-consumer package.
- Multiple consumers, different speeds. The same SDK is consumed by several apps, each pinned to its own version. The contract has to move forward without forcing every consumer to upgrade in lockstep.
The job was to make the contract trustworthy enough that building a new screen meant calling a typed method, not reverse-engineering the API.
Approach
I treated the SDK as a product contract, not a utility folder, and standardized one delivery pattern around it — what I came to think of as contract-spine delivery:
- Add the feature-scoped endpoint the UI needs on the backend.
- Express it as a typed method on the SDK, with the request and response types living next to it, and cut a new package version.
- Consume that method in the frontend, usually behind a feature flag, only after the version is published.
Done in that order, a feature moves cleanly from the database to the pixels with no untyped middle. The published version number becomes a small but real release artifact: each bump corresponds to a concrete capability that the consuming apps could then adopt at their own pace. Because every consumer pins a version, the drift between apps is itself a readable record of which features have rolled out where — I leaned on that instead of fighting it.
The defining piece of this work was building the SDK's affiliate-portal data layer essentially from a blank class and growing it method by method as the portal's surface area grew, so the portal and its contract evolved in lockstep.
Selected contributions
Affiliate-portal data layer from a blank class
I authored the affiliate-portal service class from scratch and grew it across a long run of pull requests into the portal's primary data layer — getAffiliateTotalStats, getPaidVolumesByLevel, getActiveReferredUsers, getReferralTree, and getStatsBreakdown, plus the claims methods below. This class is the spine of the self-service portal product; the portal's stats, multi-level breakdown, and referral-tree views lean directly on it.
Claims and claim-checks contract
I built the typed methods behind the reward claim-check lifecycle and the automated-claims surfaces — getClaimChecks (including a status filter), closeClaimChecks, and getClaimHistory. Because these back money-movement flows, the shape of the contract had to be exact.
Incentives and tiers
I delivered getIncentives and getIncentivesByTier, and threaded the current-tier field into affiliate data, so the admin app and portal could render tier-aware incentive economics from the same typed source.
Referral codes and rebates
I owned the referral-code and rebate methods — getAffiliateCode, getCodeAvailability, deleteReferral, and updateRebateRate — backing self-service referral-code management and configurable commission splits.
Bonus-campaign methods
I built the affiliate bonus-campaign methods (getAffiliateBonuses, covering a qualified-user-bonus track and a high-volume-taker-bonus track) as the SDK half of an end-to-end feature delivered across backend, SDK, and portal UI in one coordinated push.
A breaking migration, run as a migration
I ran a breaking getUserReferrer migration — evolving an established method's contract and shepherding consumers through the change rather than forking around it. Evolving a contract in a published, multi-consumer package is mostly a migration job, not a diff.
Platform-level typing and hygiene
I standardized chain identifiers across the SDK, converting chain_id from number to string so large chain IDs are handled safely — a typing change that rippled out to multiple frontends as part of a cross-service refactor. I also kept the published package secure and lean: a dependency security bump for axios, and a simplification of the source-detection logic. Maintenance like that matters precisely because the package is public and imported everywhere.
Technical highlights
- A typed boundary over a multi-consumer REST contract. Each SDK method maps to one backend endpoint, with request and response types co-located per domain. A server contract change surfaces as a compile error in consumers, not a production incident.
- Versioning as a release discipline. The package moved across the v7.x line as a sequence of deliberate, feature-bearing releases — claim-check filtering, incentive-by-tier reads, current-tier data, and claim history — so each version is a checkpoint a consumer can adopt intentionally.
- Cross-service delivery in dependency order. The hard part of contract-spine delivery is sequencing: endpoint, then typed method and version, then UI — never out of order against a published package. I ran that repeatedly across three repositories.
- Multi-chain, multi-identifier typing. The
chain_idnumber→string change removed a class of large-chain-ID bugs at the type level, and the identifier model spans EVM, Solana, Sui, XRPL, email, and UUID — typing that has to be correct for a protocol that pays out across chains. - Browser-safe, dependency-light client. A pure client — minimal runtime dependencies, browser-environment guards, and colocated tests per service — so consuming apps inherit a small, predictable surface rather than a heavy SDK.
Developer experience as design
Developer experience is the design surface here: the shape of an SDK method directly determines how simple — or how tangled — the UI that consumes it can be.
- Domain-named methods. Names read as product intent (
getReferralTree,getPaidVolumesByLevel,getActiveReferredUsers), so a component author calls something that already matches the screen they're building instead of assembling it from primitives. - Predictable, typed responses shrink UI glue. Because each method returns a typed, server-shaped object, consuming components stayed thin — they could lean on the contract rather than defensively normalize data.
- Contracts designed to unblock the next screen. Each method was added in service of a specific UI — a bonus-campaign tab, a claim-history view, a tier-aware incentive table — so the data layer never lagged the design intent.
- Stable seams for white-labeling. A single, versioned contract is what let one product surface ship across the admin app, the self-service portal, the hosted dashboard, and external partner deployments without each one diverging into its own bespoke client.
Outcome
I created a typed, versioned contract that became the default way Fuul's frontends consumed backend capability — and the package's history reads as that story. I am the top contributor to @fuul/sdk and its dominant author through the v7.x era, and the version trail from the affiliate-portal era onward maps onto the product features each version unblocked: claim-check filtering, incentive-by-tier reads, current-tier data, and claim history.
The contract-spine pattern let affiliate, claims, tiering, and incentive features move from backend to UI without cross-team handoffs and without untyped fetch code accumulating in the apps — and the typed boundary turned a class of server-contract changes into compile-time catches rather than runtime breakage. I don't have public product or performance metrics for this work, so I'm describing the contract and delivery outcomes I can stand behind, not user or revenue numbers.