Skip to content

Commit 323423d

Browse files
committed
feat: Web Monetization + Open Payments: Part 2 Payment Sessions
1 parent 206b8b0 commit 323423d

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

src/content/blog/2025-12-03-web-monetization-open-payments-part-1-connecting-wallet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,4 @@ If you believe your tokens are compromised, you can revoke the extension's grant
174174

175175
## Next up: Sending money
176176

177-
With your wallet connected and access tokens handy, the next article will focus on the payment session setup, followed by a detailed look at the core function: precisely how, when, and where the extension triggers the payments through your wallet.
177+
With your wallet connected and access tokens handy, the [next article](./web-monetization-open-payments-part-2-payment-sessions) will focus on the payment session setup, followed by a detailed look at the core function: precisely how, when, and where the extension triggers the payments through your wallet.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: 'How Web Monetization uses Open Payments - Part 2: Payment Sessions'
3+
description: 'Explore next steps for sending money: how the extension finds receiving wallet addresses and sets up the necessary payment sessions.'
4+
date: 2025-12-08
5+
slug: web-monetization-open-payments-part-2-payment-sessions
6+
authors:
7+
- Sid Vishnoi
8+
author_urls:
9+
- https://sidvishnoi.com?ref=ilf_engg_blog
10+
tags:
11+
- Open Payments
12+
- Web Monetization
13+
---
14+
15+
In the previous article, we discussed how the [Web Monetization extension connects to your wallet](./web-monetization-open-payments-part-1-connecting-wallet) using the Open Payments API. This article will explore the next steps for sending money: discovering the receiving wallet addresses on websites and setting up the necessary payment sessions.
16+
17+
## Finding receiving wallet addresses
18+
19+
As part of the Web Monetization API, websites declare the wallet address where they'd like to receive the money by using the HTML `<link>` tag:
20+
21+
```html
22+
<link rel="monetization" href="https://example-wallet.com/my-wallet" />
23+
```
24+
25+
The extension's background script and pop-up cannot access this HTML from webpage directly due to browser security restrictions. This is where the content scripts come into play.
26+
27+
The content script is the primary interface between the extension and the web page content. It operates directly within the context of web pages, including content inside iframes. In the context of the Web Monetization extension, its primary responsibility is the discovery, validation, and lifecycle management of every potential monetization link element on the site (precisely, within a `Document`).
28+
29+
Once the page loads, the injected content script immediately begins actively monitoring the DOM for all monetization link elements (`<link rel="monetization">` tags).
30+
31+
```js
32+
function getLinkElements() {
33+
return document.querySelector('link[rel="monetization"]')
34+
}
35+
```
36+
37+
When a new such link element is discovered, the content script's first critical action is to assign it a unique ID. This ID creates a map between the live DOM element and its representation in the background script.
38+
39+
The script proceeds by validating the link element, ensuring its `href` attribute contains a valid wallet address URL. If the address successfully loads with a valid wallet address response, a `load` event is immediately fired on the element, signaling to the web page that the link element is viable for upcoming payments. Conversely, any invalid or malformed address triggers an `error` event, excluding the element from the monetization pool.
40+
41+
The script uses the `MutationObserver` API to maintain a persistent, real-time awareness of the state of all the link elements in a document. For instance, if a developer adds the `disabled` attribute to a monetized link element, the content script instantly signals the background to pause payments for that element. Similarly, when a monetized link element gets removed from the DOM, the content script signals the background script to terminate the associated monetization stream.
42+
43+
```ts
44+
function documentMutationCallback() {
45+
const linkElements = getLinkElements()
46+
const addedLinkElements = diff(linkElements, existingLinkElements)
47+
const removedLinkElements = diff(existingLinkElements, linkElements)
48+
49+
addedLinkElements.forEach((elem) => {
50+
const id = assignId(elem)
51+
const walletAddressInfo = await validateAndFetch(elem.href)
52+
dispatch('start_monetization', { walletAddressInfo, id })
53+
})
54+
55+
removedLinkElements.forEach((elem) => {
56+
const id = getAssignedId(elem)
57+
dispatch('stop_monetization', { id })
58+
})
59+
}
60+
```
61+
62+
All of these state changes — discovery, modification, and removal — are communicated via message passing to the background script. This ensures the background script, which manages the actual payment logic, is always perfectly synchronized with the live state of the web page. This synchronization ultimately feeds the pop-up to display accurate, real-time monetization status to the user.
63+
64+
## Payment Session
65+
66+
A Payment Session in the background script acts as the operational counterpart for a monetization link element. It uses the unique ID from the content script to maintain a link between the element in the DOM and its background representation. When a new payment session is initiated, it must undergo a series of checks.
67+
68+
First, it verifies whether the user's connected wallet address is authorized to send payments to the specified wallet address. The extension sends a “[create quote](https://openpayments.dev/apis/resource-server/operations/create-quote/)” request to the receiver’s resource server endpoint. If this request results in an "invalid receiver" error, it means the two wallets are not "peered" or connected. This may occur due to legal or technical reasons. For example, a wallet on the test namespace (e.g., [Interledger Test Wallet](https://wallet.interledger-test.dev/)) _fake money_ cannot send a payment involving real money to a production wallet (e.g. [Interledger Wallet](https://interledger.app/) or GateHub). Consequently, we mark this payment session as "invalid", and no payments can proceed during the browsing session for this wallet address.
69+
70+
The Payment Session object also has some helper methods to manage the payments effectively: it includes a method to send a specified amount of money, a method for dispatching `MonetizationEvent` to corresponding link elements (next article), and helpers to check if wallet address is disabled, paused or payable, ensuring the recipient's status is constantly confirmed to prevent failed payments or unnecessary transactions.
71+
72+
It also needs to find a minimum sendable amount, which I’ll explain next.
73+
74+
## Find minimum sendable amount
75+
76+
Before sending money, we need to determine the amount for a specific wallet address.
77+
78+
For simplicity, we will only use an asset scale of 2 for this discussion. Think of it as a currency divided into hundredths, like dollars (100 cents) or rupees (100 paise). This means the smallest transferable unit is 0.01.
79+
80+
If both the sending and receiving wallets use the same currency, and there are no fees, we could easily send a micropayment of 1 unit. For instance, with USD, this typically represents $0.01.
81+
82+
When transferring funds across currencies (e.g., from a USD wallet to an MXN wallet), the recipient amount can fluctuate. For example, sending US$0.01 could result in the MXN wallet receiving approximately MX$0.18 (based on the current exchange rate). If a transaction fee is applied, the net amount received could be reduced, perhaps to MX$0.15.
83+
84+
But what if we were sending from an MXN wallet to a USD wallet? Can we send MX$0.01 to a USD wallet? If so, how much will the USD wallet receive - ignoring any transaction fees? Ideally, it would be US$0.00054, but an asset scale of 2 implies the minimum receivable amount is constrained to US$0.01. To ensure supporters send only what receivers can actually receive, we must determine an appropriate amount to send.
85+
86+
When using Open Payments, attempting to create an outgoing payment quote of MX$0.01 to a USD wallet will result in a "non-positive receive amount" error. This is because MX$0.01 translates to a USD amount of US$0.00 - not a positive amount (any value greater than zero is considered positive here; let's steer clear of [positive and negative zeroes](https://math.stackexchange.com/a/26708) for now!).
87+
88+
In the newer implementations, the Open Payments API will provide us with the minimum sendable amount along with the aforementioned error.
89+
However, for wallets that do not use the latest implementation, we need to employ a workaround to determine that amount. It's time for some math and algorithm work!
90+
91+
### Manually finding minimum sendable amount
92+
93+
We use a probing method to find the minimum sendable amount. For example, if we try to send MX$0.01, we receive a "non-positive receive amount" error. We note this error and then attempt MX$0.02, which also fails.
94+
95+
We can continue testing currency amounts starting from MX$0.03 up to MX$0.18, which would require 18 separate quote requests to find the right amount. To optimize this process, we can change the increment to an exponential scale by testing MX$0.02, MX$0.04, MX$0.08, MX$0.16, and so on. This leads to far fewer requests - especially with greater currency exchange rates (e.g., 1 USD ~ 88 INR - imagine how many requests it can take without the exponential approach).
96+
97+
Now, we can't send MX$0.16 either, but MX$0.32 is sendable. But it’s not the minimum sendable amount. Why does that matter? MX$0.32 is equal to US$0.017. The wallet can only receive US$0.010, so where does the remaining US$0.007 go? That’s up to the wallet's implementation. They could round up the amount to US$0.02, losing US$0.003 each transaction, or give the receiver US$0.01, keeping a profit of US$0.007 each time. They could also find a balance to maintain liquidity for currency conversion differences.
98+
99+
The sender is sending MX$0.32, expecting the receiver to receive the same amount. However, this is not the case. Micropayments, without some form of consolidation at a higher asset scale (such as US$0.000001), will always lead to this issue.
100+
101+
In the extension, we chose fairness by sending only amounts that can be properly converted for the recipient. We need to determine the minimum amount that can be sent. How do we do that?
102+
103+
We observed that MX$0.16 was not sendable, while MX$0.32 was sendable. Therefore, the minimum sendable amount lies somewhere in between. This is a textbook use-case of the binary search algorithm!
104+
The midpoint between the two amounts is MX$0.24, which is sendable, but it is still above the minimum sendable amount. When we find the midpoint of MX$0.16 and MX$0.24, we get MX$0.20, which is also not the minimum sendable. After some back and forth, we determine that MX$0.17 isn’t sendable, but MX$0.18 is, concluding that MX$0.18 must be the minimum sendable amount.
105+
106+
Each test to determine whether an amount is sendable or not costs us one create quote request, which can add up significantly and delay the time it takes to send money to the recipient. To optimize this process, we can utilize currency exchange rates. Instead of starting our tests from MX$0.01, MX$0.02, MX$0.04, and so on up to MX$0.32, we can begin at MX$0.18. This way, we’ve trimmed our search space by a considerable margin. We still need to confirm whether the amount is sendable, as there may be associated fees, and to verify if it is indeed the minimum sendable amount. However, we have minimized the number of unnecessary requests.
107+
108+
As I mentioned earlier, newer implementations of the Open Payments API provide us with a minimum sendable amount directly, and we won’t need this probing in the clients in future.
109+
110+
I hope this clarifies the importance of the minimum sendable amount. Give yourself a pat on the back for navigating the complexities of cross-currency micro-payments!
111+
112+
## Create incoming payment
113+
114+
Before creating the outgoing payment with our previously acquired tokens, the Open Payments API requires us to set up a receiving channel.
115+
116+
The process begins by creating an incoming payment grant on the receiver's wallet. This grant is non-interactive, meaning the user does not need to manually approve each incoming payment, which would be very cumbersome. Once we obtain the permission, we can proceed with creating the actual incoming payment.
117+
118+
The incoming payment serves as a dedicated "bucket" into which we can stream micro-payments throughout the entire browsing session. Instead of specifying a set amount to receive, we provide an expiration time, after which we will need to create a new bucket. When the incoming payment expires, the receiving wallet can start settling the amount in the background.
119+
120+
## Sending money
121+
122+
This article is already long enough, so let's dive into the fun part — actually executing a payment — in the next article!

0 commit comments

Comments
 (0)