attn-protocol

ATTN Framework

Hook-based framework for building Bitcoin-native attention marketplace implementations using the ATTN Protocol on Nostr.

Overview

The ATTN Framework provides a Rely-style hook system for receiving and processing ATTN Protocol events. It handles Nostr relay connections, Bitcoin block synchronization, and event lifecycle management, allowing you to focus on implementing your marketplace logic.

The framework depends on @attn/ts-core for shared constants and type definitions. Event kind constants are available from the core package for consistency across the ATTN Protocol ecosystem.

Installation

# JSR (recommended)
bunx jsr add @attn/ts-framework
# or
npx jsr add @attn/ts-framework

# npm
npm install @attn/ts-framework

Quick Start

import { Attn } from "@attn/ts-framework";

// Basic usage (uses default relay: wss://relay.attnprotocol.org)
const attn = new Attn({
  private_key: myPrivateKey, // Uint8Array for NIP-42
});

// With explicit relay configuration (recommended pattern)
const attnWithRelays = new Attn({
  relays_auth: ["wss://auth-relay.example.com"],
  relays_noauth: ["wss://public-relay.example.com"],
  relays_write_auth: ["wss://auth-relay.example.com"],
  relays_write_noauth: ["wss://public-relay.example.com"],
  private_key: myPrivateKey,
  node_pubkeys: [node_pubkey], // Optional: filter block events by trusted nodes
});

// With marketplace filtering
const filteredAttn = new Attn({
  relays_auth: ["wss://auth-relay.example.com"],
  relays_noauth: ["wss://public-relay.example.com"],
  relays_write_auth: ["wss://auth-relay.example.com"],
  relays_write_noauth: ["wss://public-relay.example.com"],
  private_key: myPrivateKey,
  marketplace_pubkeys: [example_marketplace_pubkey],
});

// With identity publishing (kind 0 profile, kind 10002 relay list)
const attnWithProfile = new Attn({
  relays_auth: ["wss://auth-relay.example.com"],
  relays_noauth: ["wss://public-relay.example.com"],
  relays_write_auth: ["wss://auth-relay.example.com"],
  relays_write_noauth: ["wss://public-relay.example.com"],
  private_key: myPrivateKey,
  profile: {
    name: "My Marketplace",
    about: "An ATTN Protocol marketplace",
    nip05: "marketplace@example.com",
  },
});

// Register hooks for event processing
attn.on_marketplace_event(async (context) => {
  console.log("Marketplace updated:", context.event);
});

attn.on_promotion_event(async (context) => {
  console.log("New promotion received:", context.event);
});

attn.on_attention_event(async (context) => {
  console.log("New attention received:", context.event);
});

attn.on_block_event(async (context) => {
  console.log(`Block ${context.block_height} hash ${context.block_hash}`);
});

attn.before_block_event(async (context) => {
  console.log(`Preparing for block ${context.block_height}`);
});

attn.after_block_event(async (context) => {
  console.log(`Finished processing block ${context.block_height}`);
});

// Connect to all relays
await attn.connect();

Core Features

Relay Connection Management

The framework handles Nostr relay connections, including:

Bitcoin Block Synchronization

ATTN Protocol Event Subscriptions

You can import event kind constants from @attn/ts-core:

import { ATTN_EVENT_KINDS } from "@attn/ts-core";

// Use constants instead of hardcoded numbers
if (event.kind === ATTN_EVENT_KINDS.PROMOTION) {
  // Handle promotion event
}

Standard Nostr Event Subscriptions

Event Lifecycle Hooks

The framework provides hooks for all stages of the attention marketplace lifecycle. Each event type has before_*_event, on_*_event, and after_*_event hooks:

Standard Nostr Event Hooks

The framework also subscribes to standard Nostr events for enhanced functionality (with before/after lifecycle):

Configuration

interface AttnConfig {
  // Relay Configuration
  relays?: string[]; // @deprecated - use relays_auth/relays_noauth instead
  relays_auth?: string[]; // Relays requiring NIP-42 authentication
  relays_noauth?: string[]; // Relays not requiring authentication
  // Default: ['wss://relay.attnprotocol.org'] (noauth)

  // Write Relay Configuration (for publishing events)
  relays_write_auth?: string[]; // Write relays requiring NIP-42 auth
  relays_write_noauth?: string[]; // Write relays not requiring auth
  // Default: uses subscription relays if not specified

  // Authentication
  private_key: Uint8Array; // 32-byte private key for signing

  // Event Filtering (all optional)
  node_pubkeys?: string[]; // Filter block events by trusted node pubkeys
  marketplace_pubkeys?: string[]; // Filter by marketplace pubkeys
  marketplace_d_tags?: string[]; // Filter marketplace events by d-tags
  billboard_pubkeys?: string[]; // Filter by billboard pubkeys
  advertiser_pubkeys?: string[]; // Filter by advertiser pubkeys

  // Subscription Options
  subscription_since?: number; // Unix timestamp to filter events from

  // Identity Publishing
  profile?: ProfileConfig; // Profile metadata for kind 0 event
  follows?: string[]; // Pubkeys for kind 3 follow list
  publish_identity_on_connect?: boolean; // Default: true if profile is set

  // Connection Options
  auto_reconnect?: boolean; // Default: true
  deduplicate?: boolean; // Default: true
  connection_timeout_ms?: number; // Default: 30000
  reconnect_delay_ms?: number; // Default: 5000
  max_reconnect_attempts?: number; // Default: 10
  auth_timeout_ms?: number; // Default: 10000

  // Logging
  logger?: Logger; // Custom logger instance (defaults to Pino)
}

