• Home
  • Projects
  • Resume

Remote

GitHubX

Remote · GMT+4

Leveraged Mining 2025 Updated 15 Oct 2026

One system for mining clients, coaches and referral partners

Four services behind one business: a Go API over a mining pool with no product API, a client portal, an admin console, and a partner platform that turns CRM deals into commission owed.

Visit the client portal
Role
Full-stack engineer, led v2 work
Timeline
Sep 2025 – Oct 2026
Team
Solo build, client as product owner
Stack
Go, Fiber, Postgres, Next.js, Supabase

Overview

One system for mining clients, coaches and referral partners

Leveraged Mining sells hosted bitcoin mining alongside a coaching program. That means three groups of people who need completely different things from the same business. Clients want to see their machines earning. The team running the program wants to see who is engaged and who has gone quiet. Referral partners want to know what they are owed and when.

I built the system that serves all three. I joined on the frontend, moved to full-stack when the backend developer left, and shipped v1 in three months; the client portal served about 200 clients. The engagement ran from September 2025 to October 2026. A Go API owns the data and the pool integration. Three Next.js applications sit on top of it: a client portal, an internal admin console, and a separate partner platform with its own database and its own login. They share nothing except the business they describe, which was deliberate.

The impact

Before this, client mining numbers lived in a pool dashboard nobody could safely give a client access to, and partner commissions were worked out by hand against a CRM export. Now a client signs in and sees their own machines. A partner signs in and sees every deal attributed to them with the commission already calculated. The team stopped doing the arithmetic and started checking it.

2
Databases, and one upstream nobody controls
70
Test files on the money path
29
Schema migrations shipped

Problem

Numbers that existed, but nobody could safely reach

Every number the business needed already existed somewhere. Mining performance sat with the pool. Deals sat in the CRM. Coaching hours sat in whatever the client happened to write down. None of it was reachable by the person who actually needed to look at it, and every workaround was some version of a human copying figures between two screens.

The pool was the sharpest version of it. The product surface exposes the account view and nothing narrower, so anything per client had to be derived rather than requested. So the numbers were technically available and practically unusable, which is a worse position than not having them at all.

  • No contract to build againstThe upstream surface carries no version and no compatibility promise. Anything built on it has to assume the shape can move.
  • Access was all or nothingThere was no way to show one client their own machines without exposing everyone else's alongside them.
  • Payouts were reconciled by handEvery cycle, someone exported the CRM, worked through it in a spreadsheet, and hoped the two agreed. Nobody could answer why a figure was what it was six months later.
  • Nobody could demo the productShowing the portal to a prospect meant showing either a real client's earnings or an empty account.

Discovery

What the upstream turned out to be

The first week went into understanding the integration rather than writing it. What came back changed the plan twice.

The pool's data is real but the surface around it is not a contract, so nothing coming out of it can be trusted to keep its shape. That pushed every field through defensive parsing and pushed the whole integration behind one boundary, so the rest of the system would never learn what the upstream looked like.

The second finding was better news. Underneath that surface, access can be scoped to one client's workers, which meant permissions did not need a bespoke model at all. Attach the scope to the client record and the question collapses into ordinary row ownership, which is a problem every part of the stack already knows how to answer.

The CRM had its own surprise. The record the business calls a deal was not the record the earliest rows had been keyed by. A naive first sync would have duplicated every historical deal, which is the kind of thing you find in week one or explain in month six.

System design

Two systems that stay out of each other's way

Four applications, two databases, one upstream nobody controls. A request starts at a client, passes an edge that decides who is asking, reaches exactly one service that owns those rules, and only then touches data or something external.

admin lifecyclescoped per clientscheduled pull + webhook

Clients

Edge

Services

Data and upstream

Operator channelHighest privilege actions
Client portalMachines, payouts, logs
Admin consoleClients, packages, content
Partner portalDeals and commission
Operator guardOut of band, not a browser session
Session + role guardClient and admin
Partner sessionSeparate credentials entirely
Go APIAccounts, activity, packages, content
Pool adapterThe only code that knows the upstream
Sync + commissionPricing engine, run lock
Mining databaseSystem of record
Mining poolNo product API
Partner databaseSeparate system of record
CRMSource of deals

Clients

  • Operator channelHighest privilege actions
    • → Operator guard
  • Client portalMachines, payouts, logs
    • → Session + role guard
  • Admin consoleClients, packages, content
    • → Session + role guard
  • Partner portalDeals and commission
    • → Partner session

Edge

  • Operator guardOut of band, not a browser session
    • → Go API · admin lifecycle
  • Session + role guardClient and admin
    • → Go API
  • Partner sessionSeparate credentials entirely
    • → Sync + commission

Services

  • Go APIAccounts, activity, packages, content
    • → Mining database
    • → Pool adapter
  • Pool adapterThe only code that knows the upstream
    • → Mining pool · scoped per client
  • Sync + commissionPricing engine, run lock
    • → Partner database
    • → CRM · scheduled pull + webhook

