App Directory
The public directory of Lightning and Nostr apps — zap-weighted ranking, boosts, and how listings work.
A public directory of Lightning/nostr apps, tools and services — ranked by a zap-weighted, boostable score. Owners list their app free, the community zaps the listings they love (organic rank), and owners can pay sats to boost (paid rank, clearly labeled, decaying). Amount alone is gameable — unique zappers anchor the score.
1. The ranking formula (the heart)
score(app, now) =
zap_score(app) — community signal
+ boost_score(app, now) — paid signal (decays to 0)
zap_score = 10 × Σ log2(1 + sats_i) — per zap, diminishing returns
+ 25 × unique_zappers(30d) — breadth beats depth (anti-self-zap)
boost_score = Σ over active boosts of:
sats_paid × remaining_fraction (linear decay over duration)
A boost of N sats for D days starts at score N and decays linearly to 0
at expiry. At any moment the directory is a fair mix of money-now vs.
love-forever. Boosted rows always render a "Boosted" badge.Why log per zap: a single 100k-sat self-zap shouldn't buy the top spot. log2(1+100000) ≈ 16.6 166 points, while 10 different people zapping 1k each gives 10 × 10 × log2(1001) ≈ 996 points + 250 unique-zapper points. Community breadth dominates whale spend — by construction.
Why linear-decay boost: permanent pay-to-win kills the directory; a decaying boost is honest advertising ("promoted this week").
Default boost products (configurable): 1k sats/3d, 5k sats/7d, 20k sats/14d.
2. Entities
ListedApp
id, ownerUserId (null = admin/seeded), name, slug (unique),
description, url, iconUrl?, category, tags[],
lnAddress (zap target — owner-provided OR auto-provisioned app-{slug}@zapapi.app),
zapAddressClaimId? (our first-party LightningAddress row when auto-provisioned),
nostrPubkey?, nostrNpub?,
status: 'pending' | 'active' | 'rejected' | 'suspended',
rejectedReason?, featured (admin pin), createdAt, updatedAt
AppBoost
id, appId, buyerUserId,
satsPaid, durationDays,
invoice: { paymentHash, bolt11, status },
status: 'pending' | 'active' | 'expired' | 'failed',
startsAt (set when paid confirms), expiresAt, createdAtZap stats come from the existing zaps table (paid zaps only),
aggregated by recipientAddress = the app's lnAddress. No new zap
infrastructure needed — when a listing is auto-provisioned on zapapi.app,
every zap to it flows through our existing LNURL callback recordZap
settlement processor. Owner-provided addresses get zaps recorded only if
they resolve to our engine (documented limitation; we show "external zaps
not tracked" for those).
3. Flows
3.1 Submit an app (any signed-in user, admin-approved at launch)
POST /directory/appswith body{ name, description, url, category, tags, lnAddress? }- If no lnAddress: auto-provision
app-{slug}@zapapi.appvia the existing first-party claim engine (owner = the app's owner; zaps land in THEIR wallet). - Status
pendingadmin approvesactive(same trust model as marketplace providers, §9.1 of the marketplace doc).
3.2 Zap a listing (anyone)
- The listing card shows a button with total zap stats.
- Click amount picker we resolve the app's lnAddress via LNURL (our own engine for provisioned ones) invoice pay from ZapAPI wallet or external QR.
- Settlement existing zaps pipeline records it rank recomputed on read.
3.3 Boost a listing (owner or anyone who wants to promote it)
POST /directory/apps/:id/boostwith body{ product: '1k/3d' | '5k/7d' | '20k/14d' }- Invoice minted on the platform wallet (this is ZapAPI revenue — the one place the platform does get paid).
- Paid boost active,
expiresAt = now + durationDays. - A 5-min cron flips expired boosts; score decays automatically (§1).
3.4 Directory browse (public)
GET /directory/apps?category=&sort=score|new|zaps&q=
rows with live score breakdown: zapSats, uniqueZappers, activeBoostSats and score.
4. Anti-abuse
| Attack | Mitigation |
|---|---|
| Self-zap whale | log per zap + unique-zapper term dominates (§1) |
| Sockpuppet zaps | each zap costs real sats (structural cost, Zap Trails argument) |
| Permanent pay-to-win | boost decays linearly to 0 |
| Hidden ads | "Boosted" badge always rendered on boosted rows |
| Spam listings | admin-approved at launch (same model as providers) |
| Zap farming via our free addresses | zaps are counted per recipient address from OUR settlement records only — we never count unverifiable external claims |
5. API surface
# Public
GET /directory/apps?category=&q=&sort=score|new|zaps&page=
GET /directory/apps/:slug
GET /directory/categories
# Authed
POST /directory/apps submit (pending)
GET /directory/my-apps my listings + stats
PATCH /directory/apps/:id edit (re-review if url changes)
POST /directory/apps/:id/boost { boost, invoice }
GET /directory/boosts my boosts
POST /directory/boosts/:id/check poll payment activate
# Admin
GET /admin/directory/apps?status=
POST /admin/directory/apps/:id/approve
POST /admin/directory/apps/:id/reject
POST /admin/directory/apps/:id/feature (pin) / unfeaturePages: /apps (public directory), /dashboard/apps (my listings + boost),
admin review under /admin/directory.