Files
kashilo/docs/KILLER-FEATURES.md

18 KiB
Raw Blame History

Killer-Features — kashilo.com

Differenzierung gegenüber eBay Kleinanzeigen, Tutti, XMRBazaar. Drei Features, die kein Konkurrent hat.

Status: Verifiable Listings implementiert, Rest in Planung


1. Blind Meeting Points

Problem: Bei lokalen Deals muss eine Seite ihre Adresse preisgeben. Privacy-bewusste User vermeiden das — oder treffen sich an zufälligen, unsicheren Orten.

Lösung: Beide Parteien geben einen ungefähren Radius an (z.B. "Zürich HB ± 1 km"). Die App berechnet einen öffentlichen, neutralen Treffpunkt im Überschneidungsbereich (Bahnhof, Einkaufszentrum, Café). Keine Seite gibt ihre echte Adresse preis.

Datenquelle

  • OpenStreetMap Overpass API (kostenlos, kein API-Key)
  • Query: POIs mit Tags wie amenity=cafe, shop=mall, railway=station im Überschneidungskreis
  • Fallback: Mittelpunkt der beiden Radien als generischer Treffpunkt

Algorithmus

1. Seller setzt Punkt A + Radius rA (z.B. 1 km)
2. Buyer setzt Punkt B + Radius rB (z.B. 1.5 km)
3. Berechne Überschneidungsfläche der beiden Kreise
4. Falls keine Überschneidung: Mittelpunkt A↔B vorschlagen, Radien vergrößern
5. Query Overpass API für geeignete POIs in der Überschnittfläche
6. Filtere nach Kategorien (priorisiert):
   a. Bahnhöfe / ÖV-Haltestellen (öffentlich, belebt)
   b. Einkaufszentren (überdacht, sicher)
   c. Cafés / Restaurants (neutral)
   d. Parks / Plätze (Fallback)
7. Wähle den POI, der am nächsten zum Mittelpunkt liegt
8. Zeige Kartenausschnitt (Leaflet + OSM-Tiles) mit dem Treffpunkt

UX-Flow

Chat-Widget:
┌─────────────────────────────────────┐
│  💬 Chat mit Käufer                 │
│                                     │
│  ... Nachrichten ...                │
│                                     │
│  ┌─────────────────────────────┐    │
│  │ 📍 Treffpunkt vorschlagen   │    │
│  └─────────────────────────────┘    │
│                                     │
│  → Klick öffnet Modal:             │
│  ┌─────────────────────────────┐    │
│  │ 🗺 Karte (Leaflet/OSM)     │    │
│  │                             │    │
│  │   [Pin auf Karte setzen]    │    │
│  │   Radius: [====●===] 1 km  │    │
│  │                             │    │
│  │   [Treffpunkt berechnen]    │    │
│  └─────────────────────────────┘    │
│                                     │
│  Ergebnis wird als Chat-Nachricht   │
│  gesendet (E2E verschlüsselt):     │
│  ┌─────────────────────────────┐    │
│  │ 📍 Vorschlag: Zürich HB    │    │
│  │ Bahnhofstrasse 1            │    │
│  │ [Auf Karte anzeigen]        │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

Technische Umsetzung

Komponente Datei Beschreibung
Service js/services/meeting-points.js Geo-Berechnung, Overpass-API-Query, POI-Filterung
Component js/components/meeting-point-modal.js Karte (Leaflet), Radius-Slider, Ergebnis-Anzeige
Integration js/components/chat-widget.js Button "Treffpunkt vorschlagen", Ergebnis als Nachricht
CSS css/components.css Karten-Styles, Modal-Styles
Vendor js/vendor/leaflet/ Leaflet.js (self-hosted, ~40 KB gzip)

Overpass API Query (Beispiel)

[out:json][timeout:10];
(
  node["railway"="station"](around:500,47.3769,8.5417);
  node["amenity"="cafe"](around:500,47.3769,8.5417);
  node["shop"="mall"](around:500,47.3769,8.5417);
);
out center 5;

Privacy-Aspekte

  • Kein serverseitiger Standort: Berechnung komplett im Browser (Client-Side)
  • Overpass-Query: Geht direkt an OSM-Server (kein Proxy nötig, keine Logs bei uns)
  • Treffpunkt im Chat: E2E verschlüsselt, Server sieht nur Ciphertext
  • Kein GPS nötig: User setzt Pin manuell auf der Karte
  • Seller-Standort ≠ Treffpunkt (Treffpunkt ist immer ein öffentlicher Ort)

i18n Keys

