Herald LogoHerald Docs
Guides

Full Integration Examples

Copy-paste ready code for common Herald integration patterns.

Full Integration Examples

1. Backend Express Server

A complete Express.js server that monitors DeFi positions and sends liquidation alerts.

import "dotenv/config";
import express from "express";
import { Herald, ReadClient, HeraldError } from "@herald-protocol/sdk";

const app = express();
app.use(express.json());

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });
const readClient = new ReadClient({ cluster: "mainnet-beta" });

async function sendLiquidationAlert(wallet: string, healthFactor: number) {
  const isRegistered = await readClient.isRegistered(wallet);
  if (!isRegistered) {
    console.log(`Wallet ${wallet} not registered — skipping.`);
    return null;
  }

  return herald.notify({
    wallet,
    subject: "Liquidation Warning",
    body: `Your position health factor dropped to ${healthFactor.toFixed(2)}.`,
    category: "defi",
    priority: "critical",
    receipt: true,
    idempotencyKey: `liq_${wallet}_${Date.now()}`,
  });
}

app.post("/alert", async (req, res) => {
  const { wallet, healthFactor } = req.body;
  try {
    const result = await sendLiquidationAlert(wallet, healthFactor);
    if (!result) return res.json({ status: "skipped", reason: "unregistered" });
    res.json({ status: result.status, notificationId: result.notificationId });
  } catch (e) {
    if (e instanceof HeraldError) {
      return res.status(e.status).json({ error: e.code, message: e.message });
    }
    res.status(500).json({ error: "delivery_failed" });
  }
});

app.get("/status/:notificationId", async (req, res) => {
  const status = await herald.getStatus(req.params.notificationId);
  res.json(status);
});

app.listen(3001, () => console.log("Herald integration running on :3001"));

2. Next.js API Route (Serverless)

Trigger notifications from a Next.js app using Route Handlers.

// app/api/notify/route.ts
import { NextResponse } from "next/server";
import { Herald, HeraldError } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

export async function POST(request: Request) {
  const { wallet, subject, body, category } = await request.json();

  if (!wallet || !subject || !body) {
    return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
  }

  try {
    const result = await herald.notify({
      wallet,
      subject,
      body,
      category: category ?? "defi",
      idempotencyKey: crypto.randomUUID(),
    });

    return NextResponse.json(
      { notificationId: result.notificationId, status: result.status },
      { status: 202 }
    );
  } catch (e) {
    if (e instanceof HeraldError) {
      return NextResponse.json({ error: e.code, message: e.message }, { status: e.status });
    }
    return NextResponse.json({ error: "internal_error" }, { status: 500 });
  }
}

3. Bulk Notifications

Send to many wallets at once using notifyBulk. The SDK maps your wallet list to individual notification objects internally.

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

async function notifyGovernanceVoters(wallets: string[], proposalId: number) {
  // notifyBulk handles up to 100 wallets per call
  for (let i = 0; i < wallets.length; i += 100) {
    const chunk = wallets.slice(i, i + 100);

    const result = await herald.notifyBulk({
      wallets: chunk,
      subject: `Governance Proposal #${proposalId} Live`,
      body: `Vote on proposal #${proposalId} before it closes.`,
      category: "governance",
      receipt: true,
      idempotencyPrefix: `prop${proposalId}`, // each wallet gets `prop42:{wallet}`
    });

    result.results.forEach((r, idx) => {
      console.log(`Wallet ${chunk[idx]}: ${r.status}`);
    });
  }
}

4. Receipt Status Polling

Wait for a ZK receipt to land on-chain after sending a critical notification.

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

async function sendAndConfirmReceipt(wallet: string) {
  const result = await herald.notify({
    wallet,
    subject: "Security Alert",
    body: "Unusual activity detected on your account.",
    category: "security",
    priority: "critical",
    receipt: true,
    idempotencyKey: `sec_${wallet}_${Date.now()}`,
  });

  console.log(`Queued: ${result.notificationId}`);

  // Wait up to 60s for delivery, polling every 3s
  const delivered = await herald.waitForDelivery(result.notificationId, 60_000, 3_000);
  console.log(`Delivery status: ${delivered.status}`);

  if (delivered.receiptStatus === "confirmed") {
    console.log(`ZK receipt on-chain: ${delivered.receiptTx}`);
  } else if (delivered.receiptStatus === "failed") {
    console.warn(`Receipt failed: ${delivered.receiptFailureReason}`);
  }
}

5. Scheduled Notifications

Send a one-time reminder and a weekly recurring digest.

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

// One-time: notify at a specific time
const oneTime = await herald.scheduleOnce({
  wallet: "7xR4mKp2nQ...",
  subject: "Staking rewards ready",
  body: "You have 12.4 SOL in unclaimed staking rewards.",
  category: "defi",
  scheduledFor: "2026-06-01T09:00:00Z",
  timezone: "America/New_York",
});

console.log(`Scheduled ${oneTime.id} — fires at ${oneTime.nextRunAt}`);

