# 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**: 2–3 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**: 1–2 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.js` → `confirmDeal()`: ```js // 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.5–2 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 | 2–3 Tage | Sehr hoch — kein Konkurrent hat das | Hoch | | 2 | Verifiable Listings | 1–2 Tage | Hoch — innovative Lösung ohne KYC | Mittel | | 3 | Selbstzerstörende Listings | 1.5–2 Tage | Hoch — starkes Privacy-Signal | Sehr hoch | | 4 | Öffentliche Q&A | 1–2 Tage | Mittel — reduziert Chat-Last, baut Vertrauen | Mittel | **Gesamt: ~6–9 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 4. **Öffentliche Q&A** — kein MVP-Blocker, sinnvoll ab Open Beta --- ## 4. Öffentliche Q&A pro Listing **Problem:** Käufer stellen oft die gleichen Fragen per E2E-Chat. Der Verkäufer beantwortet sie immer wieder einzeln. Andere Interessenten sehen die Antworten nicht. **Lösung:** Öffentlicher Frage-Antwort-Bereich auf jeder Listing-Seite. Fragen werden unter Pseudonym gestellt (via `identity.js`), nur der Seller kann antworten. ### Directus: Collection `listing_questions` | Feld | Typ | Hinweise | |------|-----|----------| | `id` | UUID (auto) | Primary Key | | `listing` | M2O → `listings` | Zugehöriges Listing | | `question_user` | UUID | Fragender User (für Pseudonym via `identity.js`) | | `question_text` | Text | Fragetext | | `answer_text` | Text, nullable | Antwort des Sellers | | `date_created` | Timestamp (auto) | | | `date_answered` | DateTime, nullable | Wann beantwortet | | `status` | String, default `published` | `published` / `hidden` (Seller kann ausblenden) | ### Permissions | Rolle | Read | Create | Update | Delete | |-------|------|--------|--------|--------| | Public | ✓ (status=published) | ✓ (nur `question_*`) | – | – | | User | ✓ | ✓ | ✓ (nur eigene Antworten als Seller) | – | ### UX-Flow ``` Listing-Detail-Seite: ┌─────────────────────────────────────┐ │ 📸 Bilder, Titel, Preis ... │ │ │ │ ─── Fragen & Antworten (3) ───── │ │ │ │ 🐻 BlauerBär42: │ │ "Funktioniert auch mit 220V?" │ │ ↳ Seller: "Ja, 110–240V." │ │ │ │ 🦊 RoterFuchs17: │ │ "Versand möglich?" │ │ ↳ (noch nicht beantwortet) │ │ │ │ ┌─────────────────────────────┐ │ │ │ Frage stellen... │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ``` ### Anti-Spam - PoW-Captcha vor dem Absenden einer Frage - Rate-Limit: max. 3 Fragen pro Listing pro User - Seller kann Fragen ausblenden (`status → hidden`)