meeting.suggest          = "Treffpunkt vorschlagen"
meeting.setLocation      = "Deinen ungefähren Standort wählen"
meeting.radius           = "Radius"
meeting.calculate        = "Treffpunkt berechnen"
meeting.result           = "Vorgeschlagener Treffpunkt"
meeting.noOverlap        = "Kein gemeinsamer Bereich — Mittelpunkt wird vorgeschlagen"
meeting.noPoi            = "Kein öffentlicher Ort gefunden — Punkt auf der Karte"
meeting.send             = "Im Chat senden"
meeting.showMap          = "Auf Karte anzeigen"
meeting.types.station    = "Bahnhof"
meeting.types.mall       = "Einkaufszentrum"
meeting.types.cafe       = "Café"
meeting.types.park       = "Park / Platz"

Aufwand

  • Geschätzt: 23 Tage
  • Dependencies: Leaflet.js (self-hosted), Overpass API (kostenlos)
  • Risiko: Overpass API Rate-Limits (Fallback: Mittelpunkt ohne POI-Suche)

2. Verifiable Listings (Proof of Possession)

Problem: Auf anonymen Marktplätzen ist Scam ein Hauptproblem. Seller können Bilder aus dem Internet nehmen und Artikel listen, die sie nicht besitzen. Ohne KYC gibt es keine Konsequenzen.

Lösung: Seller können optional ein Foto hochladen, auf dem ein von der App generierter Einmal-Verifizierungscode sichtbar ist (auf Zettel, Display, etc.). Das beweist: "Dieser Artikel war zum Zeitpunkt X physisch vorhanden."

Verfahren

1. Seller klickt "Besitz verifizieren" beim Listing erstellen/bearbeiten
2. App generiert einen 6-stelligen Code + QR-Code + Zeitstempel
   - Code: kryptografisch zufällig (crypto.getRandomValues)
   - Gültig: 10 Minuten (danach neuer Code)
3. Seller fotografiert den Artikel MIT dem Code (auf Zettel, Handy-Display daneben, etc.)
4. Foto wird hochgeladen als spezielles Verifikations-Bild
5. App speichert: Code + Zeitstempel + Foto-ID als Verifikationsnachweis
6. Listing zeigt Badge: "✓ Besitz verifiziert am [Datum]"

Verifikations-Sicherheit

Aspekt Lösung
Fake-Code auf echtem Bild Code ist nur 10 Min gültig, Zeitfenster wird angezeigt
Photoshop / Bildbearbeitung EXIF-Metadaten prüfen (optional), Hash des Originalbilds speichern
Fremdes Foto mit Code Nicht 100% verhinderbar, aber Aufwand steigt deutlich
Wiederverwendung Code ist einmalig, an Listing-ID gebunden

Wichtig: Das ist kein perfekter Beweis, sondern ein Vertrauenssignal. Es erhöht den Aufwand für Scammer signifikant.

Datenmodell (Directus)

Neues Feld auf listings:

Feld Typ Beschreibung
verification_code String (6) Der generierte Code
verification_image UUID, FK → directus_files Das Verifikationsfoto
verification_date DateTime Zeitpunkt der Verifikation
verified Boolean true wenn Verifikation abgeschlossen

UX-Flow

Listing erstellen / bearbeiten:
┌─────────────────────────────────────┐
│  📷 Bilder hochladen               │
│  [Bild 1] [Bild 2] [+]            │
│                                     │
│  ─────────────────────────────      │
│                                     │
│  ✓ Besitz verifizieren (optional)  │
│                                     │
│  → Klick öffnet Verifikation:      │
│  ┌─────────────────────────────┐    │
│  │                             │    │
│  │  Dein Code: 4 8 2 7 1 5    │    │
│  │  Gültig noch: 08:42        │    │
│  │                             │    │
│  │  Schreibe diesen Code auf   │    │
│  │  einen Zettel und fotografiere│   │
│  │  deinen Artikel zusammen    │    │
│  │  mit dem Code.              │    │
│  │                             │    │
│  │  [📷 Verifikationsfoto     │    │
│  │       hochladen]            │    │
│  │                             │    │
│  └─────────────────────────────┘    │
│                                     │
│  Nach Upload:                      │
│  ✅ Besitz verifiziert             │
└─────────────────────────────────────┘

Auf dem Listing (für Käufer):
┌──────────────────────┐
│  📷 Bild             │
│  Titel der Anzeige   │
│  50.00 EUR           │
│  ✓ Besitz verifiziert│  ← Badge
│  📍 Zürich  🟢 10+   │
└──────────────────────┘

Technische Umsetzung

Komponente Datei Beschreibung
Service js/services/verification.js Code-Generierung, Validierung, Directus CRUD
Component js/components/verification-widget.js Code-Anzeige, Countdown, Upload
Integration js/components/pages/page-create.js Einbettung in Listing-Formular
Integration js/components/pages/page-listing.js Badge-Anzeige für Käufer
CSS css/components.css Badge-Styles, Verifikations-Widget

