diff --git a/.github/pyproject.toml b/.github/pyproject.toml
index a86d6072..ff8c5dad 100644
--- a/.github/pyproject.toml
+++ b/.github/pyproject.toml
@@ -19,11 +19,11 @@ asyncpg = "*"
 base58 = "*"
 black = "*"
 mypy = "*"
-openai = "*"
+openai = "^0.20.0"
 phonenumbers = "*"
 prometheus_async = "*"
 prometheus_client = "*"
-protobuf = "*"
+protobuf = "^3"
 pycryptodome = "*"
 pylint = "*"
 PyQRCode = "^1.2.1"
@@ -67,7 +67,7 @@ disable= [
     "consider-using-with",
     "consider-using-from-import",
     "fixme",
-    "no-self-use",
+    # "no-self-use",
     "unspecified-encoding",
     # handled by black
     "format",
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index a92ed091..345277b0 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -21,7 +21,7 @@ jobs:
           key: '${{ runner.os }}-poetry-v2-${{ hashFiles(''**/poetry.lock'') }}'
       - name: Install dependencies
         if: steps.cache-poetry.outputs.cache-hit != 'true'
-        run: cp .github/pyproject.toml .; rm poetry.lock; poetry install 
+        run: cp .github/pyproject.toml .; rm poetry.lock; poetry run pip install --upgrade pip; poetry install
       - name: List dependency versions (check this if you get confusing results)
         run: poetry show --tree
       # - name: Setup tmate session # this lets you SSH into the container
diff --git a/forest/core.py b/forest/core.py
index 9d8aa948..4d138991 100755
--- a/forest/core.py
+++ b/forest/core.py
@@ -351,6 +351,10 @@ async def enqueue_blob_messages(self, blob: JSON) -> None:
         if "result" in blob:
             if isinstance(blob["result"], dict):
                 message_blob = blob
+            elif isinstance(blob["result"], list):
+                # Message expects a dict with an id, but signal-cli listContacts returns a list
+                # we're usually only get a single contact, so we massage this
+                message_blob = {"id": blob["id"], "result": blob["result"][0]}
             else:
                 logging.warning(blob["result"])
         if "error" in blob:
@@ -1049,6 +1053,17 @@ async def do_uptime(self, _: Message) -> str:
         return t
 
 
+def compose_payment_content(receipt: str, note: str) -> dict:
+    # serde expects bytes to be u8[], not b64
+    tx = {"mobileCoin": {"receipt": u8(receipt)}}
+    note = note or "check out this java-free payment notification"
+    payment = {"Item": {"notification": {"note": note, "Transaction": tx}}}
+    # SignalServiceMessageContent protobuf represented as JSON (spicy)
+    # destination is outside the content so it doesn't matter,
+    # but it does contain the bot's profileKey
+    return {"dataMessage": {"body": None, "payment": payment}}
+
+
 class PayBot(ExtrasBot):
     @requires_admin
     async def do_fsr(self, msg: Message) -> Response:
@@ -1129,9 +1144,19 @@ async def payment_response(self, msg: Message, amount_pmob: int) -> Response:
 
     async def get_signalpay_address(self, recipient: str) -> Optional[str]:
         "get a receipient's mobilecoin address"
-        result = await self.signal_rpc_request("getPayAddress", peer_name=recipient)
+        if utils.AUXIN:
+            result = await self.signal_rpc_request("getPayAddress", peer_name=recipient)
+            b64_address = (
+                result.blob.get("Address", {})
+                .get("mobileCoinAddress", {})
+                .get("address")
+            )
+            if result.error or not b64_address:
+                logging.info("bad address: %s", result.blob)
+                return None
+        result = await self.signal_rpc_request("listContacts", recipient=recipient)
         b64_address = (
-            result.blob.get("Address", {}).get("mobileCoinAddress", {}).get("address")
+            result.blob.get("result", {}).get("profile", {}).get("mobileCoinAddress")
         )
         if result.error or not b64_address:
             logging.info("bad address: %s", result.blob)
@@ -1207,6 +1232,39 @@ async def build_gift_code(self, amount_pmob: int) -> list[str]:
             f"redeemable for {str(mc_util.pmob2mob(amount_pmob - FEE_PMOB)).rstrip('0')} MOB",
         ]
 
