diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 03462e3b..24b5bbf7 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -3,7 +3,7 @@ import { join, resolve } from 'path'
 import { GraphQLError } from 'graphql'
 import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
 import { msatsToSats } from '../../lib/format'
-import { bioSchema, emailSchema, lnAddrAutowithdrawSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
+import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
 import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item'
 import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID } from '../../lib/constants'
 import { viewGroup } from './growth'
@@ -426,36 +426,6 @@ export default {
         throw error
       }
     },
-    setAutoWithdraw: async (parent, data, { me, models }) => {
-      if (!me) {
-        throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
-      }
-
-      await ssValidate(lnAddrAutowithdrawSchema, data, { me, models })
-
-      await models.user.update({
-        where: { id: me.id },
-        data
-      })
-
-      return true
-    },
-    removeAutoWithdraw: async (parent, data, { me, models }) => {
-      if (!me) {
-        throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
-      }
-
-      await models.user.update({
-        where: { id: me.id },
-        data: {
-          lnAddr: null,
-          autoWithdrawThreshold: null,
-          autoWithdrawMaxFeePercent: null
-        }
-      })
-
-      return true
-    },
     setSettings: async (parent, { settings: { nostrRelays, ...data } }, { me, models }) => {
       if (!me) {
         throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index 7f381387..c2f79bc6 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -1,4 +1,4 @@
-import { getIdentity, createHodlInvoice, createInvoice, decodePaymentRequest, payViaPaymentRequest, cancelHodlInvoice, getInvoice as getInvoiceFromLnd, getNode } from 'ln-service'
+import { getIdentity, createHodlInvoice, createInvoice, decodePaymentRequest, payViaPaymentRequest, cancelHodlInvoice, getInvoice as getInvoiceFromLnd, getNode, authenticatedLndGrpc } from 'ln-service'
 import { GraphQLError } from 'graphql'
 import crypto from 'crypto'
 import serialize from './serial'
@@ -7,10 +7,11 @@ import lnpr from 'bolt11'
 import { SELECT } from './item'
 import { lnAddrOptions } from '../../lib/lnurl'
 import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
-import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
+import { LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
 import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '../../lib/constants'
 import { datePivot } from '../../lib/time'
 import assertGofacYourself from './ofac'
+import { HEX_REGEX } from '../../lib/macaroon'
 
 export async function getInvoice (parent, { id }, { me, models, lnd }) {
   const inv = await models.invoice.findUnique({
@@ -61,6 +62,42 @@ export function createHmac (hash) {
 export default {
   Query: {
     invoice: getInvoice,
+    wallet: async (parent, { id }, { me, models }) => {
+      if (!me) {
+        throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
+      }
+
+      return await models.wallet.findUnique({
+        where: {
+          userId: me.id,
+          id: Number(id)
+        }
+      })
+    },
+    walletByType: async (parent, { type }, { me, models }) => {
+      if (!me) {
+        throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
+      }
+
+      const wallet = await models.wallet.findFirst({
+        where: {
+          userId: me.id,
+          type
+        }
+      })
+      return wallet
+    },
+    wallets: async (parent, args, { me, models }) => {
+      if (!me) {
+        throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
+      }
+
+      return await models.wallet.findMany({
+        where: {
+          userId: me.id
+        }
+      })
+    },
     withdrawl: async (parent, { id }, { me, models, lnd }) => {
       if (!me) {
         throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
@@ -266,7 +303,11 @@ export default {
       }
     }
   },
-
+  WalletDetails: {
+    __resolveType (wallet) {
+      return wallet.address ? 'WalletLNAddr' : 'WalletLND'
+    }
+  },
   Mutation: {
     createInvoice: async (parent, { amount, hodlInvoice = false, expireSecs = 3600 }, { me, models, lnd, headers }) => {
       await ssValidate(amountSchema, { amount })
@@ -295,8 +336,6 @@ export default {
           expires_at: expiresAt
         })
 
-        console.log('invoice', balanceLimit)
-
         const [inv] = await serialize(models,
           models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${hodlInvoice ? invoice.secret : null}::TEXT, ${invoice.request},
             ${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL, NULL,
@@ -348,6 +387,52 @@ export default {
         data: { bolt11: null, hash: null }
       })
       return { id }
+    },
+    upsertWalletLND: async (parent, { settings, ...data }, { me, models }) => {
+      // store hex inputs as base64
+      if (HEX_REGEX.test(data.macaroon)) {
+        data.macaroon = Buffer.from(data.macaroon, 'hex').toString('base64')
+      }
+      if (HEX_REGEX.test(data.cert)) {
+        data.cert = Buffer.from(data.cert, 'hex').toString('base64')
+      }
+
+      return await upsertWallet(
+        {
+          schema: LNDAutowithdrawSchema,
+          walletName: 'walletLND',
+          walletType: 'LND',
+          testConnect: async ({ cert, macaroon, socket }) => {
+            const { lnd } = await authenticatedLndGrpc({
+              cert,
+              macaroon,
+              socket
+            })
+            return await getIdentity({ lnd })
+          }
+        },
+        { settings, data }, { me, models })
+    },
+    upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
+      return await upsertWallet(
+        {
+          schema: lnAddrAutowithdrawSchema,
+          walletName: 'walletLightningAddress',
+          walletType: 'LIGHTNING_ADDRESS',
+          testConnect: async ({ address }) => {
+            return await lnAddrOptions(address)
+          }
+        },
+        { settings, data }, { me, models })
+    },
+    removeWallet: async (parent, { id }, { me, models }) => {
+      if (!me) {
+        throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
+      }
+
+      await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
+
+      return true
     }
   },
 
@@ -379,6 +464,80 @@ export default {
   }
 }
 
+async function upsertWallet (
+  { schema, walletName, walletType, testConnect }, { settings, data }, { me, models }) {
+  if (!me) {
+    throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
+  }
+
+  await ssValidate(schema, { ...data, ...settings }, { me, models })
+
+  if (testConnect) {
+    try {
+      await testConnect(data)
+    } catch (error) {
+      console.error(error)
+      throw new GraphQLError('failed to connect to wallet', { extensions: { code: 'BAD_INPUT' } })
+    }
+  }
+
+  const { id, ...walletData } = data
+  const { autoWithdrawThreshold, autoWithdrawMaxFeePercent, priority } = settings
+
+  const txs = [
+    models.user.update({
+      where: { id: me.id },
+      data: {
+        autoWithdrawMaxFeePercent,
+        autoWithdrawThreshold
+      }
+    })
+  ]
+
+  if (priority) {
+    txs.push(
+      models.wallet.updateMany({
+        where: {
+          userId: me.id
+        },
+        data: {
+          priority: 0
+        }
+      }))
+  }
+
+  if (id) {
+    txs.push(
+      models.wallet.update({
+        where: { id: Number(id), userId: me.id },
+        data: {
+          priority: priority ? 1 : 0,
+          [walletName]: {
+            update: {
+              where: { walletId: Number(id) },
+              data: walletData
+            }
+          }
+        }
+      }))
+  } else {
+    txs.push(
+      models.wallet.create({
+        data: {
+          priority: Number(priority),
+          userId: me.id,
+          type: walletType,
+          [walletName]: {
+            create: walletData
+          }
+        }
+      }))
+  }
+
+  await models.$transaction(txs)
+  return true
+}
+
 export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, autoWithdraw = false }) {
   await ssValidate(withdrawlSchema, { invoice, maxFee })
   await assertGofacYourself({ models, headers })
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 61b826e5..6c41e9d7 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -31,8 +31,6 @@ export default gql`
     subscribeUserPosts(id: ID): User
     subscribeUserComments(id: ID): User
     toggleMute(id: ID): User
-    setAutoWithdraw(lnAddr: String!, autoWithdrawThreshold: Int!, autoWithdrawMaxFeePercent: Float!): Boolean
-    removeAutoWithdraw: Boolean
   }
 
   type User {
diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js
index d170060d..c2cca6ae 100644
--- a/api/typeDefs/wallet.js
+++ b/api/typeDefs/wallet.js
@@ -7,6 +7,9 @@ export default gql`
     numBolt11s: Int!
     connectAddress: String!
     walletHistory(cursor: String, inc: String): History
+    wallets: [Wallet!]!
+    wallet(id: ID!): Wallet
+    walletByType(type: String!): Wallet
   }
 
   extend type Mutation {
@@ -15,6 +18,35 @@ export default gql`
     sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl!
     cancelInvoice(hash: String!, hmac: String!): Invoice!
     dropBolt11(id: ID): Withdrawl
+    upsertWalletLND(id: ID, socket: String!, macaroon: String!, cert: String, settings: AutowithdrawSettings!): Boolean
+    upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
+    removeWallet(id: ID!): Boolean
+  }
+
+  type Wallet {
+    id: ID!
+    createdAt: Date!
+    type: String!
+    priority: Boolean!
+    wallet: WalletDetails!
+  }
+
+  type WalletLNAddr {
+    address: String!
+  }
+
+  type WalletLND {
+    socket: String!
+    macaroon: String!
+    cert: String
+  }
+
+  union WalletDetails = WalletLNAddr | WalletLND
+
+  input AutowithdrawSettings {
+    autoWithdrawThreshold: Int!
+    autoWithdrawMaxFeePercent: Float!
+    priority: Boolean!
   }
 
   type Invoice {
diff --git a/components/autowithdraw-shared.js b/components/autowithdraw-shared.js
new file mode 100644
index 00000000..b826f829
--- /dev/null
+++ b/components/autowithdraw-shared.js
@@ -0,0 +1,62 @@
+import { InputGroup } from 'react-bootstrap'
+import { Checkbox, Input } from './form'
+import { useMe } from './me'
+import { useEffect, useState } from 'react'
+import { isNumber } from 'mathjs'
+import { numWithUnits } from '../lib/format'
+
+function autoWithdrawThreshold ({ me }) {
+  return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
+}
+
+export function autowithdrawInitial ({ me, priority = false }) {
+  return {
+    priority,
+    autoWithdrawThreshold: autoWithdrawThreshold({ me }),
+    autoWithdrawMaxFeePercent: isNumber(me?.privates?.autoWithdrawMaxFeePercent) ? me?.privates?.autoWithdrawMaxFeePercent : 1
+  }
+}
+
+export function AutowithdrawSettings ({ priority }) {
+  const me = useMe()
+  const threshold = autoWithdrawThreshold({ me })
+
+  const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
+
+  useEffect(() => {
+    setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
+  }, [autoWithdrawThreshold])
+
+  return (
+    <>
+      
+      
+    >
+
+  )
+}
diff --git a/fragments/users.js b/fragments/users.js
index 7a1c8d47..2d12673b 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -111,17 +111,10 @@ mutation setSettings($settings: SettingsInput!) {
 }
 `
 
-export const SET_AUTOWITHDRAW =
+export const DELETE_WALLET =
 gql`
-mutation setAutoWithdraw($lnAddr: String!, $autoWithdrawThreshold: Int!, $autoWithdrawMaxFeePercent: Float!) {
-  setAutoWithdraw(lnAddr: $lnAddr, autoWithdrawThreshold: $autoWithdrawThreshold, autoWithdrawMaxFeePercent: $autoWithdrawMaxFeePercent)
-}
-`
-
-export const REMOVE_AUTOWITHDRAW =
-gql`
-mutation removeAutoWithdraw {
-  removeAutoWithdraw
+mutation removeWallet {
+  removeWallet
 }
 `
 
diff --git a/fragments/wallet.js b/fragments/wallet.js
index d530cc9c..f1a3c09e 100644
--- a/fragments/wallet.js
+++ b/fragments/wallet.js
@@ -74,3 +74,78 @@ export const SEND_TO_LNADDR = gql`
       id
     }
 }`
+
+export const UPSERT_WALLET_LNADDR =
+gql`
+mutation upsertWalletLNAddr($id: ID, $address: String!, $settings: AutowithdrawSettings!) {
+  upsertWalletLNAddr(id: $id, address: $address, settings: $settings)
+}
+`
+
+export const UPSERT_WALLET_LND =
+gql`
+mutation upsertWalletLND($id: ID, $socket: String!, $macaroon: String!, $cert: String, $settings: AutowithdrawSettings!) {
+  upsertWalletLND(id: $id, socket: $socket, macaroon: $macaroon, cert: $cert, settings: $settings)
+}
+`
+
+export const REMOVE_WALLET =
+gql`
+mutation removeWallet($id: ID!) {
+  removeWallet(id: $id)
+}
+`
+
+export const WALLET = gql`
+  query Wallet($id: ID!) {
+    wallet(id: $id) {
+      id
+      createdAt
+      priority
+      type
+      wallet {
+        __typename
+        ... on WalletLNAddr {
+          address
+        }
+        ... on WalletLND {
+          socket
+          macaroon
+          cert
+        }
+      }
+    }
+  }
+`
+
+export const WALLET_BY_TYPE = gql`
+  query WalletByType($type: String!) {
+    walletByType(type: $type) {
+      id
+      createdAt
+      priority
+      type
+      wallet {
+        __typename
+        ... on WalletLNAddr {
+          address
+        }
+        ... on WalletLND {
+          socket
+          macaroon
+          cert
+        }
+      }
+    }
+  }
+`
+
+export const WALLETS = gql`
+  query Wallets {
+    wallets {
+      id
+      priority
+      type
+    }
+  }
+`
diff --git a/lib/macaroon-id.js b/lib/macaroon-id.js
new file mode 100644
index 00000000..266dbe8f
--- /dev/null
+++ b/lib/macaroon-id.js
@@ -0,0 +1,546 @@
+// GENERATED pbjs -t static-module -w es6 lib/macaroon-id.proto -l eslint-disable -o lib/macaroon-id.js
+/*eslint-disable*/
+// REPLACED AFTER GENERATION because tsx does not like the * import
+// import * as $protobuf from "protobufjs/minimal";
+import $protobuf from "protobufjs/minimal";
+
+// Common aliases
+const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
+
+// Exported root namespace
+const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});
+
+export const MacaroonId = $root.MacaroonId = (() => {
+
+    /**
+     * Properties of a MacaroonId.
+     * @exports IMacaroonId
+     * @interface IMacaroonId
+     * @property {Uint8Array|null} [nonce] MacaroonId nonce
+     * @property {Uint8Array|null} [storageId] MacaroonId storageId
+     * @property {Array.|null} [ops] MacaroonId ops
+     */
+
+    /**
+     * Constructs a new MacaroonId.
+     * @exports MacaroonId
+     * @classdesc Represents a MacaroonId.
+     * @implements IMacaroonId
+     * @constructor
+     * @param {IMacaroonId=} [properties] Properties to set
+     */
+    function MacaroonId(properties) {
+        this.ops = [];
+        if (properties)
+            for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * MacaroonId nonce.
+     * @member {Uint8Array} nonce
+     * @memberof MacaroonId
+     * @instance
+     */
+    MacaroonId.prototype.nonce = $util.newBuffer([]);
+
+    /**
+     * MacaroonId storageId.
+     * @member {Uint8Array} storageId
+     * @memberof MacaroonId
+     * @instance
+     */
+    MacaroonId.prototype.storageId = $util.newBuffer([]);
+
+    /**
+     * MacaroonId ops.
+     * @member {Array.} ops
+     * @memberof MacaroonId
+     * @instance
+     */
+    MacaroonId.prototype.ops = $util.emptyArray;
+
+    /**
+     * Creates a new MacaroonId instance using the specified properties.
+     * @function create
+     * @memberof MacaroonId
+     * @static
+     * @param {IMacaroonId=} [properties] Properties to set
+     * @returns {MacaroonId} MacaroonId instance
+     */
+    MacaroonId.create = function create(properties) {
+        return new MacaroonId(properties);
+    };
+
+    /**
+     * Encodes the specified MacaroonId message. Does not implicitly {@link MacaroonId.verify|verify} messages.
+     * @function encode
+     * @memberof MacaroonId
+     * @static
+     * @param {IMacaroonId} message MacaroonId message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    MacaroonId.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        if (message.nonce != null && Object.hasOwnProperty.call(message, "nonce"))
+            writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.nonce);
+        if (message.storageId != null && Object.hasOwnProperty.call(message, "storageId"))
+            writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.storageId);
+        if (message.ops != null && message.ops.length)
+            for (let i = 0; i < message.ops.length; ++i)
+                $root.Op.encode(message.ops[i], writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim();
+        return writer;
+    };
+
+    /**
+     * Encodes the specified MacaroonId message, length delimited. Does not implicitly {@link MacaroonId.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof MacaroonId
+     * @static
+     * @param {IMacaroonId} message MacaroonId message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    MacaroonId.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes a MacaroonId message from the specified reader or buffer.
+     * @function decode
+     * @memberof MacaroonId
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {MacaroonId} MacaroonId
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    MacaroonId.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        let end = length === undefined ? reader.len : reader.pos + length, message = new $root.MacaroonId();
+        while (reader.pos < end) {
+            let tag = reader.uint32();
+            switch (tag >>> 3) {
+            case 1: {
+                    message.nonce = reader.bytes();
+                    break;
+                }
+            case 2: {
+                    message.storageId = reader.bytes();
+                    break;
+                }
+            case 3: {
+                    if (!(message.ops && message.ops.length))
+                        message.ops = [];
+                    message.ops.push($root.Op.decode(reader, reader.uint32()));
+                    break;
+                }
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes a MacaroonId message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof MacaroonId
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {MacaroonId} MacaroonId
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    MacaroonId.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies a MacaroonId message.
+     * @function verify
+     * @memberof MacaroonId
+     * @static
+     * @param {Object.} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    MacaroonId.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        if (message.nonce != null && message.hasOwnProperty("nonce"))
+            if (!(message.nonce && typeof message.nonce.length === "number" || $util.isString(message.nonce)))
+                return "nonce: buffer expected";
+        if (message.storageId != null && message.hasOwnProperty("storageId"))
+            if (!(message.storageId && typeof message.storageId.length === "number" || $util.isString(message.storageId)))
+                return "storageId: buffer expected";
+        if (message.ops != null && message.hasOwnProperty("ops")) {
+            if (!Array.isArray(message.ops))
+                return "ops: array expected";
+            for (let i = 0; i < message.ops.length; ++i) {
+                let error = $root.Op.verify(message.ops[i]);
+                if (error)
+                    return "ops." + error;
+            }
+        }
+        return null;
+    };
+
+    /**
+     * Creates a MacaroonId message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof MacaroonId
+     * @static
+     * @param {Object.} object Plain object
+     * @returns {MacaroonId} MacaroonId
+     */
+    MacaroonId.fromObject = function fromObject(object) {
+        if (object instanceof $root.MacaroonId)
+            return object;
+        let message = new $root.MacaroonId();
+        if (object.nonce != null)
+            if (typeof object.nonce === "string")
+                $util.base64.decode(object.nonce, message.nonce = $util.newBuffer($util.base64.length(object.nonce)), 0);
+            else if (object.nonce.length >= 0)
+                message.nonce = object.nonce;
+        if (object.storageId != null)
+            if (typeof object.storageId === "string")
+                $util.base64.decode(object.storageId, message.storageId = $util.newBuffer($util.base64.length(object.storageId)), 0);
+            else if (object.storageId.length >= 0)
+                message.storageId = object.storageId;
+        if (object.ops) {
+            if (!Array.isArray(object.ops))
+                throw TypeError(".MacaroonId.ops: array expected");
+            message.ops = [];
+            for (let i = 0; i < object.ops.length; ++i) {
+                if (typeof object.ops[i] !== "object")
+                    throw TypeError(".MacaroonId.ops: object expected");
+                message.ops[i] = $root.Op.fromObject(object.ops[i]);
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Creates a plain object from a MacaroonId message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof MacaroonId
+     * @static
+     * @param {MacaroonId} message MacaroonId
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.} Plain object
+     */
+    MacaroonId.toObject = function toObject(message, options) {
+        if (!options)
+            options = {};
+        let object = {};
+        if (options.arrays || options.defaults)
+            object.ops = [];
+        if (options.defaults) {
+            if (options.bytes === String)
+                object.nonce = "";
+            else {
+                object.nonce = [];
+                if (options.bytes !== Array)
+                    object.nonce = $util.newBuffer(object.nonce);
+            }
+            if (options.bytes === String)
+                object.storageId = "";
+            else {
+                object.storageId = [];
+                if (options.bytes !== Array)
+                    object.storageId = $util.newBuffer(object.storageId);
+            }
+        }
+        if (message.nonce != null && message.hasOwnProperty("nonce"))
+            object.nonce = options.bytes === String ? $util.base64.encode(message.nonce, 0, message.nonce.length) : options.bytes === Array ? Array.prototype.slice.call(message.nonce) : message.nonce;
+        if (message.storageId != null && message.hasOwnProperty("storageId"))
+            object.storageId = options.bytes === String ? $util.base64.encode(message.storageId, 0, message.storageId.length) : options.bytes === Array ? Array.prototype.slice.call(message.storageId) : message.storageId;
+        if (message.ops && message.ops.length) {
+            object.ops = [];
+            for (let j = 0; j < message.ops.length; ++j)
+                object.ops[j] = $root.Op.toObject(message.ops[j], options);
+        }
+        return object;
+    };
+
+    /**
+     * Converts this MacaroonId to JSON.
+     * @function toJSON
+     * @memberof MacaroonId
+     * @instance
+     * @returns {Object.} JSON object
+     */
+    MacaroonId.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    /**
+     * Gets the default type url for MacaroonId
+     * @function getTypeUrl
+     * @memberof MacaroonId
+     * @static
+     * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
+     * @returns {string} The default type url
+     */
+    MacaroonId.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
+        if (typeUrlPrefix === undefined) {
+            typeUrlPrefix = "type.googleapis.com";
+        }
+        return typeUrlPrefix + "/MacaroonId";
+    };
+
+    return MacaroonId;
+})();
+
+export const Op = $root.Op = (() => {
+
+    /**
+     * Properties of an Op.
+     * @exports IOp
+     * @interface IOp
+     * @property {string|null} [entity] Op entity
+     * @property {Array.|null} [actions] Op actions
+     */
+
+    /**
+     * Constructs a new Op.
+     * @exports Op
+     * @classdesc Represents an Op.
+     * @implements IOp
+     * @constructor
+     * @param {IOp=} [properties] Properties to set
+     */
+    function Op(properties) {
+        this.actions = [];
+        if (properties)
+            for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * Op entity.
+     * @member {string} entity
+     * @memberof Op
+     * @instance
+     */
+    Op.prototype.entity = "";
+
+    /**
+     * Op actions.
+     * @member {Array.} actions
+     * @memberof Op
+     * @instance
+     */
+    Op.prototype.actions = $util.emptyArray;
+
+    /**
+     * Creates a new Op instance using the specified properties.
+     * @function create
+     * @memberof Op
+     * @static
+     * @param {IOp=} [properties] Properties to set
+     * @returns {Op} Op instance
+     */
+    Op.create = function create(properties) {
+        return new Op(properties);
+    };
+
+    /**
+     * Encodes the specified Op message. Does not implicitly {@link Op.verify|verify} messages.
+     * @function encode
+     * @memberof Op
+     * @static
+     * @param {IOp} message Op message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    Op.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        if (message.entity != null && Object.hasOwnProperty.call(message, "entity"))
+            writer.uint32(/* id 1, wireType 2 =*/10).string(message.entity);
+        if (message.actions != null && message.actions.length)
+            for (let i = 0; i < message.actions.length; ++i)
+                writer.uint32(/* id 2, wireType 2 =*/18).string(message.actions[i]);
+        return writer;
+    };
+
+    /**
+     * Encodes the specified Op message, length delimited. Does not implicitly {@link Op.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof Op
+     * @static
+     * @param {IOp} message Op message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    Op.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes an Op message from the specified reader or buffer.
+     * @function decode
+     * @memberof Op
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {Op} Op
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    Op.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        let end = length === undefined ? reader.len : reader.pos + length, message = new $root.Op();
+        while (reader.pos < end) {
+            let tag = reader.uint32();
+            switch (tag >>> 3) {
+            case 1: {
+                    message.entity = reader.string();
+                    break;
+                }
+            case 2: {
+                    if (!(message.actions && message.actions.length))
+                        message.actions = [];
+                    message.actions.push(reader.string());
+                    break;
+                }
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes an Op message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof Op
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {Op} Op
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    Op.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies an Op message.
+     * @function verify
+     * @memberof Op
+     * @static
+     * @param {Object.} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    Op.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        if (message.entity != null && message.hasOwnProperty("entity"))
+            if (!$util.isString(message.entity))
+                return "entity: string expected";
+        if (message.actions != null && message.hasOwnProperty("actions")) {
+            if (!Array.isArray(message.actions))
+                return "actions: array expected";
+            for (let i = 0; i < message.actions.length; ++i)
+                if (!$util.isString(message.actions[i]))
+                    return "actions: string[] expected";
+        }
+        return null;
+    };
+
+    /**
+     * Creates an Op message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof Op
+     * @static
+     * @param {Object.} object Plain object
+     * @returns {Op} Op
+     */
+    Op.fromObject = function fromObject(object) {
+        if (object instanceof $root.Op)
+            return object;
+        let message = new $root.Op();
+        if (object.entity != null)
+            message.entity = String(object.entity);
+        if (object.actions) {
+            if (!Array.isArray(object.actions))
+                throw TypeError(".Op.actions: array expected");
+            message.actions = [];
+            for (let i = 0; i < object.actions.length; ++i)
+                message.actions[i] = String(object.actions[i]);
+        }
+        return message;
+    };
+
+    /**
+     * Creates a plain object from an Op message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof Op
+     * @static
+     * @param {Op} message Op
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.} Plain object
+     */
+    Op.toObject = function toObject(message, options) {
+        if (!options)
+            options = {};
+        let object = {};
+        if (options.arrays || options.defaults)
+            object.actions = [];
+        if (options.defaults)
+            object.entity = "";
+        if (message.entity != null && message.hasOwnProperty("entity"))
+            object.entity = message.entity;
+        if (message.actions && message.actions.length) {
+            object.actions = [];
+            for (let j = 0; j < message.actions.length; ++j)
+                object.actions[j] = message.actions[j];
+        }
+        return object;
+    };
+
+    /**
+     * Converts this Op to JSON.
+     * @function toJSON
+     * @memberof Op
+     * @instance
+     * @returns {Object.} JSON object
+     */
+    Op.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    /**
+     * Gets the default type url for Op
+     * @function getTypeUrl
+     * @memberof Op
+     * @static
+     * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
+     * @returns {string} The default type url
+     */
+    Op.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
+        if (typeUrlPrefix === undefined) {
+            typeUrlPrefix = "type.googleapis.com";
+        }
+        return typeUrlPrefix + "/Op";
+    };
+
+    return Op;
+})();
+
+export { $root as default };
diff --git a/lib/macaroon-id.proto b/lib/macaroon-id.proto
new file mode 100644
index 00000000..764c0744
--- /dev/null
+++ b/lib/macaroon-id.proto
@@ -0,0 +1,14 @@
+syntax="proto3";
+
+option go_package = "macaroonpb";
+
+message MacaroonId {
+	bytes nonce = 1;
+	bytes storageId = 2;
+	repeated Op ops = 3;
+}
+
+message Op {
+	string entity = 1;
+	repeated string actions = 2;
+}
\ No newline at end of file
diff --git a/lib/macaroon.js b/lib/macaroon.js
new file mode 100644
index 00000000..80bab88a
--- /dev/null
+++ b/lib/macaroon.js
@@ -0,0 +1,206 @@
+import { importMacaroon, base64ToBytes } from 'macaroon'
+import { MacaroonId } from './macaroon-id'
+import isEqual from 'lodash/isEqual'
+import isEqualWith from 'lodash/isEqualWith'
+
+export const B64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
+export const HEX_REGEX = /^[0-9a-fA-F]+$/
+
+function decodeMacaroon (macaroon) {
+  if (HEX_REGEX.test(macaroon)) {
+    return importMacaroon(Buffer.from(macaroon, 'hex'))
+  }
+
+  if (B64_REGEX.test(macaroon)) {
+    return importMacaroon(Buffer.from(macaroon, 'base64'))
+  }
+
+  throw new Error('invalid macaroon encoding')
+}
+
+function macaroonOPs (macaroon) {
+  try {
+    const m = decodeMacaroon(macaroon)
+    const macJson = m.exportJSON()
+
+    if (macJson.i64) {
+      const identBytes = Buffer.from(base64ToBytes(macJson.i64))
+      if (identBytes[0] === 0x03) {
+        const id = MacaroonId.decode(identBytes.slice(1))
+        return id.toJSON().ops
+      }
+    }
+  } catch (e) {
+    console.error('macaroonOPs error:', e)
+  }
+
+  return []
+}
+
+function arrayCustomizer (value1, value2) {
+  if (Array.isArray(value1) && Array.isArray(value2)) {
+    value1.sort()
+    value2.sort()
+    return value1.length === value2.length &&
+     (isEqual(value1, value2) || value1.every((v, i) => isEqualWith(v, value2[i], arrayCustomizer)))
+  }
+}
+
+export function isInvoiceMacaroon (macaroon) {
+  return isEqualWith(macaroonOPs(macaroon), INVOICE_MACAROON_OPS, arrayCustomizer)
+}
+
+export function isAdminMacaroon (macaroon) {
+  return isEqualWith(macaroonOPs(macaroon), ADMIN_MACAROON_OPS, arrayCustomizer)
+}
+
+export function isReadOnlyMacaroon (macaroon) {
+  return isEqualWith(macaroonOPs(macaroon), READ_ONLY_MACAROON_OPS, arrayCustomizer)
+}
+
+const INVOICE_MACAROON_OPS = [
+  {
+    entity: 'address',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'invoices',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'onchain',
+    actions: [
+      'read'
+    ]
+  }
+]
+
+const ADMIN_MACAROON_OPS = [
+  {
+    entity: 'address',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'info',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'invoices',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'macaroon',
+    actions: [
+      'generate',
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'message',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'offchain',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'onchain',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'peers',
+    actions: [
+      'read',
+      'write'
+    ]
+  },
+  {
+    entity: 'signer',
+    actions: [
+      'generate',
+      'read'
+    ]
+  }
+]
+
+const READ_ONLY_MACAROON_OPS = [
+  {
+    entity: 'address',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'info',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'invoices',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'macaroon',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'message',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'offchain',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'onchain',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'peers',
+    actions: [
+      'read'
+    ]
+  },
+  {
+    entity: 'signer',
+    actions: [
+      'read'
+    ]
+  }
+]
diff --git a/lib/validate.js b/lib/validate.js
index 4abc9e9d..5131a3ab 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -4,12 +4,12 @@ import {
   MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
   TERRITORY_BILLING_TYPES, MAX_COMMENT_TEXT_LENGTH, MAX_POST_TEXT_LENGTH, MIN_TITLE_LENGTH, BOUNTY_MIN, BOUNTY_MAX, BALANCE_LIMIT_MSATS
 } from './constants'
-import { URL_REGEXP, WS_REGEXP } from './url'
 import { SUPPORTED_CURRENCIES } from './currency'
 import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
 import { msatsToSats, numWithUnits, abbrNum } from './format'
 import * as usersFragments from '../fragments/users'
 import * as subsFragments from '../fragments/subs'
+import { B64_REGEX, HEX_REGEX, isInvoiceMacaroon } from './macaroon'
 const { SUB } = subsFragments
 const { NAME_QUERY } = usersFragments
 
@@ -44,6 +44,63 @@ addMethod(string, 'or', function (schemas, msg) {
   })
 })
 
+addMethod(string, 'url', function (schemas, msg = 'invalid url') {
+  return this.test({
+    name: 'url',
+    message: msg,
+    test: value => {
+      try {
+        // eslint-disable-next-line no-new
+        new URL(value)
+        return true
+      } catch (e) {
+        try {
+          // eslint-disable-next-line no-new
+          new URL(`http://${value}`)
+          return true
+        } catch (e) {
+          return false
+        }
+      }
+    },
+    exclusive: false
+  })
+})
+
+addMethod(string, 'ws', function (schemas, msg = 'invalid websocket') {
+  return this.test({
+    name: 'ws',
+    message: msg,
+    test: value => {
+      try {
+        // eslint-disable-next-line no-new
+        new URL(value)
+        return true
+      } catch (e) {
+        return false
+      }
+    },
+    exclusive: false
+  })
+})
+
+addMethod(string, 'socket', function (schemas, msg = 'invalid socket') {
+  return this.test({
+    name: 'socket',
+    message: msg,
+    test: value => {
+      try {
+        const url = new URL(`http://${value}`)
+        return url.hostname && url.port && !url.username && !url.password &&
+            (!url.pathname || url.pathname === '/') && !url.search && !url.hash
+      } catch (e) {
+        return false
+      }
+    },
+    exclusive: false
+  })
+})
+
 addMethod(string, 'https', function () {
   return this.test({
     name: 'https',
@@ -81,6 +138,11 @@ const lightningAddressValidator = process.env.NODE_ENV === 'development'
     'address is no good')
   : string().email('address is no good')
 
+const hexOrBase64Validator = string().or([
+  string().matches(HEX_REGEX),
+  string().matches(B64_REGEX)
+], 'invalid encoding')
+
 async function usernameExists (name, { client, models }) {
   if (!client && !models) {
     throw new Error('cannot check for user')
@@ -220,14 +282,34 @@ export function advSchema (args) {
 
 export function lnAddrAutowithdrawSchema ({ me } = {}) {
   return object({
-    lnAddr: lightningAddressValidator.required('required').test({
-      name: 'lnAddr',
+    address: lightningAddressValidator.required('required').test({
+      name: 'address',
       test: addr => !addr.endsWith('@stacker.news'),
       message: 'automated withdrawals must be external'
     }),
+    ...autowithdrawSchemaMembers({ me })
+  })
+}
+
+export function LNDAutowithdrawSchema ({ me } = {}) {
+  return object({
+    socket: string().socket().required('required'),
+    macaroon: hexOrBase64Validator.required('required').test({
+      name: 'macaroon',
+      test: isInvoiceMacaroon,
+      message: 'not an invoice macaroon'
+    }),
+    cert: hexOrBase64Validator,
+    ...autowithdrawSchemaMembers({ me })
+  })
+}
+
+export function autowithdrawSchemaMembers ({ me } = {}) {
+  return {
+    priority: boolean(),
     autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`),
     autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50')
-  })
+  }
 }
 
 export function bountySchema (args) {
@@ -263,7 +345,7 @@ export function linkSchema (args) {
   return object({
     title: titleValidator,
     text: textValidator(MAX_POST_TEXT_LENGTH),
-    url: string().matches(URL_REGEXP, 'invalid url').required('required'),
+    url: string().url().required('required'),
     ...advPostSchemaMembers(args),
     ...subSelectSchemaMembers(args)
   }).test({
@@ -364,12 +446,12 @@ export const emailSchema = object({
 })
 
 export const urlSchema = object({
-  url: string().matches(URL_REGEXP, 'invalid url').required('required')
+  url: string().url().required('required')
 })
 
 export const namedUrlSchema = object({
   text: string().required('required').trim(),
-  url: string().matches(URL_REGEXP, 'invalid url').required('required')
+  url: string().url().required('required')
 })
 
 export const amountSchema = object({
@@ -390,7 +472,7 @@ export const settingsSchema = object({
       string().nullable().matches(NOSTR_PUBKEY_HEX, 'must be 64 hex chars'),
       string().nullable().matches(NOSTR_PUBKEY_BECH32, 'invalid bech32 encoding')], 'invalid pubkey'),
   nostrRelays: array().of(
-    string().matches(WS_REGEXP, 'invalid web socket address')
+    string().ws()
   ).max(NOSTR_MAX_RELAY_NUM,
     ({ max, value }) => `${Math.abs(max - value.length)} too many`),
   hideBookmarks: boolean(),
diff --git a/package-lock.json b/package-lock.json
index 2360d1ae..20021bcb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,11 +33,13 @@
         "domino": "^2.1.6",
         "formik": "^2.4.5",
         "github-slugger": "^2.0.0",
+        "google-protobuf": "^3.21.2",
         "graphql": "^16.8.1",
         "graphql-scalar": "^0.1.0",
         "graphql-tag": "^2.12.6",
         "graphql-type-json": "^0.3.2",
         "ln-service": "^57.1.3",
+        "macaroon": "^3.0.4",
         "mathjs": "^11.11.2",
         "mdast-util-find-and-replace": "^3.0.1",
         "mdast-util-from-markdown": "^2.0.0",
@@ -7250,6 +7252,11 @@
         "node": ">=4"
       }
     },
+    "node_modules/google-protobuf": {
+      "version": "3.21.2",
+      "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz",
+      "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA=="
+    },
     "node_modules/gopd": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
diff --git a/package.json b/package.json
index c4bae7d7..3a758fae 100644
--- a/package.json
+++ b/package.json
@@ -36,11 +36,13 @@
     "domino": "^2.1.6",
     "formik": "^2.4.5",
     "github-slugger": "^2.0.0",
+    "google-protobuf": "^3.21.2",
     "graphql": "^16.8.1",
     "graphql-scalar": "^0.1.0",
     "graphql-tag": "^2.12.6",
     "graphql-type-json": "^0.3.2",
     "ln-service": "^57.1.3",
+    "macaroon": "^3.0.4",
     "mathjs": "^11.11.2",
     "mdast-util-find-and-replace": "^3.0.1",
     "mdast-util-from-markdown": "^2.0.0",
diff --git a/pages/settings/wallets/index.js b/pages/settings/wallets/index.js
index a03b5589..a9ffe44c 100644
--- a/pages/settings/wallets/index.js
+++ b/pages/settings/wallets/index.js
@@ -5,17 +5,29 @@ import { WalletCard } from '../../../components/wallet-card'
 import { LightningAddressWalletCard } from './lightning-address'
 import { LNbitsCard } from './lnbits'
 import { NWCCard } from './nwc'
+import { LNDCard } from './lnd'
+import { WALLETS } from '../../../fragments/wallet'
+import { useQuery } from '@apollo/client'
+import PageLoading from '../../../components/page-loading'
 
-export const getServerSideProps = getGetServerSideProps({ authRequired: true })
+export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
+
+export default function Wallet ({ ssrData }) {
+  const { data } = useQuery(WALLETS)
+
+  if (!data && !ssrData) return 
+  const { wallets } = data || ssrData
+  const lnd = wallets.find(w => w.type === 'LND')
+  const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS')
 
-export default function Wallet () {
   return (
     
       
         attach wallets
         attach wallets to supplement your SN wallet
         
-          
+          
+          
           
           
           
diff --git a/pages/settings/wallets/lightning-address.js b/pages/settings/wallets/lightning-address.js
index f8605de1..d3301dfb 100644
--- a/pages/settings/wallets/lightning-address.js
+++ b/pages/settings/wallets/lightning-address.js
@@ -1,92 +1,70 @@
-import { InputGroup } from 'react-bootstrap'
 import { getGetServerSideProps } from '../../../api/ssrApollo'
 import { Form, Input } from '../../../components/form'
 import { CenterLayout } from '../../../components/layout'
 import { useMe } from '../../../components/me'
 import { WalletButtonBar, WalletCard } from '../../../components/wallet-card'
 import { useMutation } from '@apollo/client'
-import { REMOVE_AUTOWITHDRAW, SET_AUTOWITHDRAW } from '../../../fragments/users'
 import { useToast } from '../../../components/toast'
-import { lnAddrAutowithdrawSchema, isNumber } from '../../../lib/validate'
+import { lnAddrAutowithdrawSchema } from '../../../lib/validate'
 import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
-import { numWithUnits } from '../../../lib/format'
+import { AutowithdrawSettings, autowithdrawInitial } from '../../../components/autowithdraw-shared'
+import { REMOVE_WALLET, UPSERT_WALLET_LNADDR, WALLET_BY_TYPE } from '../../../fragments/wallet'
 
-export const getServerSideProps = getGetServerSideProps({ authRequired: true })
+const variables = { type: 'LIGHTNING_ADDRESS' }
+export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
 
-function useAutoWithdrawEnabled () {
-  const me = useMe()
-  return me?.privates?.lnAddr && isNumber(me?.privates?.autoWithdrawThreshold) && isNumber(me?.privates?.autoWithdrawMaxFeePercent)
-}
-
-export default function LightningAddress () {
+export default function LightningAddress ({ ssrData }) {
   const me = useMe()
   const toaster = useToast()
   const router = useRouter()
-  const [setAutoWithdraw] = useMutation(SET_AUTOWITHDRAW)
-  const enabled = useAutoWithdrawEnabled()
-  const [removeAutoWithdraw] = useMutation(REMOVE_AUTOWITHDRAW)
-  const autoWithdrawThreshold = isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
-  const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(autoWithdrawThreshold / 10), 1))
+  const [upsertWalletLNAddr] = useMutation(UPSERT_WALLET_LNADDR)
+  const [removeWallet] = useMutation(REMOVE_WALLET)
 
-  useEffect(() => {
-    setSendThreshold(Math.max(Math.floor(me?.privates?.autoWithdrawThreshold / 10), 1))
-  }, [autoWithdrawThreshold])
+  const { walletByType: wallet } = ssrData || {}
 
   return (
     
       lightning address
-      autowithdraw to a lightning address to maintain desired balance
+      autowithdraw to a lightning address