AI Guardrails
AI Guardrails is a runtime validation system designed to catch common mistakes when using NDK, especially those made by LLMs (Large Language Models) generating code.
Overview
AI Guardrails provides:
- Educational error messages - Clear explanations of what went wrong and how to fix it
- Zero performance impact - Disabled by default, opt-in only
- Granular control - Enable all checks or selectively disable specific ones
- LLM-friendly - Designed to help AI-generated code self-correct
- Always visible - Both errors and warnings throw exceptions AND log to console.error (so AIs see them even if throws get swallowed)
Quick Start
import NDK from "@nostr-dev-kit/ndk";
// Enable all guardrails (recommended for development)
const ndk = new NDK({aiGuardrails: true});
// Or enable with exceptions
const ndk = new NDK({
aiGuardrails: {
skip: new Set(['filter-large-limit', 'fetch-events-usage'])
}
});
// Or programmatically control
ndk.aiGuardrails.skip('event-param-replaceable-no-dtag');
ndk.aiGuardrails.enable('filter-bech32-in-array');Available Guardrails
Filter-Related Checks
filter-bech32-in-array
Level: Error
Catches bech32-encoded values in filter arrays. Filters expect hex values, not bech32.
// ❌ WRONG
ndk.subscribe({
authors: ['npub1...'] // bech32 npub
});
// ✅ CORRECT
import {nip19} from 'nostr-tools';
const {data} = nip19.decode('npub1...');
ndk.subscribe({
authors: [data] // hex pubkey
});
// Or use filterFromId for complete bech32 entities
import {filterFromId} from '@nostr-dev-kit/ndk';
const filter = filterFromId('nevent1...');
ndk.subscribe(filter);filter-only-limit
Level: Error
Catches filters with only a limit parameter and no filtering criteria.
// ❌ WRONG - will fetch random events
ndk.subscribe({limit: 10});
// ✅ CORRECT
ndk.subscribe({
kinds: [1],
limit: 10
});filter-large-limit
Level: Warning
Warns about very large limit values that can cause performance issues.
// ⚠️ WARNING
ndk.subscribe({
kinds: [1],
limit: 10000 // Too large!
});
// ✅ BETTER
ndk.subscribe({
kinds: [1],
limit: 100 // More reasonable
});filter-empty
Level: Error
Catches completely empty filters.
// ❌ WRONG
ndk.subscribe({});
// ✅ CORRECT
ndk.subscribe({kinds: [1]});filter-since-after-until
Level: Error
Catches filters where since is after until, which would match zero events.
// ❌ WRONG
ndk.subscribe({
since: 1000000,
until: 500000
});
// ✅ CORRECT
ndk.subscribe({
since: 500000,
until: 1000000
});filter-invalid-a-tag
Level: Error
Catches malformed #a tag values. Must be kind:pubkey:d-tag format.
// ❌ WRONG
ndk.subscribe({
'#a': ['nevent1...'] // bech32 instead of address
});
// ✅ CORRECT
ndk.subscribe({
'#a': ['30023:fa984bd7...:my-article']
});fetchEvents Anti-Pattern
fetch-events-usage
Level: Warning
Warns about using fetchEvents() which is a blocking operation.
// ⚠️ SUBOPTIMAL - blocks until EOSE
const events = await ndk.fetchEvents({kinds: [1]});
// ✅ BETTER - reactive, non-blocking
ndk.subscribe({kinds: [1]}, {
onEvent: (event) => {
console.log('Got event:', event);
}
});
// Or for single events
const event = await ndk.fetchEvent({kinds: [1]});When to disable: If you truly need to block until all events arrive (rare).
ndk.aiGuardrails.skip('fetch-events-usage');
const events = await ndk.fetchEvents(filter);Event Construction Checks
event-missing-kind
Level: Error
Catches attempts to sign events without a kind.
// ❌ WRONG
const event = new NDKEvent(ndk);
event.content = "Hello";
await event.sign(); // Error!
// ✅ CORRECT
const event = new NDKEvent(ndk);
event.kind = 1; // Set kind first
event.content = "Hello";
await event.sign();event-content-is-object
Level: Error
Catches attempts to set event content to an object instead of a string.
// ❌ WRONG
const event = new NDKEvent(ndk);
event.kind = 30023;
event.content = {title: "My Article"}; // Object!
await event.sign(); // Error!
// ✅ CORRECT
const event = new NDKEvent(ndk);
event.kind = 30023;
event.content = JSON.stringify({title: "My Article"});
await event.sign();event-param-replaceable-no-dtag
Level: Warning
Warns about parameterized replaceable events (kinds 30000-39999) without a d-tag.
// ⚠️ WARNING - will use empty string as d-tag
const event = new NDKEvent(ndk);
event.kind = 30023;
event.content = "My article";
await event.sign(); // Warning!
// ✅ BETTER
const event = new NDKEvent(ndk);
event.kind = 30023;
event.dTag = "my-unique-article-id";
event.content = "My article";
await event.sign();event-created-at-milliseconds
Level: Error
Catches using milliseconds instead of seconds for created_at.
// ❌ WRONG
const event = new NDKEvent(ndk);
event.kind = 1;
event.content = "Hello";
event.created_at = Date.now(); // Milliseconds!
await event.sign(); // Error!
// ✅ CORRECT
const event = new NDKEvent(ndk);
event.kind = 1;
event.content = "Hello";
event.created_at = Math.floor(Date.now() / 1000); // Seconds
await event.sign();Tag Validation Checks
tag-invalid-p-tag
Level: Error
Catches invalid p-tags (must be 64-character hex pubkeys).
// ❌ WRONG
const event = new NDKEvent(ndk);
event.kind = 1;
event.tags.push(['p', 'npub1...']); // bech32!
await event.sign(); // Error!
// ✅ CORRECT
const event = new NDKEvent(ndk);
event.kind = 1;
event.tags.push(['p', ndkUser.pubkey]); // hex pubkey
await event.sign();tag-invalid-e-tag
Level: Error
Catches invalid e-tags (must be 64-character hex event IDs).
// ❌ WRONG
const event = new NDKEvent(ndk);
event.kind = 1;
event.tags.push(['e', 'note1...']); // bech32!
await event.sign(); // Error!
// ✅ CORRECT
const event = new NDKEvent(ndk);
event.kind = 1;
event.tags.push(['e', referencedEvent.id]); // hex event ID
await event.sign();event-manual-reply-markers
Level: Warning
Warns about manually adding e-tags with reply/root markers instead of using .reply().
// ⚠️ SUBOPTIMAL
const reply = new NDKEvent(ndk);
reply.kind = 1;
reply.content = "Great post!";
reply.tags.push(['e', parentEvent.id, '', 'reply']); // Manual marker
await reply.sign(); // Warning!
// ✅ BETTER - Use reply() method
const reply = new NDKEvent(ndk);
reply.kind = 1;
reply.content = "Great post!";
await reply.reply(parentEvent); // Handles threading automaticallyProgrammatic Control
Temporarily Disable a Check
// Disable for one-time use
ndk.aiGuardrails.skip('fetch-events-usage');
const events = await ndk.fetchEvents(filter);
// Re-enable it
ndk.aiGuardrails.enable('fetch-events-usage');Check What's Skipped
const skipped = ndk.aiGuardrails.getSkipped();
console.log('Skipped checks:', skipped);Runtime Enable/Disable
// Start with guardrails disabled
const ndk = new NDK();
// Enable later
ndk.aiGuardrails.setMode(true);
// Or enable with specific skips
ndk.aiGuardrails.setMode({
skip: new Set(['filter-large-limit'])
});
// Disable entirely
ndk.aiGuardrails.setMode(false);Error Messages
When a guardrail is triggered, it will:
- Log to console.error (visible even if the exception is caught)
- Throw an Error (stops execution)
Both "ERROR" and "WARNING" level checks throw exceptions - warnings are not just console warnings.
Fatal vs Non-Fatal Errors
Some errors are fatal - they represent fundamental mistakes that cannot be bypassed:
- Missing event kind
- Content as object instead of string
- Timestamps in milliseconds instead of seconds
- Invalid p-tag/e-tag formats
- Bech32 in filter arrays
- Empty filters or invalid time ranges
Fatal errors do NOT show the "To disable this check" message.
Example fatal error (no disable option):
🤖 AI_GUARDRAILS ERROR: Cannot sign event without 'kind'. Set event.kind before signing.
💡 Example: event.kind = 1; // for text noteExample non-fatal error (can be disabled):
🤖 AI_GUARDRAILS ERROR: Filter[0] contains only 'limit' without any filtering criteria.
💡 Add filtering criteria like 'kinds', 'authors', or '#e' tags.
🔇 To disable this check:
ndk.aiGuardrails.skip('filter-only-limit')
or set: ndk.aiGuardrails = { skip: new Set(['filter-only-limit']) }Best Practices
For Development
Enable all guardrails during development:
const ndk = new NDK({
aiGuardrails: true,
// ... other options
});For Production
Keep guardrails disabled in production for zero performance impact:
const ndk = new NDK({
aiGuardrails: process.env.NODE_ENV === 'development',
// ... other options
});For AI-Generated Code
If you're using AI to generate code, enable guardrails and let the AI learn from the errors:
// In your prompt/system message:
"When NDK throws an AI_GUARDRAILS error, read the error message carefully.
It
explains
what
's wrong and how to fix it. Update your code accordingly."The AI can also programmatically skip checks it knows are safe:
// LLM can disable specific checks when it knows what it's doing
ndk.aiGuardrails.skip('filter-large-limit'); // I know I need 5000 events
ndk.subscribe({kinds: [1], limit: 5000});Complete Check ID Reference
Import these for type-safe check IDs:
import {GuardrailCheckId} from '@nostr-dev-kit/ndk';
// All available check IDs:
GuardrailCheckId.FILTER_BECH32_IN_ARRAY
GuardrailCheckId.FILTER_ONLY_LIMIT
GuardrailCheckId.FILTER_LARGE_LIMIT
GuardrailCheckId.FILTER_EMPTY
GuardrailCheckId.FILTER_SINCE_AFTER_UNTIL
GuardrailCheckId.FILTER_INVALID_A_TAG
GuardrailCheckId.FETCH_EVENTS_USAGE
GuardrailCheckId.EVENT_MISSING_KIND
GuardrailCheckId.EVENT_PARAM_REPLACEABLE_NO_DTAG
GuardrailCheckId.EVENT_CREATED_AT_MILLISECONDS
GuardrailCheckId.EVENT_NO_NDK_INSTANCE
GuardrailCheckId.EVENT_CONTENT_IS_OBJECT
GuardrailCheckId.EVENT_MODIFIED_AFTER_SIGNING
GuardrailCheckId.EVENT_MANUAL_REPLY_MARKERS
GuardrailCheckId.TAG_E_FOR_PARAM_REPLACEABLE
GuardrailCheckId.TAG_BECH32_VALUE
GuardrailCheckId.TAG_DUPLICATE
GuardrailCheckId.TAG_INVALID_P_TAG
GuardrailCheckId.TAG_INVALID_E_TAG
GuardrailCheckId.SUBSCRIBE_NOT_STARTED
GuardrailCheckId.SUBSCRIBE_CLOSE_ON_EOSE_NO_HANDLER
GuardrailCheckId.SUBSCRIBE_PASSED_EVENT_NOT_FILTER
GuardrailCheckId.SUBSCRIBE_AWAITED
GuardrailCheckId.RELAY_INVALID_URL
GuardrailCheckId.RELAY_HTTP_INSTEAD_OF_WS
GuardrailCheckId.RELAY_NO_ERROR_HANDLERS
GuardrailCheckId.VALIDATION_PUBKEY_IS_NPUB
GuardrailCheckId.VALIDATION_PUBKEY_WRONG_LENGTH
GuardrailCheckId.VALIDATION_EVENT_ID_IS_BECH32
GuardrailCheckId.VALIDATION_EVENT_ID_WRONG_LENGTHPhilosophy
AI Guardrails is designed with these principles:
- Educational, not punitive - Error messages teach, don't just reject
- Opt-in, not opt-out - Zero impact when disabled (default)
- Flexible - Granular control over what's checked
- LLM-friendly - Help AI code self-correct and learn patterns
Contributing
To add a new guardrail:
- Add the check ID to
GuardrailCheckIdinai-guardrails.ts - Implement the check where appropriate (filter validation, event signing, etc.)
- Use
ndk.aiGuardrails.error()orndk.aiGuardrails.warn() - Add clear, actionable error messages with hints
- Document it in this file
- Add tests
Example:
if (ndk?.aiGuardrails.isEnabled()) {
ndk.aiGuardrails.error(
GuardrailCheckId.YOUR_NEW_CHECK,
"Clear explanation of what's wrong",
"Helpful hint on how to fix it"
);
}