NIP-98 URL Query Parameter Mismatch
SkillMonitoring & opsFix NIP-98 HTTP authentication 401 errors caused by URL mismatch between the `u` tag and what the server expects. Use when: (1) NIP-98 auth returns 401 with "URL mismatch" error, (2) Token creation succeeds but server rejects authentication, (3) Server logs show expected vs actual URL difference. CRITICAL: Server implementations vary - some strip query params, some don't. Check the actual error message to determine which behavior applies.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the NIP-98 URL Query Parameter Mismatch skill
What this skill tells your AI
The instructions your AI receives, as published by divinevideo/divine-mobile in .agents/skills/nip98-url-query-param-mismatch/SKILL.md and read by ahel’s review.
Problem
NIP-98 HTTP authentication fails with 401 Unauthorized even though the token is created
successfully and the signature is valid. The actual cause is a URL mismatch between
the u tag in the signed event and what the server expects.
Context / Trigger Conditions
- Server returns 401 Unauthorized for NIP-98 protected endpoints
- Logs show token creation succeeded (event signed and validated)
- Error response contains "URL mismatch" with expected vs actual URLs
- The URLs differ only by query parameters
Example error response:
{"error":"Auth failed: URL mismatch: expected .../notifications, got .../notifications?limit=50"}
Root Cause
The NIP-98 spec says:
The
utag MUST be exactly the same as the absolute request URL (including query parameters).
But server implementations vary:
- Some servers follow the spec strictly (require query params in
utag) - Some servers normalize URLs by stripping query params before validation
- You MUST match what your specific server does
Solution
Step 1: Add logging to see the actual error
} else if (response.statusCode == 401) {
Log.error(
'NIP-98 auth failed (401)\n'
'URL: $url\n'
'Response: ${response.body}', // <-- This reveals the actual issue
...
);
}
Step 2: Check the error message
- If error says "expected .../path?params, got .../path" → Include query params
- If error says "expected .../path, got .../path?params" → Strip query params
Step 3: Adjust URL normalization accordingly
For servers that STRIP query params (like Divine Relay):
final uri = Uri.parse(url);
// Server strips query params before NIP-98 validation
final normalizedUrl = '${uri.scheme}://${uri.host}${uri.path}';
For servers that REQUIRE query params (per NIP-98 spec):
final uri = Uri.parse(url);
final normalizedUrl = uri.hasQuery
? '${uri.scheme}://${uri.host}${uri.path}?${uri.query}'
: '${uri.scheme}://${uri.host}${uri.path}';
Verification
- After the fix, the URLs in the
utag should match what server expects - The 401 error should be replaced by successful authentication (200)
- Check logs to confirm URL matching
Example
Divine Relay behavior (strips query params):
- Request URL:
https://relay.dvines.org/api/users/abc/notifications?limit=50 - Server validates against:
https://relay.dvines.org/api/users/abc/notifications utag must be:https://relay.dvines.org/api/users/abc/notifications
Notes
- The NIP-98 spec is clear about including query params, but not all servers follow it
- Always check the actual error response to determine server behavior
- When in doubt, try both approaches and see which works
- Consider filing a bug with servers that don't follow the spec
References
- NIP-98 HTTP Auth Specification
- Spec quote: "The
utag MUST be exactly the same as the absolute request URL (including query parameters)" - Reality: Server implementations vary, always check actual behavior
Signals
- GitHub stars
- 265
- Forks
- 55
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
nip98-url-query-param-mismatch- Source
- github.com/divinevideo/divine-mobile