Data and upstream

  • Mining databaseSystem of record
  • Mining poolNo product API
  • Partner databaseSeparate system of record
  • CRMSource of deals

Solid lines are request paths. Dashed lines are out of band: a scheduled pull, a webhook, a notification that fires after the response has already gone out. The partner platform sits on its own database on purpose, so nothing in the mining system can read a partner row and nothing in the partner system can read a client's mining data.

Two rules hold the shape. Nothing skips a layer, and nothing reaches sideways into a sibling's data. A request handler knows about the web and nothing else. A service knows the rules and owns the money. Storage answers questions and holds no opinions. It sounds like bookkeeping until the sixth month, when the reason a package is still readable is that it never grew a shortcut.

The pool adapter is the sharpest example. It is the only code in the system that knows the upstream exists. Every other package asks for a number and gets a number, or gets an error already phrased for a person. Nothing outside that boundary has ever had to care how the numbers are obtained, and when the upstream shifts, exactly one package changes.

That boundary owns the traffic policy too, because it is the only place that knows the upstream is slow and occasionally hostile. Every call carries a deadline. Failures retry a bounded number of times with backoff and jitter rather than hammering something already struggling, and sustained failure opens a breaker so the system stops trying for a while instead of queueing up behind a dead dependency.

The client dashboard is why that matters in practice. A home screen asks for several different figures at once, and the naive shape is one upstream call per card. Instead the adapter reads through a short lived cache scoped to the client, and collapses concurrent misses into a single in flight request, so ten cards rendering together produce one call rather than ten. When the breaker is open the portal serves the last good reading with its age on it, because a number from four minutes ago is worth more to a client than a spinner that resolves into an apology.

  1. 01

    A client opens their dashboard

    The session is verified against current account state rather than trusted from the token alone, so access revoked a minute ago is revoked on this request and not at the next expiry.

    On failureAn unverified or revoked session is refused before any business code runs.

  2. 02

    Identity comes from the session, never the request

    Whose data is being asked for is decided by the verified session and nothing else. There is no path where the answer comes from something the caller supplied.

  3. 03

    The service resolves the account

    A demonstration account never reaches the network at all. Its numbers come from a generated dataset instead, so the portal can be shown to a prospect without exposing anyone real.

  4. 04

    The cache answers first

    A recent reading for this client is served straight back. Several cards asking at the same moment share one lookup rather than racing each other upstream, and a miss is what actually reaches the network.

  5. 05

    Live reads go through the adapter

    The client's own scope is what the upstream is asked about, which is the mechanism that makes one client's view structurally incapable of containing another client's machines. The call has a deadline, a bounded retry, and a breaker behind it.

    On failureTimeouts, throttling and malformed answers resolve to one short sentence the client can act on, or to the last good reading with its age shown. The upstream's raw response is kept for diagnosis and never rendered.

  6. 06

    Parse defensively, degrade honestly

    Anything that does not match the expected shape is skipped rather than crashing the request, and an account with nothing in it yet reads as empty rather than as a failure. A new client with no machines is a normal state, not an error.

  7. 07

    Return one type

    Demonstration and live data return the same shape, so the interface has no branch for which one it is rendering. That is what stops the demo path from quietly rotting while nobody looks at it.

Privilege is a ladder, not a switch

Clients reach their own data. Admins reach client data and program content. The ability to grant or remove an administrator sits above both, on a channel outside the web app entirely, so the most damaging action in the system cannot be reached by a stolen browser session no matter whose it is.

“I worked with Zana on our leveraged mining project, and he proved himself when it mattered most. When our backend developer left, he moved from frontend to full-stack almost overnight, cleared every blocker, and shipped a successful v1 on time.”
Shahab PapoonLeveraged Mining

Correctness

Numbers that have to agree with themselves

One question shaped most of the schema: store the total, or compute it. The obvious move for a number the admin console shows constantly is to keep a running counter beside it. I did not, because a counter is a second source of truth, and it goes wrong quietly the first time a record is edited or deleted. Nobody notices until someone asks why a figure moved. Anything describing the present is computed from the underlying records, every time.

Anything describing a moment goes the other way. A figure that was published, acted on, or paid against is not a fact about now. It is a statement about what was true on a date, under rules that were in force on that date. Recomputing it later against today's rules would silently rewrite history the first time anything changed, so those figures are written down when they are settled and never derived again.

That is the rule the whole model runs on. Describes the present, compute it and keep one source of truth. Describes a moment, record it and treat it as immutable. Getting that backwards is how reporting systems end up disagreeing with themselves, and why the answer to why is this number what it is turns into a shrug.

Money never touches a float

