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],