diff --git a/prisma/migrations/20250106164328_zebedee_sender/migration.sql b/prisma/migrations/20250106164328_zebedee_sender/migration.sql
new file mode 100644
index 000000000..973f61eac
--- /dev/null
+++ b/prisma/migrations/20250106164328_zebedee_sender/migration.sql
@@ -0,0 +1,2 @@
+-- AlterEnum
+ALTER TYPE "WalletType" ADD VALUE 'ZEBEDEE';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index dcdb3b938..6ef4ed060 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -193,6 +193,7 @@ enum WalletType {
BLINK
LNC
WEBLN
+ ZEBEDEE
}
model Wallet {
diff --git a/public/wallets/zbd-dark.svg b/public/wallets/zbd-dark.svg
new file mode 100644
index 000000000..d2b03bd9a
--- /dev/null
+++ b/public/wallets/zbd-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/wallets/zbd.svg b/public/wallets/zbd.svg
new file mode 100644
index 000000000..b92c3ff5c
--- /dev/null
+++ b/public/wallets/zbd.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/wallets/client.js b/wallets/client.js
index 8bd44698f..e5e5c3287 100644
--- a/wallets/client.js
+++ b/wallets/client.js
@@ -7,5 +7,6 @@ import * as lnd from '@/wallets/lnd/client'
import * as webln from '@/wallets/webln/client'
import * as blink from '@/wallets/blink/client'
import * as phoenixd from '@/wallets/phoenixd/client'
+import * as zebedee from '@/wallets/zebedee/client'
-export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd]
+export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd, zebedee]
diff --git a/wallets/server.js b/wallets/server.js
index f14e9fb36..fd96aa0cb 100644
--- a/wallets/server.js
+++ b/wallets/server.js
@@ -10,6 +10,7 @@ import * as blink from '@/wallets/blink/server'
// we import only the metadata of client side wallets
import * as lnc from '@/wallets/lnc'
import * as webln from '@/wallets/webln'
+import * as zebedee from '@/wallets/zebedee'
import { walletLogger } from '@/api/resolvers/wallet'
import walletDefs from '@/wallets/server'
@@ -20,7 +21,7 @@ import { timeoutSignal, withTimeout } from '@/lib/time'
import { canReceive } from './common'
import wrapInvoice from './wrap'
-export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
+export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, zebedee]
const MAX_PENDING_INVOICES_PER_WALLET = 25
diff --git a/wallets/zebedee/client.js b/wallets/zebedee/client.js
new file mode 100644
index 000000000..d5375d72e
--- /dev/null
+++ b/wallets/zebedee/client.js
@@ -0,0 +1,73 @@
+import { API_URL } from '@/wallets/zebedee'
+import { assertContentTypeJson } from '@/lib/url'
+export * from '@/wallets/zebedee'
+
+export async function testSendPayment ({ apiKey }, { signal }) {
+ const wallet = await apiCall('wallet', { apiKey, method: 'GET' }, { signal })
+ if (!wallet.data) throw new Error('wallet not found')
+}
+
+export async function sendPayment (bolt11, { apiKey }, { signal }) {
+ const res = await apiCall('payments', { body: { invoice: bolt11 }, apiKey }, { signal })
+ if (!res?.data) throw new Error('payment failed')
+
+ const { id, preimage } = res.data
+ if (preimage) return preimage
+
+ // the api might return before the invoice is paid, so we'll wait for the preimage
+ return await waitForPreimage(id, { apiKey }, { signal })
+}
+
+async function waitForPreimage (id, { apiKey }, { signal }) {
+ while (!signal.aborted) {
+ const res = await apiCall('payments/{id}', { body: { id }, apiKey, method: 'GET' }, { signal })
+
+ // return preimage if it's available
+ const preimage = res?.data?.preimage
+ if (preimage) return preimage
+
+ // wait a before checking again
+ await new Promise(resolve => setTimeout(resolve, 30))
+ }
+ return null
+}
+
+export async function apiCall (api, { body, apiKey, method = 'POST' }, { signal }) {
+ // if get request, put params into the url
+ if (method === 'GET' && body) {
+ for (const [k, v] of Object.entries(body)) {
+ api = api.replace(`{${k}}`, v)
+ }
+ }
+
+ const res = await fetch(API_URL + api, {
+ method,
+ headers: {
+ apikey: apiKey,
+ 'Content-Type': 'application/json'
+ },
+ signal,
+ body: method === 'POST' ? JSON.stringify(body) : undefined
+ })
+
+ // Catch errors
+ // ref: https://zbd.dev/api-reference/errors
+ if (res.status < 200 || res.status > 299) {
+ // try to extract the error message from the response
+ let error
+ try {
+ assertContentTypeJson(res)
+ const json = await res.json()
+ if (json?.message) error = json.message
+ } catch (e) {
+ console.log('failed to parse error', e)
+ }
+
+ // throw the error, if we don't have one, we try to use the request status
+ if (!error) error = res.statusText || `error ${res.status}`
+ throw new Error(error)
+ }
+
+ assertContentTypeJson(res)
+ return await res.json()
+}
diff --git a/wallets/zebedee/index.js b/wallets/zebedee/index.js
new file mode 100644
index 000000000..15b497804
--- /dev/null
+++ b/wallets/zebedee/index.js
@@ -0,0 +1,26 @@
+import { string } from '@/lib/yup'
+
+export const DASHBOARD_URL = 'https://dashboard.zebedee.io/'
+export const API_URL = 'https://api.zebedee.io/v0/'
+
+export const name = 'zebedee'
+export const walletType = 'ZEBEDEE'
+export const walletField = 'walletZebedee'
+
+export const fields = [
+ {
+ name: 'apiKey',
+ label: 'api key',
+ type: 'password',
+ optional: 'for sending',
+ help: `you can get an API key from [Zebedee Dashboard](${DASHBOARD_URL}) from \n\`Project->API->Live\``,
+ clientOnly: true,
+ validate: string().min(8, 'invalid api key').max(64, 'api key is too long')
+ }
+]
+
+export const card = {
+ title: 'Zebedee',
+ subtitle: 'use [Zebedee](https://zebedee.io) for payments',
+ image: { src: '/wallets/zbd.svg' }
+}