Referrals
Who pays a referral fee, and how to sign an app-paid payout after the fill is already terminal.
A referral pays referrer from the same trade. On a sell it applies only when you set a sell rate. Rates and the parameter table live on Fees. Signature payload types live on Self-custody signing.
Send referral fields from your backend with x-app-api-key, alongside x-app-id and the
user's access token. The API key does not replace the user's session. Never expose it in
browser code.
Who pays
referralPayer is "user" (default) or "app".
A user-paid referral is reserved from maxSpend and collected the same way as the app fee: on the amount actually filled. feeBreakdown.appFee is the maximum if the buy fills in full. feeBreakdown.referralFee is the user-paid referral.
An app-paid referral is not a user cost. A partial fill pays the quoted referral amount. A failed fill pays nothing. You pay it from referralPayerAddress.
Per-venue rates go in referrerFeeBipsByVenue. Hyperliquid charges 1 USDC to activate a referrer with no HyperCore account. On a self-custody Hyperliquid buy with a user-paid referral, referralActivationSponsorAddress names a wallet of yours that pays it instead, and its signature is listed with purpose: "hl_activation_sponsor". See Fees.
Poll past terminal
An app-paid payout can appear in pendingSignatures with purpose: "referral_payout" after
the trade is already terminal, including for managed trades. When you requested an app-paid
referral, continue polling for that payout after a successful fill. Read each request's
expiresAt; an unanswered request expires without undoing the trade.
let payout;
const deadline = Date.now() + 15 * 60 * 1000;
while (!payout && Date.now() < deadline) {
const status = await client.getExecutionStatus({ quoteId });
payout = status.pendingSignatures?.find(
(item) => item.purpose === "referral_payout" &&
item.signerAddress.toLowerCase() === payerAddress.toLowerCase(),
);
if (status.terminal && ["failed", "cancelled", "expired"].includes(status.overallState)) break;
if (!payout) await new Promise((resolve) => setTimeout(resolve, status.pollAfterMs ?? 3000));
}Full shape on Get execution status.
Submit the signature on Submit signatures for a pending self-custody fill. How to sign payload is Self-custody signing. On an EVM chain the request type is transaction and you send { stepId, txHash } after broadcasting. On HyperCore (1337) the type is eip712 and you send { stepId, signature }. Solana payouts are refused.
// payerSigner uses the signing-reference implementation with your payer wallet.
// For transactions it switches chains and converts the decimal value to bigint.
if (payout && Date.parse(payout.expiresAt) > Date.now()) {
const signed = await payerSigner(payout);
await client.submitFillSignatures(quoteId, [
typeof signed === "string"
? { stepId: payout.stepId, signature: signed }
: { stepId: payout.stepId, txHash: signed.txHash },
]);
}In self-custody the same pendingSignatures list also holds the trader's requests. Skip a request whose signerAddress is not the wallet you hold.