interface ProfileConfig {
  name: string; // Display name (required)
  about?: string; // Profile bio/description
  picture?: string; // Avatar image URL
  banner?: string; // Banner image URL
  website?: string; // Website URL
  nip05?: string; // NIP-05 identifier (e.g., 'user@domain.com')
  lud16?: string; // Lightning address (e.g., 'user@getalby.com')
  display_name?: string; // Alternative display name
  bot?: boolean; // Whether this account is a bot
}

Pubkey Filtering

marketplace_pubkeys, billboard_pubkeys, and advertiser_pubkeys each scope only the event kinds that include those p tags (e.g., marketplace filters affect MARKETPLACE + MARKETPLACE_CONFIRMATION events, billboard filters apply to BILLBOARD + BILLBOARD_CONFIRMATION, etc.), so enabling one filter no longer hides unrelated traffic.

Configuration Validation

The framework validates configuration at runtime:

Validation errors are thrown as exceptions with descriptive error messages.

Hook System

The framework uses a Rely-style hook system. Register handlers using on_*, before_*, and after_* methods:

// Register a hook handler
const handle = attn.on_promotion_event(async (context) => {
  // Process promotion event
});

// Register before/after hooks for lifecycle management
attn.before_promotion_event(async (context) => {
  // Prepare state before processing
});

attn.after_promotion_event(async (context) => {
  // Cleanup after processing
});

// Unregister the handler
handle.unregister();

Hook Context Types

All hooks provide typed context objects:

import type {
  // Infrastructure
  RelayConnectContext,
  RelayDisconnectContext,
  SubscriptionContext,
  // Identity Publishing
  ProfilePublishedContext,
  PublishResult,
  // ATTN Protocol Events
  MarketplaceEventContext,
  BillboardEventContext,
  PromotionEventContext,
  AttentionEventContext,
  MatchEventContext,
  MatchPublishedContext,
  // Confirmations
  BillboardConfirmationEventContext,
  AttentionConfirmationEventContext,
  MarketplaceConfirmationEventContext,
  AttentionPaymentConfirmationEventContext,
  // Block Synchronization
  BlockEventContext,
  BlockData,
  BlockGapDetectedContext,
  // Standard Nostr Events
  ProfileEventContext,
  RelayListEventContext,
  Nip51ListEventContext,
  // Error Handling
  RateLimitContext,
  HealthChangeContext,
} from "@attn/ts-framework";

Lifecycle

The framework follows a deterministic lifecycle sequence. See HOOKS.md for detailed documentation on the hook lifecycle sequence, execution order, and when each hook fires.

Bitcoin-Native Design

The framework is designed for Bitcoin-native operations:

Error Handling

The framework provides hooks for error scenarios:

attn.on_relay_disconnect(async (context) => {
  console.error("Disconnected:", context.reason);
  // Handle reconnection logic
});

attn.on_rate_limit(async (context) => {
  console.warn("Rate limited:", context.relay_url || "unknown relay");
  // Implement backoff strategy
});

attn.on_health_change(async (context) => {
  console.log("Health changed:", context.health_status);
  // Update service status
});

Type Safety

All hook handlers are fully typed with TypeScript:

import type { HookHandler, PromotionEventContext } from "@attn/ts-framework";

const handler: HookHandler<PromotionEventContext> = async (context) => {
  // context is fully typed
  const event = context.event;
  const event_id = context.event_id;
  const pubkey = context.pubkey;
  const promotion_data = context.promotion_data;
};

Implementation Patterns

Pattern 1: Block-Synchronized Processing

The framework provides hooks for block-synchronized processing, ensuring all operations align with Bitcoin block boundaries:

import { Attn } from "@attn/ts-framework";

const attn = new Attn({
  relays_auth: ["wss://auth-relay.example.com"],
  relays_noauth: ["wss://public-relay.example.com"],
  relays_write_auth: ["wss://auth-relay.example.com"],
  relays_write_noauth: ["wss://public-relay.example.com"],
  private_key,
  node_pubkeys: [node_pubkey_hex], // Optional: filter by trusted nodes
});

// Framework hook pattern for block-synchronized processing
attn.before_block_event(async (context) => {
  // Prepare state for new block
  console.log(`Preparing for block ${context.block_height}`);
  await finalize_current_block_transactions();
  await prepare_state_for_new_block();
});

attn.on_block_event(async (context) => {
  // Process new block
  const block_height = context.block_height;
  const block_hash = context.block_hash;
  console.log(`Processing block ${block_height} (hash: ${block_hash})`);

  // Process all events for this block
  await process_block_snapshot(block_height);

  // Run matching engine for this block
  await run_matching_engine(block_height);
});

attn.after_block_event(async (context) => {
  // Cleanup after block processing
  console.log(`Finished processing block ${context.block_height}`);
  await reset_state_for_next_block();
  await archive_block_data(context.block_height);
});

await attn.connect();

This pattern ensures that:

Pattern 2: Direct Event Publishing

For publishing events directly to relays, use the exported Publisher class:

import { Publisher } from "@attn/ts-framework";

const publisher = new Publisher({
  private_key,
  write_relays: [
    { url: "wss://relay.example.com", requires_auth: false },
  ],
  read_relays: ["wss://relay.example.com"],
});

// Publish profile (kind 0)
const profile_result = await publisher.publish_profile({
  name: "My Service",
  about: "An ATTN Protocol service",
});

// Publish relay list (kind 10002)
const relay_list_result = await publisher.publish_relay_list();

// Publish follow list (kind 3)
const follow_result = await publisher.publish_follow_list([pubkey1, pubkey2]);

License

MIT