Skip to main content

Overview

The announcements API provides access to stealth address announcements. When a payment is sent to a stealth address, an announcement is published on-chain and indexed by the backend. Wallets scan announcements to detect incoming payments.
This endpoint is public and does not require authentication.

Privacy Considerations

The viewTag filter is OPTIONAL. Wallets SHOULD fetch all announcements and filter locally to avoid revealing their view tag set to the backend (privacy invariant #5).
Fetching all announcements:
  • Preserves privacy by not revealing which view tags you’re interested in
  • Allows backend to serve cached results efficiently
  • Prevents traffic analysis attacks

Endpoints

Query Announcements

Query stealth announcements with optional filters.
GET /api/identipay/v1/announcements

Query Parameters

since
string
ISO 8601 timestamp - only return announcements after this time
viewTag
integer
View tag filter (0-255) - OPTIONAL for privacy
limit
integer
default:100
Number of results to return (1-1000)
cursor
string
Pagination cursor (UUID from previous response)

Response

announcements
array
Array of announcement objects
nextCursor
string
Cursor for next page (null if no more results)

Example Request

curl "https://api.identipay.com/api/identipay/v1/announcements?limit=100"

Example Response

{
  "announcements": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "ephemeralPubkey": "a1b2c3d4e5f6...",
      "viewTag": 42,
      "stealthAddress": "0xstealth123...",
      "metadata": "encrypted_payload_hex...",
      "txDigest": "9x4Kb2FgH8...",
      "timestamp": "2026-03-09T20:15:32.000Z"
    },
    {
      "id": "650e8400-e29b-41d4-a716-446655440001",
      "ephemeralPubkey": "f6e5d4c3b2a1...",
      "viewTag": 99,
      "stealthAddress": "0xstealth456...",
      "metadata": null,
      "txDigest": "7yZ3Jb1Hf9...",
      "timestamp": "2026-03-09T20:16:45.000Z"
    }
  ],
  "nextCursor": "750e8400-e29b-41d4-a716-446655440002"
}

Error Responses

Implementation Details

Announcement Indexing

The backend runs a background indexer that polls for StealthAnnouncement events every 3 seconds:
async function pollAnnouncementEvents() {
  let cursor = await loadCursor(ANNOUNCEMENT_CURSOR_KEY);
  
  const result = await suiService.pollAnnouncementEvents(cursor);
  
  for (const event of result.events) {
    await db.insert(announcements).values({
      ephemeralPubkey: event.ephemeralPubkey,
      viewTag: event.viewTag,
      stealthAddress: event.stealthAddress,
      metadata: event.metadata,
      txDigest: event.txDigest,
      timestamp: new Date(parseInt(event.timestamp))
    });
  }
  
  if (result.nextCursor) {
    cursor = result.nextCursor;
    await saveCursor(ANNOUNCEMENT_CURSOR_KEY, cursor);
  }
}
See main.ts:167-192 for the full implementation.

Query Implementation

The query endpoint (see routes/announcements.ts:14-58) supports:
  1. Time-based filtering: since parameter filters by timestamp
  2. View tag filtering: Optional viewTag parameter
  3. Pagination: Cursor-based pagination with configurable limit
  4. Ordering: Results ordered by timestamp (descending)

Database Schema

CREATE TABLE announcements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ephemeral_pubkey TEXT NOT NULL,
  view_tag INTEGER NOT NULL,
  stealth_address TEXT NOT NULL,
  metadata TEXT,
  tx_digest TEXT NOT NULL,
  timestamp TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_announcements_timestamp ON announcements(timestamp DESC);
CREATE INDEX idx_announcements_view_tag ON announcements(view_tag);

View Tags

View tags are 8-bit values (0-255) that allow recipients to quickly filter announcements:
  • Sender computes: viewTag = hash(recipientViewKey, ephemeralKey) % 256
  • Recipient checks: Does this announcement match one of my view tags?
  • Privacy: Viewing all announcements is more private than filtering by specific tags

Wallet Scanning Flow

Privacy-Preserving Scan

// 1. Fetch all recent announcements (privacy-preserving)
const { announcements } = await fetch(
  '/api/identipay/v1/announcements?since=2026-03-09T00:00:00Z&limit=1000'
).then(r => r.json());

// 2. Compute my view tags locally
const myViewTags = computeMyViewTags(viewKey); // e.g., [42, 99, 156, ...]

// 3. Filter announcements locally
const candidates = announcements.filter(a => myViewTags.includes(a.viewTag));

// 4. For each candidate, check if it's actually mine
for (const announcement of candidates) {
  const isMineStealth = checkStealthAddress(
    announcement.stealthAddress,
    announcement.ephemeralPubkey,
    mySpendKey,
    myViewKey
  );
  
  if (isMine) {
    // Found an incoming payment!
    await processIncomingPayment(announcement);
  }
}

Incremental Sync

Wallets can sync incrementally using timestamps:
// Load last sync time from local storage
const lastSync = localStorage.getItem('lastAnnouncementSync') || '2026-01-01T00:00:00Z';

// Fetch only new announcements
const response = await fetch(
  `/api/identipay/v1/announcements?since=${lastSync}&limit=1000`
);
const { announcements } = await response.json();

// Process new announcements
for (const announcement of announcements) {
  await scanAnnouncement(announcement);
}

// Update sync timestamp
localStorage.setItem('lastAnnouncementSync', new Date().toISOString());

Privacy Best Practices

Always fetch all announcements and filter locally
Use the since parameter to sync only new announcements
Don’t use the viewTag parameter unless absolutely necessary
Filtering by view tag on the server reveals information about your wallet to the backend

Build docs developers (and LLMs) love