+    async def confirm_tx_timeout(self, tx_id: str, recipient: str, timeout: int) -> str:
+        logging.debug("Attempting to confirm tx status for %s", recipient)
+        status = "tx_status_pending"
+        for i in range(timeout):
+            tx_status = await self.mob_request(
+                "get_transaction_log", transaction_log_id=tx_id
+            )
+            status = (
+                tx_status.get("result", {}).get("transaction_log", {}).get("status")
+            )
+            if status == "tx_status_succeeded":
+                logging.info(
+                    "Tx to %s suceeded - tx data: %s",
+                    recipient,
+                    tx_status.get("result"),
+                )
+                break
+            if status == "tx_status_failed":
+                logging.warning(
+                    "Tx to %s failed - tx data: %s",
+                    recipient,
+                    tx_status.get("result"),
+                )
+                break
+            await asyncio.sleep(1)
+        if status == "tx_status_pending":
+            logging.warning(
+                "Tx to %s timed out - tx data: %s",
+                recipient,
+                tx_status.get("result"),
+            )
+        return status
+
     # FIXME: clarify signature and return details/docs
     async def send_payment(  # pylint: disable=too-many-locals
         self,
@@ -1272,51 +1330,34 @@ async def send_payment(  # pylint: disable=too-many-locals
             msg.status, msg.transaction_log_id = "tx_status_failed", tx_id
             return msg
 
-        receipt_resp = await self.mob_request(
-            "create_receiver_receipts",
-            tx_proposal=prop,
-            account_id=await self.mobster.get_account(),
-        )
-        content = await self.fs_receipt_to_payment_message_content(
-            receipt_resp, receipt_message
-        )
-        # pass our beautifully composed JSON content to auxin.
-        # message body is ignored in this case.
-        payment_notif = await self.send_message(recipient, "", content=content)
-        resp_future = asyncio.create_task(self.wait_for_response(rpc_id=payment_notif))
-
-        if confirm_tx_timeout:
-            logging.debug("Attempting to confirm tx status for %s", recipient)
-            status = "tx_status_pending"
-            for i in range(confirm_tx_timeout):
-                tx_status = await self.mob_request(
-                    "get_transaction_log", transaction_log_id=tx_id
-                )
-                status = (
-                    tx_status.get("result", {}).get("transaction_log", {}).get("status")
-                )
-                if status == "tx_status_succeeded":
-                    logging.info(
-                        "Tx to %s suceeded - tx data: %s",
-                        recipient,
-                        tx_status.get("result"),
-                    )
-                    break
-                if status == "tx_status_failed":
-                    logging.warning(
-                        "Tx to %s failed - tx data: %s",
-                        recipient,
-                        tx_status.get("result"),
-                    )
-                    break
-                await asyncio.sleep(1)
-
-            if status == "tx_status_pending":
-                logging.warning(
-                    "Tx to %s timed out - tx data: %s",
-                    recipient,
-                    tx_status.get("result"),
+        full_service_receipt = (
+            await self.mob_request(
+                "create_receiver_receipts",
+                tx_proposal=prop,
+                account_id=account_id,
+            )
+        )["result"]["receiver_receipts"][0]
+        # this gets us a Receipt protobuf
+        b64_receipt = mc_util.full_service_receipt_to_b64_receipt(full_service_receipt)
+        if utils.AUXIN:
+            content = compose_payment_content(b64_receipt, receipt_message)
+            # pass our beautifully composed JSON content to auxin.
+            # message body is ignored in this case.
+            payment_notif = await self.send_message(recipient, "", content=content)
+            resp_future = asyncio.create_task(
+                self.wait_for_response(rpc_id=payment_notif)
+            )
+        else:
+            resp_future = asyncio.create_task(
+                self.signal_rpc_request(
+                    "sendPaymentNotification",
+                    receipt=b64_receipt,
+                    note=receipt_message,
+                    recipient=recipient,
                 )
+            )
+        if confirm_tx_timeout:
+            status = await self.confirm_tx_timeout(tx_id, recipient, confirm_tx_timeout)
             resp = await resp_future
             # the calling function can use these to check the payment status
             resp.status, resp.transaction_log_id = status, tx_id  # type: ignore
diff --git a/forest/payments_monitor.py b/forest/payments_monitor.py
index f66ca749..98eca91b 100644
--- a/forest/payments_monitor.py
+++ b/forest/payments_monitor.py
@@ -156,7 +156,7 @@ async def split_txos_slow(
         built = 0
         i = 0
         utxos: list[tuple[str, int]] = list(reversed((await self.get_utxos()).items()))
-        if sum([value for _, value in utxos]) < output_millimob * 1e9 * target_quantity:
+        if sum(value for _, value in utxos) < output_millimob * 1e9 * target_quantity:
             return "insufficient MOB"
         while built < (target_quantity + 3):
             if len(utxos) < 1:
diff --git a/forest/pghelp.py b/forest/pghelp.py
index 806250e0..e806b815 100644
--- a/forest/pghelp.py
+++ b/forest/pghelp.py
@@ -150,7 +150,7 @@ def finish_init(self) -> None:
         for k in self.queries:
             if AUTOCREATE and "create" in k and "index" in k:
                 self.logger.info(f"creating index via {k}")
-                self.__getattribute__(f"sync_{k}")()
+                getattr(self, f"sync_{k}")()
 
     _autocreating_table = False
 
diff --git a/hotline/hotline.py b/hotline/hotline.py
index 725d1aef..46d7ca98 100644
--- a/hotline/hotline.py
+++ b/hotline/hotline.py
@@ -549,6 +549,7 @@ async def do_help(self, msg: Message) -> Response:
     @hide
     async def do_remove(self, msg: Message) -> Response:
         """Removes a given event by code, if the msg.uuid is the owner."""
+        # pylint: disable=unnecessary-dunder-call
         if not await self.check_user_owns(msg.uuid, msg.arg1 or ""):
             return f"Sorry, it doesn't look like you own {msg.arg1}."
         parameters = []
diff --git a/pyproject.toml b/pyproject.toml
index 4fc1351e..fd501e87 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -63,7 +63,7 @@ disable= [
     "consider-using-with",
     "consider-using-from-import",
     "fixme",
-    "no-self-use",
+    # "no-self-use",
     "unspecified-encoding",
     # handled by black
     "format",