// Recurring: every Monday at 09:00 UTC
const weekly = await herald.scheduleRecurring({
  wallet: "7xR4mKp2nQ...",
  subject: "Weekly portfolio summary",
  body: "Here is your weekly DeFi activity report.",
  category: "defi",
  cronExpr: "0 9 * * 1",
  timezone: "UTC",
});

console.log(`Recurring ${weekly.id} — next run: ${weekly.nextRunAt}`);

// Cancel a schedule
await herald.cancelScheduled(oneTime.id);

6. Broadcast to All Subscribers

Announce a governance vote to your entire subscriber list.

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

const result = await herald.broadcast({
  subject: "Governance Vote: Protocol Upgrade v2",
  body: "A governance proposal to upgrade the protocol is live. Vote before May 24.",
  category: "governance",
  receipt: false,
});

console.log(`Broadcast ${result.broadcast_id}`);
console.log(`Enqueued ${result.queued_count} / ${result.total_subscribers} subscribers`);
console.log(`Estimated delivery: ${result.estimated_delivery_s}s`);

Broadcast requires Growth tier or above.


7. Email Templates

Create a reusable template with dynamic variables, then send it.

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

// Create the template once
const { templateId } = await herald.createEmailTemplate({
  name: "Liquidation Alert",
  category: "defi",
  subjectTemplate: "Your {{protocol}} position is at risk",
  htmlSource: `
    <h1>Position Alert</h1>
    <p>Your health factor on <strong>{{protocol}}</strong> dropped to <strong>{{healthFactor}}</strong>.</p>
    <a href="{{dashboardUrl}}">Manage Position →</a>
  `,
  textSource: "Health factor on {{protocol}}: {{healthFactor}}. Manage: {{dashboardUrl}}",
  heraldFooter: "minimal",
});

// Use it when sending
await herald.notify({
  wallet: "7xR4mKp2nQ...",
  subject: "Position at risk",
  body: "Your health factor is critically low.",
  category: "defi",
  templateId,
  templateVariables: {
    protocol: "MarginFi",
    healthFactor: "1.03",
    dashboardUrl: "https://app.marginfi.com",
  },
});

8. Webhook Receiver with Signature Verification

Use the SDK's built-in verifyWebhookSignature static method.

// app/api/webhooks/herald/route.ts
import { NextResponse } from "next/server";
import { Herald } from "@herald-protocol/sdk";

export async function POST(request: Request) {
  const payload = await request.text();
  const sig = request.headers.get("x-herald-signature") ?? "";

  const isValid = await Herald.verifyWebhookSignature(
    payload,
    sig,
    process.env.HERALD_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(payload);

  switch (event.type) {
    case "notification.delivered":
      console.log(`Delivered: ${event.data.notification_id} via ${event.data.channel}`);
      break;
    case "notification.bounced":
      console.warn(`Bounced: ${event.data.notification_id}`);
      break;
    case "notification.failed":
      console.error(`Failed: ${event.data.notification_id}`);
      break;
    case "quota.exceeded":
      console.error("Monthly quota exceeded — upgrade tier!");
      break;
  }

  return NextResponse.json({ received: true });
}

9. Registering and Managing Webhooks via SDK

import { Herald } from "@herald-protocol/sdk";

const herald = new Herald({ apiKey: process.env.HERALD_API_KEY! });

// Register an endpoint
const webhook = await herald.createWebhook({
  url: "https://myprotocol.com/webhooks/herald",
  events: ["notification.delivered", "notification.failed", "notification.bounced"],
});

// Store this immediately — shown only once
console.log(`Webhook secret: ${webhook.secret}`);
console.log(`Webhook ID: ${webhook.id}`);

// Verify connectivity
await herald.testWebhook(webhook.id);

// Update event subscriptions
await herald.updateWebhook(webhook.id, {
  events: ["notification.delivered", "notification.bounced"],
  isActive: true,
});

// List all endpoints
const all = await herald.listWebhooks();
console.log(`${all.length} webhook(s) registered`);

// Remove when done
await herald.deleteWebhook(webhook.id);

10. React User Registration Check

Frontend component to check wallet registration status.

"use client";
import { useWallet } from "@solana/wallet-adapter-react";
import { ReadClient } from "@herald-protocol/sdk";
import { useEffect, useState } from "react";

export function NotificationSettings() {
  const { publicKey } = useWallet();
  const [isRegistered, setIsRegistered] = useState<boolean | null>(null);

  useEffect(() => {
    if (!publicKey) return;
    const client = new ReadClient({ cluster: "mainnet-beta" });
    client.isRegistered(publicKey.toBase58()).then(setIsRegistered);
  }, [publicKey]);

  if (!publicKey) return <p>Connect your wallet to check notification status.</p>;
  if (isRegistered === null) return <p>Checking...</p>;

  return (
    <div>
      {isRegistered ? (
        <p>Notifications are active for this wallet.</p>
      ) : (
        <a
          href={`https://notify.useherald.xyz/join?wallet=${publicKey.toBase58()}`}
          target="_blank"
          rel="noopener noreferrer"
        >
          Enable Notifications
        </a>
      )}
    </div>
  );
}

Next Steps

On this page