From a538bd7add35f16bc6c4ecaf97c00167dd4b82f5 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Thu, 1 Oct 2026 15:57:42 +0700 Subject: [PATCH] feat(reputation): surface 'payment proof' as its own tier, never as verified The proof tag is self-reported and unverifiable client-side, so it must not read as proof of payment. useTradeReputation now exposes withPaymentProof (mutual trades carrying a proof), shown on TradeReputationCard as 'with payment proof (self-reported - not verified)' distinct from the verified count. --- AGENTS.md | 7 +++++-- src/components/TradeReputationCard.tsx | 6 ++++++ src/hooks/useTradeAttestations.ts | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5713661..b2e300c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -927,8 +927,11 @@ session with **no other memory of this project**. So: plus `tradedCount` (mutual trades) + `tradedTraders` (distinct people), and keeps `tradedSatsClaimed` (the raw claim) so the card shows both. New query fields ⇒ `PERSIST_BUSTER` → **v5**. **There are no trade attestations on the - relay yet**, so the honest values are 0 today. (Broader anti-sybil plan — - claimed-vs-verified tiers, ring detection, PoW — is a follow-up.) + relay yet**, so the honest values are 0 today. Related: `useTradeReputation` + now exposes **`withPaymentProof`** — mutually-confirmed trades that carry a + `proof` tag — surfaced on `TradeReputationCard` as *"with payment proof + (self-reported — not verified)"*, **never** as verification. (Remaining + anti-sybil: ring detection + PoW.) - **Reverse geocoding hits Nominatim — disclosed (2026-10-01).** `src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `geocodeBigDataCloud` / `pushRegionTags`) is the **single source of truth**, used by **spot create, spot diff --git a/src/components/TradeReputationCard.tsx b/src/components/TradeReputationCard.tsx index e981272..9b74124 100644 --- a/src/components/TradeReputationCard.tsx +++ b/src/components/TradeReputationCard.tsx @@ -61,6 +61,12 @@ export function TradeReputationCard({ pubkey, className }: { pubkey: string; cla ? `${data.verified} mutually-confirmed ${data.verified === 1 ? 'trade' : 'trades'} with ${people} ${people === 1 ? 'person' : 'people'}` : 'Awaiting mutual confirmation'}

+ {data.withPaymentProof > 0 && ( +

+ {data.withPaymentProof} with a payment + proof attached (self-reported — not verified) +

+ )} {singlePartnerWarning && (

⚠ Every confirmation is with the same account. Mutual confirmation is diff --git a/src/hooks/useTradeAttestations.ts b/src/hooks/useTradeAttestations.ts index 88ae749..917e954 100644 --- a/src/hooks/useTradeAttestations.ts +++ b/src/hooks/useTradeAttestations.ts @@ -150,6 +150,13 @@ export interface TradeReputation { purchases: number; /** Deals where both sides attested the same listing. */ verified: number; + /** + * Mutually-confirmed trades that ALSO carry a `proof` tag (a payment claim). + * ⚠️ The `proof` is self-reported and the app does NOT verify it — it is a + * stronger *claim*, never evidence of payment. Show it as "with payment proof", + * not "verified". + */ + withPaymentProof: number; /** * Distinct counterparties with at least one mutually-confirmed trade. * @@ -182,7 +189,7 @@ export function useTradeReputation(pubkey: string | undefined) { enabled: !!pubkey, staleTime: 5 * 60 * 1000, queryFn: async (c): Promise => { - const empty: TradeReputation = { sales: 0, purchases: 0, verified: 0, verifiedCounterparties: 0, verifiedWith: [], counterparties: [], attestations: [] }; + const empty: TradeReputation = { sales: 0, purchases: 0, verified: 0, withPaymentProof: 0, verifiedCounterparties: 0, verifiedWith: [], counterparties: [], attestations: [] }; if (!pubkey) return empty; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(12000)]); @@ -214,6 +221,7 @@ export function useTradeReputation(pubkey: string | undefined) { let sales = 0; let purchases = 0; let verified = 0; + let withPaymentProof = 0; for (const [key, attestations] of byListing) { const coord = parseListingCoord(key); @@ -232,6 +240,11 @@ export function useTradeReputation(pubkey: string | undefined) { for (const a of attestors) { if (a !== pubkey) verifiedWith.set(a, (verifiedWith.get(a) ?? 0) + 1); } + // A `proof` tag is a payment CLAIM, not verification — count it on its + // own tier so it is never confused with a confirmed trade. + if (attestations.some((e) => e.tags.some(([t]) => t === 'proof'))) { + withPaymentProof++; + } } // Record counterparties (everyone else who attested this listing). @@ -248,6 +261,7 @@ export function useTradeReputation(pubkey: string | undefined) { sales, purchases, verified, + withPaymentProof, verifiedCounterparties: verifiedWith.size, verifiedWith: [...verifiedWith.entries()].map(([pubkey, count]) => ({ pubkey, count })), counterparties: [...counterparties],