Hook-based framework for building Bitcoin-native attention marketplace implementations using the ATTN Protocol on Nostr.
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.
# JSR (recommended)
bunx jsr add @attn/ts-framework
# or
npx jsr add @attn/ts-framework
# npm
npm install @attn/ts-framework
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();
The framework handles Nostr relay connections, including:
CITY_PROTOCOL_KINDS.BLOCK / 38808)clock_pubkeys for securityATTN_EVENT_KINDS.MARKETPLACE (38188)ATTN_EVENT_KINDS.BILLBOARD (38288)ATTN_EVENT_KINDS.PROMOTION (38388)ATTN_EVENT_KINDS.ATTENTION (38488)ATTN_EVENT_KINDS.BILLBOARD_CONFIRMATION (38588)ATTN_EVENT_KINDS.ATTENTION_CONFIRMATION (38688)ATTN_EVENT_KINDS.MARKETPLACE_CONFIRMATION (38788)ATTN_EVENT_KINDS.MATCH (38888)ATTN_EVENT_KINDS.ATTENTION_PAYMENT_CONFIRMATION (38988)marketplace_pubkeys, billboard_pubkeys, or advertiser_pubkeysYou 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
}
The framework provides hooks for all stages of the attention marketplace lifecycle. Each event type has before_*_event, on_*_event, and after_*_event hooks:
on_relay_connect, on_relay_disconnect, on_subscriptionon_profile_published (emitted after kind 0, 10002, and optionally kind 3 are published)before_marketplace_event, on_marketplace_event, after_marketplace_eventbefore_billboard_event, on_billboard_event, after_billboard_eventbefore_promotion_event, on_promotion_event, after_promotion_eventbefore_attention_event, on_attention_event, after_attention_eventbefore_match_event, on_match_event, after_match_eventon_match_published (backward compatibility, includes promotion/attention IDs)before_billboard_confirmation_event, on_billboard_confirmation_event, after_billboard_confirmation_eventbefore_attention_confirmation_event, on_attention_confirmation_event, after_attention_confirmation_eventbefore_marketplace_confirmation_event, on_marketplace_confirmation_event, after_marketplace_confirmation_eventbefore_attention_payment_confirmation_event, on_attention_payment_confirmation_event, after_attention_payment_confirmation_eventbefore_block_event, on_block_event, after_block_event, on_block_gap_detectedon_rate_limit, on_health_changeThe framework also subscribes to standard Nostr events for enhanced functionality (with before/after lifecycle):
before_profile_event, on_profile_event, after_profile_event (kind 0) - User profile metadatabefore_relay_list_event, on_relay_list_event, after_relay_list_event (kind 10002) - User relay preferencesbefore_nip51_list_event, on_nip51_list_event, after_nip51_list_event (kind 30000) - Trusted billboards, trusted marketplaces, blocked promotionsinterface 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
}
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.
The framework validates configuration at runtime:
private_key must be a Uint8Array (32 bytes) for NIP-42 authentication['wss://relay.attnprotocol.org']node_pubkeys is optional - if not provided, block events won’t be filtered by nodeValidation errors are thrown as exceptions with descriptive error messages.
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();
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";
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.
The framework is designed for Bitcoin-native operations:
["t", "<block_height>"] tagsbefore_block_event → on_block_event → after_block_event lifecycle hooksThe 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
});
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;
};
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:
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]);
MIT