This is the self-serve referral API served by
backend-global.It is not the on-chain program documented in GNSReferrals and ReferralsUtils, and it is not the older KOL-slug system behind /api/referrals/links/…. Three different things share the word “referral”. Do not mix them.Check availability first
The API is enabled per deployment. Probe it at startup:200 means referrals are unavailable on that host. Disable the feature in your UI rather than falling back to stored values.
chainId and distributor come from this endpoint at runtime by design. A distributor redeploy is meant to be a configuration change on our side, not a client rebuild on yours. Any address you pin elsewhere will eventually be wrong.Three rules that break integrations
A code must be applied before the trader's first trade
A code must be applied before the trader's first trade
Once a wallet has traded on any chain covered by the program,
POST /api/referrals/bind returns 409 AlreadyTraded and no referrer can ever be attached. This is permanent.Bind at onboarding, before the wallet reaches the exchange. Binding shortly after the first trade earns nothing.There is no endpoint that answers “is this wallet still bindable”. Derive it from trading history, and ask us for the exact list of chain ids in the attribution window so your check covers the same set as ours. Cover fewer chains than we do and you will show an eligible state that the API then refuses.Treat any chain you cannot resolve as not eligible and disable the bind UI. Failing open here produces a link that is accepted and then earns nothing.Never hardcode the distributor address or the chain id
Never hardcode the distributor address or the chain id
Read both from
/config on every session.A stale chainId or distributor silently changes the EIP-712 domain. The recovered signer becomes a different address, so the failure surfaces as 401 unauthorized-signer rather than as anything that mentions the domain. If you see that error and your subject really is the signer, re-fetch /config before looking anywhere else.Neither `claimableMicroUsdc` nor `unpaidMicroUsdc` is claimable
Neither `claimableMicroUsdc` nor `unpaidMicroUsdc` is claimable
Despite the names:
claimableMicroUsdcis lifetime gross earnings. It ignores everything already paid out.unpaidMicroUsdcis lifetime minus already claimed. It still includes earnings that no published batch carries yet, which the contract will refuse to pay.
200 from /proof/{address} and a positive delta. See Claiming.Signing
Both write operations share one EIP-712 domain.salt, and you must not add EIP712Domain to the types object.
Types
Field order is part of the hash. Do not reorder.Message fields
The nonce high-water mark is per signer and shared across both message kinds. A bind sent after a register must carry a strictly larger nonce, and you must never have two writes in flight for the same wallet.
validUntil is checked against our clock: it must be in the future and at most 900 seconds ahead. Sign immediately before submitting rather than pre-signing and queueing.
Sending it
Both transports are equivalent; the signature is the only thing that matters.bind_code and /api/referrals/bind to bind. kind must be present and must match the path, but it is transport metadata only and is not part of the signed struct.
The GET transport exists so the request stays a CORS simple request and survives edge rules that block POST. If you use it, send no custom headers, or you reintroduce the preflight you were avoiding.
201 is success.
Worked example
Write errors
Reading
Every response is wrapped in{"result": …}; errors are {"error": "…"}. Reads are not rate limited and CORS is open, but there is no server-side cache, so poll conservatively. Accruals move at most every 30 seconds, and the claimable amount changes roughly once a day.
Units
- Micro-USDC integers as strings — divide by 1e6, parse with
BigInt:claimableMicroUsdc,settledMicroUsdc,unpaidMicroUsdc,cumulativeMicroUsdc,amountMicroUsdc. - Decimal USD strings, already human-scaled:
lifetimeUsd,l1Usd,l2Usd,cashbackUsd,accruedUsd,earnedUsd,volume30dUsd. - Basis points as integers, 10000 = 100%:
l1RateBps,l2RateBps,cashbackBps. - Unix seconds:
boundAt, nullable.YYYY-MM-DDUTC:day.
What to display
effectiveRate reports the standard programme rate. A negotiated partner rate is applied to actual earnings but is not reflected in that field today, so do not present it as a contractual rate.Claiming
1
Fetch the proof
GET /api/referrals/proof/{address} returns day, cumulativeMicroUsdc and proof.A 404 nothing claimable is a normal state, not an error.2
Compute the payout
The contract pays
cumulativeAmount − claimed[account]. Read it exactly with the claimableAmount(account, cumulativeAmount) view, or approximate it as cumulativeMicroUsdc − settledMicroUsdc.Prefer the on-chain view for the amount next to the button: settledMicroUsdc is only as fresh as our indexer, so it can over-report just after a claim.3
Send the transaction
Call Payment is native USDC on the hub chain, 6 decimals. The call is permissionless: anyone can submit it and funds always go to
claim on config.distributor, on config.chainId. Pass cumulativeMicroUsdc unchanged — the contract computes the delta itself.account, so a relayer can pay the gas.InvalidProof. Handle NothingToClaim, InvalidProof and IsPaused.
Referral links
To match the gTrade app:
Validate against
/^[a-z0-9_]{1,31}$/ before storing.
The
by and referredBy parameters and the referral cookie belong to the older KOL-link system and are unrelated to this API.Before you go live
Ask us for the following. The first three are blocking.- Your exact origins added to the write allowlist. Matching is exact string comparison on the
Originheader: no wildcards, no subdomain matching. Without it, every write returns403. Tell us whether you call from a browser or server-side, because a request with noOriginheader is also rejected. - Confirmation that signed writes are enabled on the environment you target.
- Confirmation that accrual indexing is enabled. If it is off, writes succeed and reads return zeroes indefinitely, with no HTTP signal to detect it.
- The API base host for each environment.
- The chain ids in the attribution window, so your eligibility check matches ours.
- Reserved code namespaces, if you want branded codes protected from squatting.
- Rate-limit headroom, if your traffic egresses from a small set of IPs. The limit is 30 writes per minute per IP, shared across both write endpoints and both transports.
Integration checklist
- Probe
/config. Non-200disables the feature. - Capture
?ref=, validate it, store it inreferral_v2for 7 days. - Resolve the code with
/code/{code}/address. Anullowner means do not attempt a bind. - Check eligibility across every attribution chain. Treat anything unresolved as not eligible.
- Check
/binding/{address}. Non-null means already bound. - Sign with values from
/config, one write in flight per wallet. - On
201you are done. On409or500, re-read state before concluding anything. - Offer the claim only on a
200proof with a positive delta, with the wallet onconfig.chainId.
