---
title: "Verifizierte Verkaufsbenachrichtigung mit Polar und Astro erstellen"
description: "Zeigen Sie echte Kaufaktivität auf Ihrer Marketing-Website, indem Sie aktuelle Polar-Bestellungen serverseitig abrufen, Antworten am Edge cachen und eine schlanke verifizierte Benachrichtigung im Browser rendern – ohne gefälschte Pop-ups."
source: "https://www.launchfa.st/de/blog/polar-verified-latest-sale-badge-astro"
author: "Rishi Raj Jain"
created_at: 2026-04-05T12:00:00.000Z
keywords: "astro, polar, polar.sh, verified notification, social proof, payments, api routes, vercel, serverless, typescript"
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="1600" height="900" alt="Verifizierte Verkaufsbenachrichtigung mit Polar und Astro erstellen" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/polar-verified-latest-sale-badge-astro.png" />

„Jemand aus **Deutschland** hat vor 2 Stunden **Pro** gekauft.“ Diese Zeile findet man auf fast jeder SaaS-Landingpage – und oft ist sie komplett erfunden. Besser ist es, **echte** Bestellungen aus Ihrem Zahlungsanbieter anzuzeigen. Wenn Sie mit **[Polar](https://polar.sh/)** verkaufen, können Sie aktuelle Bestellungen über die offizielle API abrufen, einen kleinen JSON-Endpunkt aus Ihrer Astro-Anwendung bereitstellen und dem Client ein dezentes Toast mit dem Label **Verifiziert** rendern lassen – damit Besucher wissen, dass das Signal aus Ihrem Checkout stammt und nicht aus einem Zufallsgenerator.

In dieser Anleitung binden Sie das [Polar SDK](https://github.com/polarsource/polar-js) in eine Astro-Server-Route ein, deployen auf Vercel und fügen eine schwebende Benachrichtigungskomponente hinzu, die durch aktuelle Verkäufe rotiert. Das Muster entspricht dem, das wir auf dieser Website verwenden: Geheimnisse nur auf dem Server, edge-freundliche Cache-Header und ein minimales Client-Skript.

## Inhaltsverzeichnis

- [Wie die Bausteine zusammenspielen](#how-the-pieces-fit-together)
- [Polar-Zugangstoken erstellen](#create-a-polar-access-token)
- [Astro mit dem Vercel-Adapter einrichten](#scaffold-astro-with-the-vercel-adapter)
- [API-Route implementieren](#implement-the-api-route)
- [Umgebungsvariablen lokal und auf Vercel](#environment-variables-locally-and-on-vercel)
- [Schwebende Benachrichtigung für aktuelle Verkäufe hinzufügen](#add-the-floating-recent-sale-notification)

## Voraussetzungen

Sie benötigen:

- [Node.js 20](https://nodejs.org/en/blog/announcements/v20-release-announce) oder höher
- Ein [Polar](https://polar.sh/)-Konto mit mindestens einem Produkt und (für aussagekräftige Demos) einigen Test- oder echten Bestellungen
- Ein [Vercel](https://vercel.com/)-Konto, wenn Sie dort deployen möchten

## Wie die Bausteine zusammenspielen

```
1. Browser
   → GET /api/sale (Ihre Astro-Route)
   → Erhält JSON: Produktname, ISO-Zeitstempel, optionales Rechnungsland-Label

2. Astro-API-Route (Node / Vercel Function)
   → Verwendet POLAR_ACCESS_TOKEN mit Polar SDK
   → Listet Bestellungen (neueste zuerst), mappt auf eine kleine öffentliche Struktur
   → Setzt Cache-Control für CDN-Caching

3. Polar API
   → Autoritative Liste von Bestellungen und Positionen
```

Das **Verifiziert**-Badge in der UI signalisiert, dass die Nutzlast **zum Anfragezeitpunkt serverseitig von Polar abgerufen** wurde (oder aus einer gecachten Antwort stammt, die so erzeugt wurde). Halten Sie das Access Token aus Client-Bundles und Umgebungspräfixen fern.

## Polar-Zugangstoken erstellen

1. Öffnen Sie das [Polar-Dashboard](https://polar.sh/) und melden Sie sich an.
2. Gehen Sie zu den **Settings** Ihrer Organisation und suchen Sie **Developers** (die Bezeichnung kann leicht variieren).
3. Erstellen Sie ein Token mit der Berechtigung **orders:read**.
4. Kopieren Sie das Token einmalig und speichern Sie es sicher – Sie fügen es in Vercel und lokal in `.env` als `POLAR_ACCESS_TOKEN` ein. (Committen Sie diesen Wert niemals in Git!)

## Astro mit dem Vercel-Adapter einrichten

Erstellen Sie ein neues Projekt (oder verwenden Sie ein bestehendes mit `output: 'server'`):

```bash
npm create astro@latest polar-sale-badge
cd polar-sale-badge
```

Fügen Sie den Vercel-Adapter hinzu, damit API-Routen beim Deploy als Serverless Functions laufen:

```bash
npx astro add vercel --yes
```

Stellen Sie sicher, dass `astro.config.mjs` **server**-Output verwendet. Eine minimale Konfiguration sieht so aus:

```mjs
import { defineConfig } from 'astro/config'
import vercel from '@astrojs/vercel'

export default defineConfig({
  output: 'server',
  adapter: vercel(),
})
```

Installieren Sie das Polar SDK:

```bash
npm install @polar-sh/sdk
```

## API-Route implementieren

Fügen Sie `src/pages/api/sale.ts` hinzu. Die wichtigen Details:

1. **`export const prerender = false`**, damit Astro diese URL nicht statisch zur Build-Zeit backt.
2. Instanziieren Sie **`Polar`** mit `import.meta.env.POLAR_ACCESS_TOKEN`.
3. Rufen Sie **`polar.orders.list`** mit Sortierung **neueste zuerst** und einem kleinen **limit** auf, und mappen Sie jedes Element auf das, was die UI braucht (Produktname, createdAt, optionales Rechnungsland).
4. Geben Sie **`Cache-Control`** zurück, damit das CDN von Vercel das JSON einen Tag cachen kann und gleichzeitig Revalidierung erlaubt (`max-age=0, s-maxage=86400` ist ein sinnvoller Ausgangspunkt).

```ts title="src/pages/api/sale.ts" collapse={0,7-248}
export const prerender = false

import { Polar } from '@polar-sh/sdk'

const countryCodeToName: { [key: string]: string } = {
  AF: 'Afghanistan',
  AL: 'Albania',
  DZ: 'Algeria',
  AS: 'American Samoa',
  AD: 'Andorra',
  AO: 'Angola',
  AI: 'Anguilla',
  AQ: 'Antarctica',
  AG: 'Antigua and Barbuda',
  AR: 'Argentina',
  AM: 'Armenia',
  AW: 'Aruba',
  AU: 'Australia',
  AT: 'Austria',
  AZ: 'Azerbaijan',
  BS: 'Bahamas',
  BH: 'Bahrain',
  BD: 'Bangladesh',
  BB: 'Barbados',
  BY: 'Belarus',
  BE: 'Belgium',
  BZ: 'Belize',
  BJ: 'Benin',
  BM: 'Bermuda',
  BT: 'Bhutan',
  BO: 'Bolivia',
  BA: 'Bosnia and Herzegovina',
  BW: 'Botswana',
  BV: 'Bouvet Island',
  BR: 'Brazil',
  IO: 'British Indian Ocean Territory',
  BN: 'Brunei Darussalam',
  BG: 'Bulgaria',
  BF: 'Burkina Faso',
  BI: 'Burundi',
  KH: 'Cambodia',
  CM: 'Cameroon',
  CA: 'Canada',
  CV: 'Cape Verde',
  KY: 'Cayman Islands',
  CF: 'Central African Republic',
  TD: 'Chad',
  CL: 'Chile',
  CN: 'China',
  CX: 'Christmas Island',
  CC: 'Cocos (Keeling) Islands',
  CO: 'Colombia',
  KM: 'Comoros',
  CG: 'Congo',
  CD: 'Congo, Democratic Republic of the',
  CK: 'Cook Islands',
  CR: 'Costa Rica',
  CI: "Côte d'Ivoire",
  HR: 'Croatia',
  CU: 'Cuba',
  CY: 'Cyprus',
  CZ: 'Czech Republic',
  DK: 'Denmark',
  DJ: 'Djibouti',
  DM: 'Dominica',
  DO: 'Dominican Republic',
  EC: 'Ecuador',
  EG: 'Egypt',
  SV: 'El Salvador',
  GQ: 'Equatorial Guinea',
  ER: 'Eritrea',
  EE: 'Estonia',
  ET: 'Ethiopia',
  FK: 'Falkland Islands (Malvinas)',
  FO: 'Faroe Islands',
  FJ: 'Fiji',
  FI: 'Finland',
  FR: 'France',
  GF: 'French Guiana',
  PF: 'French Polynesia',
  TF: 'French Southern Territories',
  GA: 'Gabon',
  GM: 'Gambia',
  GE: 'Georgia',
  DE: 'Germany',
  GH: 'Ghana',
  GI: 'Gibraltar',
  GR: 'Greece',
  GL: 'Greenland',
  GD: 'Grenada',
  GP: 'Guadeloupe',
  GU: 'Guam',
  GT: 'Guatemala',
  GG: 'Guernsey',
  GN: 'Guinea',
  GW: 'Guinea-Bissau',
  GY: 'Guyana',
  HT: 'Haiti',
  HM: 'Heard Island and McDonald Islands',
  VA: 'Holy See (Vatican City State)',
  HN: 'Honduras',
  HK: 'Hong Kong',
  HU: 'Hungary',
  IS: 'Iceland',
  IN: 'India',
  ID: 'Indonesia',
  IR: 'Iran, Islamic Republic of',
  IQ: 'Iraq',
  IE: 'Ireland',
  IM: 'Isle of Man',
  IL: 'Israel',
  IT: 'Italy',
  JM: 'Jamaica',
  JP: 'Japan',
  JE: 'Jersey',
  JO: 'Jordan',
  KZ: 'Kazakhstan',
  KE: 'Kenya',
  KI: 'Kiribati',
  KP: "Korea, Democratic People's Republic of",
  KR: 'Korea, Republic of',
  KW: 'Kuwait',
  KG: 'Kyrgyzstan',
  LA: "Lao People's Democratic Republic",
  LV: 'Latvia',
  LB: 'Lebanon',
  LS: 'Lesotho',
  LR: 'Liberia',
  LY: 'Libyan Arab Jamahiriya',
  LI: 'Liechtenstein',
  LT: 'Lithuania',
  LU: 'Luxembourg',
  MO: 'Macao',
  MK: 'Macedonia, The Former Yugoslav Republic of',
  MG: 'Madagascar',
  MW: 'Malawi',
  MY: 'Malaysia',
  MV: 'Maldives',
  ML: 'Mali',
  MT: 'Malta',
  MH: 'Marshall Islands',
  MQ: 'Martinique',
  MR: 'Mauritania',
  MU: 'Mauritius',
  YT: 'Mayotte',
  MX: 'Mexico',
  FM: 'Micronesia, Federated States of',
  MD: 'Moldova, Republic of',
  MC: 'Monaco',
  MN: 'Mongolia',
  ME: 'Montenegro',
  MS: 'Montserrat',
  MA: 'Morocco',
  MZ: 'Mozambique',
  MM: 'Myanmar',
  NA: 'Namibia',
  NR: 'Nauru',
  NP: 'Nepal',
  NL: 'Netherlands',
  NC: 'New Caledonia',
  NZ: 'New Zealand',
  NI: 'Nicaragua',
  NE: 'Niger',
  NG: 'Nigeria',
  NU: 'Niue',
  NF: 'Norfolk Island',
  MP: 'Northern Mariana Islands',
  NO: 'Norway',
  OM: 'Oman',
  PK: 'Pakistan',
  PW: 'Palau',
  PS: 'Palestinian Territory, Occupied',
  PA: 'Panama',
  PG: 'Papua New Guinea',
  PY: 'Paraguay',
  PE: 'Peru',
  PH: 'Philippines',
  PN: 'Pitcairn',
  PL: 'Poland',
  PT: 'Portugal',
  PR: 'Puerto Rico',
  QA: 'Qatar',
  RE: 'Réunion',
  RO: 'Romania',
  RU: 'Russian Federation',
  RW: 'Rwanda',
  BL: 'Saint Barthélemy',
  SH: 'Saint Helena',
  KN: 'Saint Kitts and Nevis',
  LC: 'Saint Lucia',
  MF: 'Saint Martin (French part)',
  PM: 'Saint Pierre and Miquelon',
  VC: 'Saint Vincent and the Grenadines',
  WS: 'Samoa',
  SM: 'San Marino',
  ST: 'Sao Tome and Principe',
  SA: 'Saudi Arabia',
  SN: 'Senegal',
  RS: 'Serbia',
  SC: 'Seychelles',
  SL: 'Sierra Leone',
  SG: 'Singapore',
  SK: 'Slovakia',
  SI: 'Slovenia',
  SB: 'Solomon Islands',
  SO: 'Somalia',
  ZA: 'South Africa',
  GS: 'South Georgia and the South Sandwich Islands',
  ES: 'Spain',
  LK: 'Sri Lanka',
  SD: 'Sudan',
  SR: 'Suriname',
  SJ: 'Svalbard and Jan Mayen',
  SZ: 'Swaziland',
  SE: 'Sweden',
  CH: 'Switzerland',
  SY: 'Syrian Arab Republic',
  TW: 'Taiwan, Province of China',
  TJ: 'Tajikistan',
  TZ: 'Tanzania, United Republic of',
  TH: 'Thailand',
  TL: 'Timor-Leste',
  TG: 'Togo',
  TK: 'Tokelau',
  TO: 'Tonga',
  TT: 'Trinidad and Tobago',
  TN: 'Tunisia',
  TR: 'Turkey',
  TM: 'Turkmenistan',
  TC: 'Turks and Caicos Islands',
  TV: 'Tuvalu',
  UG: 'Uganda',
  UA: 'Ukraine',
  AE: 'United Arab Emirates',
  GB: 'United Kingdom',
  US: 'United States',
  UM: 'United States Minor Outlying Islands',
  UY: 'Uruguay',
  UZ: 'Uzbekistan',
  VU: 'Vanuatu',
  VE: 'Venezuela',
  VN: 'Viet Nam',
  VG: 'Virgin Islands, British',
  VI: 'Virgin Islands, U.S.',
  WF: 'Wallis and Futuna',
  EH: 'Western Sahara',
  YE: 'Yemen',
  ZM: 'Zambia',
  ZW: 'Zimbabwe',
}

export async function GET() {
  const polar = new Polar({
    accessToken: import.meta.env.POLAR_ACCESS_TOKEN,
  })
  let orders: { date: string; billingCountry: string | undefined; name: string }[] = []
  const result = await polar.orders.list({
    sorting: ['-created_at'],
    limit: 10,
  })
  for await (const page of result) {
    orders = page.result.items
      .map((item) => ({
        name: item.product.name,
        date: item.createdAt.toISOString(),
        billingCountry: item.billingAddress?.country ? countryCodeToName[item.billingAddress.country] || item.billingAddress.country : undefined,
      }))
      .filter((order) => order.name.includes('LaunchFast'))
      .slice(0, 5)
    break
  }
  return new Response(JSON.stringify(orders), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'max-age=0, s-maxage=86400',
    },
  })
}
```

**Nach Produkt filtern:** In der Produktion möchten Sie eventuell nur bestimmte SKUs anzeigen (z. B. Namen, die Ihre Produktfamilie enthalten). Nach dem Mapping hält `.filter((order) => order.name.includes('YourBrand'))` das Toast relevant.

**Ländernamen:** Polar liefert einen Ländercode (wie `DE`, `US` usw.) im Feld `billingAddress.country`, nicht den vollständigen Ländernamen. Um den korrekten Ländernamen anzuzeigen, mappen Sie diesen Code mit einer Lookup-Tabelle – wie in der API-Route oben gezeigt.

## Umgebungsvariablen lokal und auf Vercel

Erstellen Sie `.env` (und fügen Sie `.env` zu `.gitignore` hinzu, falls noch nicht geschehen):

```bash
POLAR_ACCESS_TOKEN="polar_oat_…"
```

Für Vercel:

1. Projekt → Settings → Environment Variables
2. Fügen Sie `POLAR_ACCESS_TOKEN` für **Production** hinzu (und **Preview**, wenn Sie PR-Deployments testen)
3. Deployen Sie erneut, damit der neue Wert verfügbar ist

Lokal starten:

```bash
npm run dev
```

Besuchen Sie `http://localhost:4321/api/sale` und prüfen Sie, ob Sie ein JSON-Array erhalten.

## Schwebende Benachrichtigung für aktuelle Verkäufe hinzufügen

Erstellen Sie eine Komponente wie `src/components/SalesPopup.astro` mit folgendem Code:

```astro title="src/components/SalesPopup.astro"
---

---

<script>
  let popupTimeout
  let salesItems: any[] = []
  let currentIndex = 0

  function showSalePopup(item) {
    // Entfernen, falls bereits vorhanden
    const existing = document.getElementById('sale-popup')
    if (existing) existing.remove()
    const popup = document.createElement('a')
    popup.id = 'sale-popup'
    popup.href = '/#pricing'
    popup.className = `fixed bottom-6 right-6 z-50 bg-white shadow-lg rounded-xl px-6 py-4 flex flex-col items-start min-w-[270px] max-w-xs border border-gray-100 transition-transform duration-500 translate-y-8 opacity-0 pointer-events-none`
    popup.innerHTML = `
          <div class="text-sm text-gray-700">
            Jemand aus <span class="font-bold text-branding">${item.billingCountry ? item.billingCountry : 'Unbekannt'}</span> hat <span class="font-bold text-branding">${item.name}</span> gekauft
          </div>
          <div class="mt-1 text-xs text-gray-500 flex items-center gap-2">
            <span>${timeAgo(item.date)}</span>
            <span>| <svg class="inline-block size-3 text-branding" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M269.4 2.9C265.2 1 260.7 0 256 0s-9.2 1-13.4 2.9L54.3 82.8c-22 9.3-38.4 31-38.3 57.2c.5 99.2 41.3 280.7 213.6 363.2c16.7 8 36.1 8 52.8 0C454.7 420.7 495.5 239.2 496 140c.1-26.2-16.3-47.9-38.3-57.2L269.4 2.9zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"></path></svg> <span class="text-branding font-semibold">Verifiziert</span></span>
          </div>
        `
    document.body.appendChild(popup)
    // Einblenden
    setTimeout(() => {
      popup.classList.remove('translate-y-8', 'opacity-0', 'pointer-events-none')
      popup.classList.add('translate-y-0', 'opacity-100', 'pointer-events-auto')
    }, 10)
    // Nach 3 s entfernen
    clearTimeout(popupTimeout)
    popupTimeout = setTimeout(() => {
      popup.classList.remove('translate-y-0', 'opacity-100', 'pointer-events-auto')
      popup.classList.add('translate-y-8', 'opacity-0', 'pointer-events-none')
      setTimeout(() => popup.remove(), 500)
    }, 3000)
  }

  function timeAgo(dateStr) {
    const now = new Date()
    const date = new Date(dateStr)
    const diff = Math.floor((now.getTime() - date.getTime()) / 1000)
    if (diff < 60) return `vor ${diff} Sekunden`
    if (diff < 3600) return `vor ${Math.floor(diff / 60)} ${Math.floor(diff / 60) === 1 ? 'Minute' : 'Minuten'}`
    if (diff < 86400) return `vor ${Math.floor(diff / 3600)} ${Math.floor(diff / 3600) === 1 ? 'Stunde' : 'Stunden'}`
    return `vor ${Math.floor(diff / 86400)} ${Math.floor(diff / 86400) === 1 ? 'Tag' : 'Tagen'}`
  }

  function fetchAndShow() {
    if (Array.isArray(salesItems) && salesItems.length > 0) {
      // Sequenziell, nicht zufällig
      const item = salesItems[currentIndex]
      if (item && item.billingCountry) showSalePopup(item)
      currentIndex = (currentIndex + 1) % salesItems.length
    }
  }

  fetch('/api/sale')
    .then((res) => res.json())
    .then((res) => {
      salesItems = Array.isArray(res) ? res : []
      currentIndex = 0
      fetchAndShow()
      setInterval(fetchAndShow, 10000)
    })
</script>
```

Der Code oben macht Folgendes:

- `fetch('/api/sale')` nach dem Laden der Seite.
- Speichert das Array im Speicher und **rotiert** Einträge in einem Intervall (z. B. alle 10 Sekunden).
- Rendert eine kleine fixierte Karte (unten rechts ist üblich) mit:
  - Land (oder „Unbekannt“, wenn Sie keine grobe Standortangabe zeigen möchten)
  - Produktname
  - **Relative Zeit** („vor 3 Minuten“) aus dem ISO-`date`
  - Eine **Verifiziert**-Zeile mit Schild-Icon, die verdeutlicht, dass die Daten von Polar stammen

Halten Sie das Skript schlank: schlichte DOM-APIs oder ein kleines `timeAgo`-Hilfsmittel reichen aus. Verlinken Sie die Karte mit Ihrem Pricing-Bereich, damit der Hinweis eine klare Handlungsaufforderung hat.

Wenn Sie strengere Datenschutzerwartungen erfüllen möchten, zeigen Sie nur die Region an oder lassen Sie Geografie ganz weg und zeigen nur Produkt und Zeit.

## Ergebnis

<img width="679" height="211" alt="Verifizierte Verkaufsbenachrichtigung mit Polar und Astro erstellen" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/sales-popup-blog.png" />

## Fazit

Sie haben jetzt einen **Polar**-gestützten Verkaufs-Feed: Die Astro-API-Route verwaltet das Geheimnis, Vercel liefert sie als Serverless Function mit CDN-Caching, und der Browser sieht nur anonymisierte Felder, die Sie freigeben. Passen Sie das Styling an Ihre Marke an, optimieren Sie Intervalle und Filter – und Sie erhalten glaubwürdigen Social Proof, ohne eine separate Analytics-Pipeline für „gefälschte“ Benachrichtigungen zu pflegen.