Privacy-Aspekte

  • Freiwillig: Keine Pflicht, reines Opt-in
  • Kein KYC: Code beweist Besitz, nicht Identität
  • Foto-Metadaten: EXIF wird beim Upload gestrippt (wie bei normalen Listing-Bildern)
  • Code serverseitig: Nur der Hash des Codes wird gespeichert (nicht der Klartext)

i18n Keys

verification.verify         = "Besitz verifizieren"
verification.optional       = "Optional — erhöht das Vertrauen"
verification.code           = "Dein Code"
verification.validFor       = "Gültig noch"
verification.instructions   = "Schreibe diesen Code auf einen Zettel und fotografiere deinen Artikel zusammen mit dem Code."
verification.upload         = "Verifikationsfoto hochladen"
verification.verified       = "Besitz verifiziert"
verification.verifiedDate   = "Verifiziert am {{date}}"
verification.expired        = "Code abgelaufen — neuen generieren"
verification.badge          = "✓ Verifiziert"

Aufwand

  • Geschätzt: 12 Tage
  • Dependencies: Keine neuen (Directus File-Upload existiert bereits)
  • Risiko: Gering — Feature ist optional und unabhängig

3. Selbstzerstörende Listings

Problem: Auf klassischen Plattformen bleiben Anzeigen und Nutzerspuren ewig gespeichert. Archivierte Listings sind oft noch über Suchmaschinen auffindbar. Für Privacy-bewusste User ist das ein Dealbreaker.

Lösung: Seller können ein Listing als selbstzerstörend markieren. Nach Ablauf oder Deal-Abschluss werden alle Daten (Bilder, Beschreibung, Verifikation) unwiderruflich gelöscht — nicht archiviert.

Modi

Modus Trigger Effekt
Zeitbasiert Seller wählt Lebensdauer: 24h, 48h, 7 Tage, 30 Tage Listing + Bilder werden nach Ablauf gelöscht
Deal-basiert Deal wird beidseitig bestätigt Listing wird X Stunden nach Deal-Bestätigung gelöscht
Manuell Seller klickt "Jetzt zerstören" Sofortige Löschung

Standard-Listings (ohne Selbstzerstörung) verhalten sich wie bisher: Archivierung nach 30 Tagen.

Löschung — was wird gelöscht?

1. Listing-Datensatz (Directus: items.delete)
2. Alle verknüpften Bilder (Directus Files: files.delete)
3. Verifikationsfoto (falls vorhanden)
4. Junction-Table-Einträge (listings_files)
5. Conversations + Messages werden NICHT gelöscht
   (die sind E2E verschlüsselt und gehören beiden Parteien)

Datenmodell (Directus)

Neue Felder auf listings:

Feld Typ Beschreibung
self_destruct Boolean true wenn Selbstzerstörung aktiv
destruct_mode String time, deal, manual
destruct_at DateTime Zeitpunkt der geplanten Löschung
destruct_after_deal_hours Integer Stunden nach Deal-Bestätigung (Default: 24)

Implementierung: Löschung

Option A: Directus Flow (empfohlen)

  • Neuer Scheduled Flow: alle 15 Minuten (wie Expired-Listings-Flow)
  • Query: self_destruct = true AND destruct_at <= NOW()
  • Action: Delete Listing + zugehörige Files
  • Vorteil: Serverseitig, zuverlässig

Option B: Frontend-Trigger (Fallback)

  • Bei App-Start prüfen: Gibt es eigene Listings mit destruct_at in der Vergangenheit?
  • Falls ja: Delete-Request an Directus
  • Nachteil: Funktioniert nur wenn Seller die App öffnet

Empfehlung: Option A als Primär, Option B als zusätzliche Absicherung.

UX-Flow

Listing erstellen:
┌─────────────────────────────────────┐
│  Titel: [___________________]       │
│  Preis: [___]  Währung: [EUR ▼]    │
│  ...                                │
│                                     │
│  ─────────────────────────────      │
│  🔥 Selbstzerstörung                │
│                                     │
│  [●] Aus (Standard: Archivierung)  │
│  [ ] Nach Zeitablauf               │
│      → [24h ▼] / 48h / 7d / 30d   │
│  [ ] Nach Deal-Abschluss           │
│      → [24] Stunden danach         │
│                                     │
│  ⚠ Achtung: Gelöschte Anzeigen    │
│  können nicht wiederhergestellt     │
│  werden.                            │
└─────────────────────────────────────┘