Bitcoin amounts are held as satoshis: integers in the smallest unit, from the moment they are parsed to the row in the database, never a binary float that has already lost the last digit. Where a figure has to be rounded, it is rounded once, at the point it is decided, and the rounded value is what gets stored. Rounding at display time is how two screens end up disagreeing about the same record.

Ingestion is the other half of correctness. Inbound events from the CRM are authenticated before anything is read out of them, and they survive a key rotation on the sender's side without dropping anything on ours. Each event carries an identity of its own, so arriving twice does not mean applying twice. At-least-once delivery is a promise every webhook makes and every integration eventually collects on.

The scheduled reconciliation exists because webhooks alone are not a guarantee. A pull on a schedule closes the gap left by any event that never arrived, and because both paths write through the same idempotent step, a record that came in twice by two different routes still lands once. Pull and push together are what make the data eventually correct rather than usually correct.

Decisions

The calls that were not obvious

Four decisions did most of the work. Each one had an obvious option that would have been faster to ship and worse to live with.

ChallengeObvious optionWhat I didWhat it cost
Upstream with no contractCall it from wherever the data is neededOne adapter that owns every fact about the upstream, including its traffic policyAn extra layer to write. It paid for itself the first time the upstream changed shape and exactly one package needed touching.
A dashboard of many figuresOne upstream call per cardRead through a per client cache, with concurrent misses collapsed into one in flight callReadings can be a few minutes old. On a metric that moves slowly, that is a trade worth making for a page that paints immediately.
Outsiders in the same systemOne database, a role flag, shared sessionsA separate application with its own store and its own accountsTwo deploys instead of one. In exchange, no authorisation mistake on one side can reach the other.
Re-keying live recordsA one shot migration over everythingA resolver that prefers the stable key and falls back to the legacy one, adopting as it goesThe fallback path has to live until it stops being used. No downtime and no duplicated history.

The demo problem was the one I did not expect to be interesting. The team needed to show the portal without a real account behind it, and both obvious options were bad: a real client's account is a privacy problem, and an empty one sells nothing. So a demonstration account is served generated data instead, derived deterministically rather than randomly. The same account shows the same believable figures all day and moves overnight, which means a call can be run twice without the story changing underneath it, and a screenshot taken in the morning still matches the product in the afternoon. It returns the same shape as the live path, so the interface cannot tell the difference and the demo cannot quietly rot.

Scheduled work is the other place where the obvious version is wrong. The reconciliation runs on a schedule and can also be started by hand, which means it can overlap with itself. Both entry points claim a lock first, so a second run refuses to start rather than racing the first, and a run that dies without releasing is reclaimed on the next attempt instead of blocking the schedule forever. Two jobs writing the same record at the same time is the kind of bug that never reproduces and shows up as a number nobody can explain.

Every long running job writes a record of itself: when it started, what it touched, whether it finished. That is not logging for its own sake. It is the difference between answering did last night's run work with a yes and answering it with a maybe.

Quality machinery

Seventy test suites sit on the places where a mistake costs money or leaks something: calculation boundaries, record identity, event authenticity, and making sure fields that must never reach a browser never reach one. Twenty nine migrations hold the schema history, so the shape of the data has a past tense. Types sit at every boundary, input is validated at the edge rather than trusted inward, and privileged actions are recorded so there is always an answer to who changed what.

Lessons

What building against someone else's product taught me

The integration work was never really about the pool. It was about deciding where the uncertainty was allowed to live. Once every upstream response passed through one boundary that either returned a clean type or a sentence a human could read, the rest of the system stopped caring how solid the ground underneath it was. That generalises: an unreliable dependency is fine as long as exactly one place is responsible for making it look reliable, and that place is small enough to hold in your head.

The other lesson was about separation being cheaper than it looks. Running the partner platform as its own application with its own database felt like extra work in week seven. It stopped feeling that way the first time a change to partner authentication shipped on its own, on its own schedule, with no chance of touching a mining client's session.

Where the time went: the upstream boundary

Not on features. It went on the boundary around the upstream, on deciding which numbers are computed and which are recorded, and on making the same record arriving twice land once. Those three are why the system has stayed quiet, and none of them are visible in a screenshot.

Explore more

  • Merchant home on a laptop and a phone: the USDC account, private payments and bank account, takings for the last 30 days, and recent paymentsA payment gateway where the shopper stays private and the merchant still gets paidWisp 2026
  • Applicant review screen with profile, AI interview analysis and section scoresAn AI interviewer that hiring teams can actually reason aboutTrellion 2026
  • A toast library you can call from anywhere, including outside Reactembertoast 2026

Have a project in mind?

I take on a small number of projects at a time. If you want to talk through yours, book a call or send an email. I reply within a day.

Book a callhey@zanastack.com

Links

  • Home
  • Projects
  • Resume
  • For language models

Contact

  • hey@zanastack.com
  • Schedule a call

Zana Salimi · Relocating to Yerevan · Remote (GMT+4)

Copyright © 2026 Zana Salimi.

Zana Salimi