diff --git a/worker/nostr.js b/worker/nostr.js
index 775ba3e..d952d0e 100644
--- a/worker/nostr.js
+++ b/worker/nostr.js
@@ -1,4 +1,4 @@
-import Nostr from '@/lib/nostr'
+import Nostr, { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
 import { createHash } from 'crypto'
 import { parsePaymentRequest } from 'ln-service'
 import { assertValidBolt11, logInvalidBolt11 } from '@/lib/bolt11-validator'
@@ -67,7 +67,9 @@ export async function nip57 ({ data: { hash }, boss, lnd, models }) {
     const addressTag = note.tags.filter(t => t?.length >= 2 && t[0] === 'a')[0]
     const senderTag = typeof note.pubkey === 'string' ? ['P', note.pubkey] : null
     const kindTag = note.tags.filter(t => t?.length >= 2 && t[0] === 'k')[0]
-    const relays = note.tags.find(t => t?.length >= 2 && t[0] === 'relays').slice(1)
+    // NIP-57 only says the zap request "should" have a relays tag, so wallets may omit it
+    const requestedRelays = note.tags.find(t => t?.length >= 2 && t[0] === 'relays')?.slice(1) ?? []
+    const relays = requestedRelays.length > 0 ? requestedRelays : DEFAULT_CROSSPOSTING_RELAYS
 
     const tags = [recipientTag]
     if (eventTag) tags.push(eventTag)
diff --git a/worker/nostr.test.js b/worker/nostr.test.js
new file mode 100644
index 0000000..92ef8ac
--- /dev/null
+++ b/worker/nostr.test.js
@@ -0,0 +1,69 @@
+import { nip57 } from './nostr'
+import Nostr, { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
+
+jest.mock('@/lib/nostr', () => ({
+  __esModule: true,
+  default: { get: jest.fn() },
+  DEFAULT_CROSSPOSTING_RELAYS: ['wss://default.example/']
+}))
+jest.mock('@/lib/bolt11-validator', () => ({
+  assertValidBolt11: jest.fn(),
+  logInvalidBolt11: jest.fn()
+}))
+
+const rawRequest = JSON.stringify({ kind: 9734, content: '', tags: [] })
+jest.mock('ln-service', () => ({
+  parsePaymentRequest: jest.fn(() => ({
+    description_hash: require('crypto').createHash('sha256').update(JSON.stringify({ kind: 9734, content: '', tags: [] })).digest('hex')
+  }))
+}), { virtual: true })
+
+const PUBKEY = 'a'.repeat(64)
+
+function modelsFor (tags) {
+  return {
+    payInBolt11: {
+      findUnique: jest.fn(async () => ({
+        bolt11: 'lnbc1...',
+        preimage: 'b'.repeat(64),
+        confirmedAt: new Date('2026-10-01T00:00:00Z'),
+        nostrNote: { note: { pubkey: PUBKEY, tags }, rawRequest }
+      }))
+    },
+    externalTransaction: { findFirst: jest.fn() }
+  }
+}
+
+describe('nip57 zap receipts', () => {
+  let publish
+  let errorSpy
+
+  beforeEach(() => {
+    publish = jest.fn(async () => {})
+    Nostr.get.mockReturnValue({ getSigner: () => ({}), publish })
+    errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
+  })
+  afterEach(() => errorSpy.mockRestore())
+
+  const run = tags => nip57({ data: { hash: 'h' }, boss: {}, lnd: {}, models: modelsFor(tags) })
+
+  it('publishes to the relays of the zap request', async () => {
+    await run([['p', PUBKEY], ['relays', 'wss://one.example/', 'wss://two.example/']])
+    expect(errorSpy).not.toHaveBeenCalled()
+    expect(publish).toHaveBeenCalledTimes(1)
+    expect(publish.mock.calls[0][1].relays).toEqual(['wss://one.example/', 'wss://two.example/'])
+  })
+
+  it('still publishes the receipt when the zap request has no relays tag', async () => {
+    await run([['p', PUBKEY]])
+    expect(errorSpy).not.toHaveBeenCalled()
+    expect(publish).toHaveBeenCalledTimes(1)
+    expect(publish.mock.calls[0][1].relays).toEqual(DEFAULT_CROSSPOSTING_RELAYS)
+  })
+
+  it('ignores a malformed relays tag without relay URLs', async () => {
+    await run([['p', PUBKEY], ['relays']])
+    expect(errorSpy).not.toHaveBeenCalled()
+    expect(publish.mock.calls[0][1].relays).toEqual(DEFAULT_CROSSPOSTING_RELAYS)
+  })
+})