Auf dem Listing (für Käufer):
┌──────────────────────┐
│  📷 Bild             │
│  Titel der Anzeige   │
│  50.00 EUR           │
│  🔥 Läuft ab in 23h  │  ← Countdown
│  📍 Zürich           │
└──────────────────────┘

Für Seller (eigenes Listing):
┌──────────────────────────────┐
│  🔥 Selbstzerstörung aktiv   │
│  Wird gelöscht am: 12.02.26  │
│  [Jetzt zerstören]           │
└──────────────────────────────┘

Technische Umsetzung

Komponente Datei Beschreibung
Service js/services/self-destruct.js Countdown-Berechnung, Delete-Requests, Deal-Trigger
Directus Flow (Server-Config) Scheduled Flow: Listings mit destruct_at <= NOW() löschen
Integration js/components/pages/page-create.js Selbstzerstörungs-Optionen im Formular
Integration js/components/pages/page-listing.js Countdown-Anzeige, "Jetzt zerstören"-Button
Integration js/components/listing-card.js Countdown-Badge auf Karten
CSS css/components.css Countdown-Badge, Warnungs-Styles

Directus Flow: Auto-Delete

Trigger: Schedule (*/15 * * * *)
Bedingung: listings WHERE self_destruct = true AND destruct_at <= NOW()

Aktionen:
1. Read: Betroffene Listings + deren File-IDs laden
2. Delete: Files löschen (directus_files)
3. Delete: Junction-Einträge löschen (listings_files)  
4. Delete: Listings löschen

Deal-basierte Zerstörung

Wenn Deal status → confirmed:
1. Prüfe: Hat das Listing self_destruct = true UND destruct_mode = 'deal'?
2. Ja → Setze destruct_at = NOW() + destruct_after_deal_hours
3. Ab dann greift der Scheduled Flow

Integration in js/services/reputation.jsconfirmDeal():

// Nach beidseitiger Bestätigung:
if (listing.self_destruct && listing.destruct_mode === 'deal') {
  const destructAt = new Date()
  destructAt.setHours(destructAt.getHours() + (listing.destruct_after_deal_hours || 24))
  await directus.updateItem('listings', listing.id, { destruct_at: destructAt.toISOString() })
}

Privacy-Aspekte

  • Echte Löschung: Kein Soft-Delete, kein Archiv — Daten werden unwiderruflich entfernt
  • Bilder inklusive: Files werden aus Directus Storage gelöscht (nicht nur DB-Einträge)
  • Keine Suchmaschinen-Spuren: noindex Meta-Tag für Listings mit Selbstzerstörung
  • Countdown sichtbar: Käufer sehen, dass das Listing zeitlich begrenzt ist (schafft Urgency)

i18n Keys

destruct.title              = "Selbstzerstörung"
destruct.off                = "Aus (Standard)"
destruct.time               = "Nach Zeitablauf"
destruct.deal               = "Nach Deal-Abschluss"
destruct.manual             = "Jetzt zerstören"
destruct.duration.24h       = "24 Stunden"
destruct.duration.48h       = "48 Stunden"
destruct.duration.7d        = "7 Tage"
destruct.duration.30d       = "30 Tage"
destruct.afterDeal          = "{{hours}} Stunden nach Deal"
destruct.warning            = "Gelöschte Anzeigen können nicht wiederhergestellt werden."
destruct.active             = "Selbstzerstörung aktiv"
destruct.expiresIn          = "Läuft ab in {{time}}"
destruct.deletedAt          = "Wird gelöscht am {{date}}"
destruct.confirm            = "Anzeige unwiderruflich löschen?"
destruct.destroyed          = "Diese Anzeige wurde gelöscht"

Aufwand

  • Geschätzt: 1.52 Tage
  • Dependencies: Directus Flow (analog zu bestehender Expired-Listings-Flow)
  • Risiko: Directus File-Löschung muss getestet werden (Cascade)

Zusammenfassung

# Feature Aufwand Einzigartigkeit Privacy-Impact
1 Blind Meeting Points 23 Tage Sehr hoch — kein Konkurrent hat das Hoch
2 Verifiable Listings 12 Tage Hoch — innovative Lösung ohne KYC Mittel
3 Selbstzerstörende Listings 1.52 Tage Hoch — starkes Privacy-Signal Sehr hoch

Gesamt: ~57 Tage Implementierung

Empfohlene Reihenfolge

  1. Verifiable Listings — geringster Aufwand, sofort sichtbarer Wert, keine neue Dependency
  2. Selbstzerstörende Listings — baut auf bestehendem Flow auf, starkes Marketingmerkmal
  3. Blind Meeting Points — höchster Aufwand, aber stärkstes Alleinstellungsmerkmal