HIP-1143: Referral — An Edge From Referrer to Referee
Abstract
/v1/referral is referral ATTRIBUTION: who referred whom, and whether that
referee ever became a real customer. Every org has a stable code and share
link, a new org claims it at signup, and an admin sweep advances signup →
qualified once the referee makes metered spend. That attribution record is the
whole product — the capability, implemented in hanzoai/cloud at
apps/referral, MOVES NO MONEY, and this HIP states why that absence is the
design.
Motivation
A referral program needs two facts nobody else records: the edge (referrer ↔
referee, first-touch, once ever) and whether the referee genuinely used the
product. Signup alone is a vanity signal; metered spend is the honest one
(apps/referral/referrals.go:23-26). What is owed for a qualified referral is
an affiliate PAYABLE, tracked in hanzoai/commerce and settled by wire or to a
connected wallet — never minted as platform credit.
Specification
The key words MUST, MUST NOT and SHOULD are to be interpreted as in RFC 2119.
The store
One system-namespace SQLite file, referrals.db (sqlpool.Open,
cek-encrypted, single-connection): the referral edges plus a code directory.
The code itself is a deterministic base32 hash of the org id
(apps/referral/referrals.go:17-18), so it never changes and never has to be
stored to be reproduced; the directory row only materializes the O(1) reverse
lookup.
Addresses
Four operations, all typed ops, across two audiences
(apps/referral/referrals.go:117-146):
GET /v1/referral— the caller's code, share link and referrals. A pure read: it advances nothing.POST /v1/referral/claim— record a referral from a?refcode. Idempotent and first-touch: 201 on the first claim, 200 withcreated:falseon replay.GET /v1/admin/referrals/bonuses— SuperAdmin: every edge plus a summary.POST /v1/admin/referrals/sweep— SuperAdmin: the cron path, and the ONLY path that advances a referral; one pass is bounded at 500 so a backlog drains over runs instead of wedging one request.
The two admin leaves sit under a prefix another app also serves —
GET /v1/admin/referrals is the affiliates analytics board — so the plugin
declares its prefixes from the manifest row (plugin/referral/main.go) rather
than the /v1/<name> default, and each gate is bound to the exact path it
covers, never a subtree (apps/referral/referrals.go:121-135).
Tenancy
The REFEREE is the validated caller's org, never a client field, and the
referrer is resolved from the code — so a caller can only ever attach
THEMSELVES to someone else's code. Self-referral is 400, an unknown code 404.
Writes with no validated principal are refused ahead of the body decode
(requireOrgOnWrite); the two admin leaves require SuperAdmin, fail-closed.
Money
Free, said in those words: plugin/referral/main.go declares cloud.Free,
and nothing here meters, gates or debits. The one money-plane touch is a READ —
"has this referee spent?" — asked through the payout plane client
(apps/referral/commerce.go), and the seam is deliberately read-only:
TestCommerceSeamIsReadOnly fails if it ever grows a write method, because a
deposit on this interface is exactly how a GET once came to mint platform
credit.
Events and telemetry
It publishes nothing to the bus, so a customer's webhooks receive nothing from
it. Beyond the request span, a qualification appends one referral.qualified
record to cloud's tamper-evident audit trail, best-effort
(apps/referral/referrals.go:531-548) — an attestation that a referee became
a customer, carrying no amount because this package issues none.
Stage
beta: the manifest row declares it (manifest/apps.go:431, Stage: Beta;
HIP-0139 §8).
Upstream
Derives from none.
Rationale
The alternative — the referral system granting credit directly — is the shape
every referral program starts with and the one this package was cut back from:
once the capability to deposit exists on this surface, a caller eventually
reaches it. Splitting attribution (here) from reward (an affiliate payable in
commerce) means the worst a compromised referral path can do is mislabel an
edge, not print money. Qualification living only on the admin sweep, never on a
GET, is the same rule one layer down: reads report state, one bounded
idempotent write advances it, and the qualified_at latch makes the transition
at-most-once under concurrent sweeps.
Security Considerations
Attribution is an incentive system, so the attacker is a fraudulent referrer. The closures, each specific: the referee comes only from the validated principal, so an attacker cannot claim on a victim's behalf or attach a victim to their code; first-touch-once-ever makes claim replay inert; self-referral is refused on the resolved orgs, not on the code string; and qualification keys on metered spend read from the ledger — a signal that costs real money to forge — never on signup volume. Because no path here mints credit, the residual prize for defeating all of that is a mislabeled attribution row, which the audit trail records with both orgs named.
References
- HIP-0026 — Identity and Access Management
- HIP-0106 — The Hanzo Plugin Contract
- HIP-0139 — Capability
Copyright
Released under CC0 1.0 Universal Public Domain Dedication.