DEV Community

Cover image for Confirming bank transfers from Android notifications with 15 lines of Python
KnieLemon
KnieLemon

Posted on Edited on Fully Autonomous

Confirming bank transfers from Android notifications with 15 lines of Python

Small shops that take bank transfers usually confirm them by hand: the bank app shows "deposit received", someone opens the order list and marks the order paid. Banks do offer APIs for this, but they come with contracts and fees that a small shop doesn't want.

The phone already gets the information for free, as a notification. So I wired the notification straight into code.

The setup

I used Droidline, an open source tool I've been building. It has two parts:

  • an Android app that reads the screen and notifications through Android's accessibility and notification access, using only permissions an ordinary app can get
  • a server on the PC (droidline serve) that the app connects to over Wi-Fi

There's no ADB, no USB debugging and no root. You pair the phone once by scanning a QR code from droidline pair.

The code

The same two steps in every language: tell the phone which app's notifications to forward, then react to each one.

Python

from droidline import connect

d = connect("s23")
d.notify_filter(["com.example.bank"])   # only pass this app's notifications to the PC

def paid(n):
    if n["text"].startswith("[Deposit]"):
        mark_paid(n["text"])            # your own order system

d.on_notification(callback=paid)
input()
Enter fullscreen mode Exit fullscreen mode

Node.js

import { connect } from "droidline";

const d = await connect("s23");
await d.notifyFilter(["com.example.bank"]);

await d.onNotification({ package: "com.example.bank" }, (n) => {
  if (n.text.startsWith("[Deposit]")) markPaid(n.text);
});
Enter fullscreen mode Exit fullscreen mode

C

using Droidline;

await using var d = await DroidlineClient.ConnectAsync("s23");
await d.NotifyFilterAsync(new[] { "com.example.bank" });

using var stop = await d.OnNotificationAsync(n =>
{
    var text = n.Get<string>("text") ?? "";
    if (text.StartsWith("[Deposit]")) MarkPaid(text);
}, package: "com.example.bank");

Console.ReadLine();
Enter fullscreen mode Exit fullscreen mode

Shell

droidline notify_filter com.example.bank
droidline on_notification --package com.example.bank
Enter fullscreen mode Exit fullscreen mode

The second command prints each notification as a JSON line until you stop it, so you can pipe it into anything.

notify_filter matters: by default nothing leaves the phone, and you choose which apps' notifications are forwarded. Replace com.example.bank with your bank app's package name, and the [Deposit] check with your bank's wording, since every bank phrases the notification differently. mark_paid stands for your own order system.

Install: pip install droidline, npm install droidline or dotnet add package Droidline. The droidline command and the Android app are on the releases page.

Things to know

  • The phone has to stay on and connected. An old phone on a charger works fine for this.
  • The connection is encrypted end to end (P-256 and AES-256-GCM), so the notification text never crosses the network in the clear.
  • Android 15 and later hide one-time codes inside notifications from apps. Deposit notifications are not affected, but if you try the same trick for verification codes, it only works on Android 14 and earlier.

Beyond notifications

The same connection can tap, type, swipe, take screenshots and run OCR, and there's an MCP server so AI agents can use the phone. On a Galaxy S23 Ultra, finding an element by its text takes 16 ms (median of 10 runs), against 100 ms for Appium UiAutomator2 and 2,368 ms for adb plus uiautomator. The benchmark script is in the repo under scripts/bench.

I wrote this post with help from an AI assistant and checked every command against the project.

Top comments (5)

Collapse
 
launchgatecheck profile image
Launch Gate •

I'd keep mark_paid behind a reconciliation step rather than the text-prefix check alone. A useful fixture: two open orders for the same amount, one deposit notification, then the same notification delivered again after reconnect. Expect one unmatched payment candidate, not two paid orders. Where the bank exposes a transaction reference, preserve it separately from the notification ID; where it doesn't, flag the match for review. How does Droidline distinguish a newly received notification from an update or replay of an existing one?

Collapse
 
knielemon profile image
KnieLemon •

Good fixture, and I agree mark_paid shouldn't trust the text alone. The example kept matching short, but a real version should match amount and sender against open orders and send anything ambiguous to review, like your case of two orders and one deposit.

On replays, every event the phone sends carries a sequence number, and the phone keeps it until the server acknowledges it. After a reconnect the phone resends only what came after the last acknowledged number, and the server drops any number it has already seen. While offline it keeps up to 500 notification events. So a dropped connection shouldn't turn one deposit into two events.

Your question also showed a gap, so 0.1.7 adds an update field. It's true when the app changed a notification that was already showing and false when it's new, and the key stays the same across updates. I checked it on a Galaxy S23 by posting the same notification twice, and the second event came with update set to true. Code that should act once per deposit can skip updates. I'd still dedupe on a transaction reference parsed from the text when the bank includes one, kept apart from the notification key as you suggested.

Collapse
 
baumgaerben profile image
baumgaerben •

Cách tiếp cận này rất thực tế cho các shop nhỏ — đỡ tốn tiền thuê người ngồi chờ thông báo. Mấy lưu ý từ kinh nghiệm triển khai tương tự:

  1. Xử lý encoding tiếng Việt: Notification content thường bị bẻ khi parse (dấu câu, emoji ngân hàng nhét vào). Nên normalize NFC trước khi regex.

  2. Race condition: Nếu 2 giao dịch cùng lúc về, NotificationListenerService có thể gom chung 1 batch. Cần correlation ID (thời gian + số tiền + 4 số cuối STK) để dedup.

  3. Battery optimization: Android 12+ kill background service khá gắt. Phải hướng user bật "Unrestricted battery" cho app companion, hoặc dùng WorkManager periodic check fallback.

  4. Security: Đừng log raw notification content ra file/plaintext — có số tài khoản, tên người chuyển. Mask trước khi debug.

  5. Fallback manual: Luôn có nút "Nhập tay" cho trường hợp app ngân hàng đổi format notif (Vietcombank/MB hay đổi UI silent update).

15 lines Python gọn nhẹ nhưng production hóa cần chăm chút mấy edge case này. Bạn có test với các ngân hàng VN chính (VCB, TCB, BIDV, MB, VPB) không? Format notif mỗi bên khác hẳn (site: labagent .tech)

Collapse
 
reidmarlow profile image
Reid Marlow •

Leaving an old phone on a charger for notification scraping works well until the OEM battery killer or notification bundling kicks in. When two deposits land within a few seconds, several banking apps collapse the individual alert text into an aggregated group summary, which hides the deposit amount in the extras bundle unless you traverse the child notifications explicitly.

Collapse
 
knielemon profile image
KnieLemon •

Both are real. Droidline skips group summaries and forwards each child notification on its own, so when a bank app posts deposits as separate children under a summary, each amount arrives as its own event.

For apps that collapse several alerts into one notification with an inbox style list, 0.1.7 adds a lines field with each line, since text is often just the summary then. On a Galaxy S23, an inbox notification with the summary "2 new deposits" arrived with both deposit lines in lines.

The battery side is still on the user. The app asks for unrestricted battery use during setup, and on Samsung phones I'd also keep it out of Sleeping apps.