kopia lustrzana https://github.com/Podcastindex-org/podcast-namespace
359 wiersze
13 KiB
Markdown
359 wiersze
13 KiB
Markdown
# bLIP 10 - A Lightning TLV record for carrying v4v payment metadata
|
|
|
|
```text
|
|
Original Author: Satoshis Stream <satoshisstream@pm.me>
|
|
Created: 2022-01-24
|
|
License: CC0
|
|
```
|
|
|
|
<br>
|
|
|
|
## Purpose
|
|
|
|
Using Value 4 Value apps, listeners can send sats to the podcasts they listen to. A per-minute payment is
|
|
called a `stream`, a manually triggered payment is called a `boost` and an automatically recurring (i.e. scheduled)
|
|
payment is called an `auto` payment. The `auto` and `boost` action types may contain a text message in the `message`
|
|
property. The `stream` type should not.
|
|
|
|
The payload is a simple JSON structure that is encoded as a TLV in the Lightning payment when it is sent. This
|
|
structure is explained in this document.
|
|
|
|
Simple Example:
|
|
|
|
```json
|
|
{
|
|
"podcast": "Podcasting 2.0",
|
|
"feedID": 920666,
|
|
"episode": "Episode 127: Fortress of Freedom",
|
|
"action": "boost",
|
|
"ts": 33
|
|
}
|
|
```
|
|
|
|
<br>
|
|
|
|
## Specification
|
|
|
|
The sender of a bLIP-10 payment (i.e. - the podcast app) needs to include some metadata so the podcast host can
|
|
identify for whom the payment is for. Most fields are optional.
|
|
|
|
<br>
|
|
|
|
### Formatting and encoding
|
|
|
|
A flat, key-value json structure is used where the keys listed below can be set. The json string is then encoded
|
|
into `utf8` and attached to the Lightning payment. Most of the time, the payment will be a `keysend` payment, but this
|
|
TLV data can be used in standard BOLT11 and BOLT12 payments also. Receivers of messages must be aware that there
|
|
is no guarantee for the order of the keys.
|
|
|
|
A more complex example:
|
|
|
|
```json
|
|
{
|
|
"app_name": "Castamatic",
|
|
"app_version": "8.0.6",
|
|
"value_msat_total": 49960,
|
|
"url": "https://feeds.buzzsprout.com/1844352.rss",
|
|
"podcast": "Mere Mortals",
|
|
"action": "stream",
|
|
"episode": "The Art Of NFT's & Aimless Wandering",
|
|
"episode_guid": "Buzzsprout-9931017",
|
|
"value_msat": 97940,
|
|
"ts": 574,
|
|
"name": "Podcaster",
|
|
"sender_name": "Peter"
|
|
}
|
|
```
|
|
|
|
Treated as `utf-8`, the hex value of the above json record would be:
|
|
|
|
```base64
|
|
7b226170705f6e616d65223a202243617374616d61746963222c20226170705f76657273696f6e223a2022382e302e36222c202276616c75655f
|
|
6d7361745f746f74616c223a2034393936302c202275726c223a202268747470733a2f2f66656564732e62757a7a7370726f75742e636f6d2f31
|
|
3834343335322e727373222c2022706f6463617374223a20224d657265204d6f7274616c73222c2022616374696f6e223a202273747265616d22
|
|
2c2022657069736f6465223a202254686520417274204f66204e4654277320262041696d6c6573732057616e646572696e67222c202265706973
|
|
6f64655f67756964223a202242757a7a7370726f75742d39393331303137222c202276616c75655f6d736174223a2039373934302c2022747322
|
|
3a203537342c20226e616d65223a2022506f64636173746572222c202273656e6465725f6e616d65223a20225065746572227d0a
|
|
```
|
|
|
|
<br>
|
|
|
|
### Types
|
|
|
|
If a field is indicated to be a `str` in the fields-list, that means it is a JSON string (within quotes). An `int` is
|
|
a plain number.
|
|
|
|
<br>
|
|
|
|
### Fields
|
|
|
|
Identifying the podcast **required**: use one or more of `podcast`, `guid`, `feedID` and/or `url`. **guid preferred**
|
|
|
|
* `guid` (
|
|
str) [The `<podcast:guid>` tag](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid).
|
|
* `podcast` (str) Title of the podcast
|
|
* `feedID` (int) ID of podcast in PodcastIndex.org directory
|
|
* `url` (str) RSS feed URL of podcast
|
|
|
|
<br>
|
|
|
|
Identifying the episode **recommended**: use any of `episode`, `itemID` and/or `episode_guid`. **episode_guid
|
|
preferred**
|
|
|
|
* `episode` (str) Title of the podcast episode
|
|
* `itemID` (int) ID of episode in PodcastIndex.org directory
|
|
* `episode_guid` (str) The GUID of the episode
|
|
|
|
<br>
|
|
|
|
Information about time within the episode **recommended**: use one of `ts` or `time`. **ts preferred**
|
|
|
|
* `ts` (int) Timestamp of when the payment was sent, in seconds, as an offset from zero (i.e. - playback position)
|
|
* `time` (str) Timestamp of when the payment was sent, in HH:MM:SS notation, as an offset from 00:00:00
|
|
(i.e. - playback position)
|
|
|
|
<br>
|
|
|
|
Other fields that are strongly recommended:
|
|
|
|
* `action` **recommended**: (str) "boost", "stream" or "auto". See Appending B of the
|
|
[value](/value.md#appendix-b---payment-actions) spec for details.
|
|
* `app_name`: **recommended** (str) Name of sending app
|
|
* `sender_name` **recommended** (str) Name of the sender (free text, not validated in any way)
|
|
* `value_msat_total`: **recommended** (int) TOTAL Number of millisats for the payment before any fees are subtracted.
|
|
This should be the number the listener entered into the app. Preserving
|
|
this value is important for numerology reasons. Certain numeric values can
|
|
have significance to the sender and/or receiver, so giving a way to show
|
|
this is critical.
|
|
|
|
<br>
|
|
|
|
Other optional fields:
|
|
|
|
* `message` (str) Text message to add to the payment. When this field is present, the payment is known as a
|
|
"boostagram".
|
|
* `app_version`: (str) Version of sending app
|
|
* `boost_link`: (str) App specific URL containing route to podcast, episode, and/or timestamp at time of the action.
|
|
The use case for this is sending a link along with the payment that will take the recipient to
|
|
the exact playback position within the episode where the payment was sent.
|
|
* `name` (str) Name for this split in value tag
|
|
* `sender_id` (str) Static random identifier for users, not displayed by apps to prevent abuse. Apps can set this
|
|
per-feed or app-wide. A GUID-like random identifier or a hash works well. Max 32 bytes (64 ascii
|
|
characters). This can be a Nostr hex encoded pubkey (not NIP-19) for purposes of sender attribution.
|
|
* `signature` (str) Optionally, this field can contain a signature for the payment, to be able to verify that the
|
|
user who sent it is actually who they claim in the `sender_id` field. If the `sender_id` contains
|
|
a Nostr public key, this field should contain a Nostr `sig` value as a 64-byte encoded hex string.
|
|
For the purpose of generating the Nostr signature, the following data should be serialized:
|
|
[0,`sender_id`,`ts`,1,[],`message`] to conform to the
|
|
[NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) specification. The resulting
|
|
serialized string should be hashed with `sha256` to obtain the value.
|
|
* `speed` (str) Speed in which the podcast was playing, in decimal notation at the time the payment was sent. So 0.5
|
|
is half speed and 2 is double speed.
|
|
* `uuid` (str) Unique UUID of the payment.
|
|
* `reply_address` (str) The pubkey of the lightning node that can receive payments for the sender. The node given
|
|
must be capable of receiving keysend payments. If this field contains an "@" symbol, it should
|
|
be interpreted as a
|
|
"[lightning address](https://github.com/andrerfneves/lightning-address/blob/master/README.md#tldr)".
|
|
* `reply_custom_key` (int) The custom key for routing a reply payment to the sender. This field should not be present
|
|
if it is not required for payment routing.
|
|
* `reply_custom_value` (str) The custom value for routing a reply payment to the sender. This field should not be
|
|
present if it is not required for payment routing.
|
|
* `remote_feed_guid` (str) Sometimes a payment will be sent to a feed's value block because a different feed referenced
|
|
it in a `<podcast:valueTimeSplit>` tag. When that happens, this field will contain the guid of the referencing feed.
|
|
* `remote_item_guid` (str) Sometimes a payment will be sent to an episode's value block because a different feed
|
|
referenced it in a `<podcast:valueTimeSplit>` tag. When that happens, this field will contain the guid of the
|
|
referencing feed's `<item>`.
|
|
|
|
<br>
|
|
|
|
## Reference Implementations
|
|
|
|
*
|
|
|
|
Breez: <https://github.com/breez/breezmobile/blob/4cf057c066d03c155964f0c4db43476cd70a57ab/lib/bloc/podcast_payments/podcast_payments_bloc.dart>
|
|
|
|
*
|
|
|
|
Podverse: <https://github.com/podverse/podverse-shared/blob/fff84c0143dea4fa01241109b8666d4c0b9a6ffc/src/satoshiStream.ts>
|
|
|
|
* PodStation: <https://github.com/podStation/podStation/pull/249>
|
|
* Helipad: <https://github.com/Podcastindex-org/helipad/blob/203e72dafb65e4f9e89540fbe4fc07a456a9907a/src/main.rs>
|
|
* Rust-V4V: <https://github.com/Conshax/Rust-V4V/blob/master/src/boostagram.rs>
|
|
|
|
## Examples & App Specifics
|
|
|
|
### Podverse
|
|
|
|
Podverse cuts the episode name short and `stream` actions are sent as `streaming`.
|
|
|
|
#### Boost
|
|
|
|
```json
|
|
{
|
|
"podcast": "Test Podcast Anchor",
|
|
"feedID": 6015671,
|
|
"episode": "this is a very very very very very very very very very very ",
|
|
"episode_guid": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"ts": 24,
|
|
"action": "boost",
|
|
"speed": "1",
|
|
"pubkey": "podverse-pubkey",
|
|
"value_msat_total": 100000,
|
|
"uuid": "75758d19-c4af-4da2-80ce-a5c84a0f1642",
|
|
"app_name": "Podverse",
|
|
"name": "Alby Test User PUT",
|
|
"sender_name": "Alwin_Conshax",
|
|
"message": "test"
|
|
}
|
|
```
|
|
|
|
#### Stream
|
|
|
|
```json
|
|
{
|
|
"podcast": "Test Podcast Anchor",
|
|
"feedID": 6015671,
|
|
"episode": "this is a very very very very very very very very very very ",
|
|
"episode_guid": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"ts": 315,
|
|
"action": "streaming",
|
|
"speed": "1",
|
|
"pubkey": "podverse-pubkey",
|
|
"value_msat_total": 100000,
|
|
"uuid": "654072b5-9825-4792-b502-31ee9f1ec6aa",
|
|
"app_name": "Podverse",
|
|
"name": "Alby Test User PUT",
|
|
"sender_name": "Alwin_Conshax"
|
|
}
|
|
```
|
|
|
|
### Castamatic
|
|
|
|
Castamatic sends `value_msat_total` only on boosts not on streams.
|
|
|
|
#### Boost
|
|
|
|
```json
|
|
{
|
|
"message": "test",
|
|
"app_name": "Castamatic",
|
|
"app_version": "8.6.0",
|
|
"value_msat_total": 100000,
|
|
"url": "https://anchor.fm/s/da6b03c0/podcast/rss",
|
|
"podcast": "Test Podcast Anchor",
|
|
"action": "boost",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"episode_guid": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"feedID": 6015671,
|
|
"value_msat": 95049,
|
|
"ts": 0,
|
|
"name": "Alby Test User PUT",
|
|
"sender_name": "Alwin_Conshax"
|
|
}
|
|
```
|
|
|
|
#### Stream
|
|
|
|
```json
|
|
{
|
|
"app_name": "Castamatic",
|
|
"app_version": "8.6.0",
|
|
"url": "https://anchor.fm/s/da6b03c0/podcast/rss",
|
|
"podcast": "Test Podcast Anchor",
|
|
"action": "stream",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"episode_guid": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"feedID": 6015671,
|
|
"value_msat": 50940,
|
|
"ts": 254,
|
|
"name": "Test User Peppe the first",
|
|
"sender_name": "Alwin_Conshax"
|
|
}
|
|
```
|
|
|
|
### Fountain
|
|
|
|
Fountain sends the itemID as a string, not an int.
|
|
|
|
#### Boost
|
|
|
|
```json
|
|
{
|
|
"app_name": "Fountain",
|
|
"value_msat_total": 100000,
|
|
"name": "Alby Test User PUT",
|
|
"podcast": "Test Podcast Anchor",
|
|
"feedID": 6015671,
|
|
"action": "boost",
|
|
"sender_id": "nSiq7id78JAdH9uY1pIy",
|
|
"sender_name": "@alwin_conshax",
|
|
"message": "test",
|
|
"itemID": "14934154309",
|
|
"boost_link": "https://fountain.fm/episode/14934154309",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"ts": 15,
|
|
"time": "00:00:15"
|
|
}
|
|
```
|
|
|
|
#### Stream
|
|
|
|
```json
|
|
{
|
|
"app_name": "Fountain",
|
|
"value_msat_total": 50000,
|
|
"name": "Alby Test User PUT",
|
|
"podcast": "Test Podcast Anchor",
|
|
"feedID": 6015671,
|
|
"action": "stream",
|
|
"sender_id": "pqwMfFdkCwtj8LKm0tNu",
|
|
"sender_name": "@moritz_conshax",
|
|
"message": null,
|
|
"itemID": "14934154309",
|
|
"boost_link": "https://fountain.fm/episode/14934154309",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"ts": 62,
|
|
"time": "00:01:02"
|
|
}
|
|
```
|
|
|
|
### Breez
|
|
|
|
Breez sends the guid on the itemID field and the sender_name is an empty string on streams.
|
|
Besides that Breez takes its fee from the total instead on top of the total as fountain does, this results in the
|
|
received value of a split being less than it should calculated from the total.
|
|
|
|
#### Boost
|
|
|
|
```json
|
|
{
|
|
"podcast": "Test Podcast Anchor",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"action": "boost",
|
|
"time": "00:00:24",
|
|
"feedID": 6015671,
|
|
"itemID": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"app_name": "Breez",
|
|
"value_msat_total": 100000,
|
|
"sender_name": "Jade Monkey",
|
|
"message": "test"
|
|
}
|
|
|
|
```
|
|
|
|
#### Stream
|
|
|
|
```json
|
|
{
|
|
"podcast": "Test Podcast Anchor",
|
|
"episode": "this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long episode name!!!",
|
|
"action": "stream",
|
|
"time": "00:02:37",
|
|
"feedID": 6015671,
|
|
"itemID": "12b4df54-af38-4c53-8099-82f9caacdcd5",
|
|
"app_name": "Breez",
|
|
"value_msat_total": 100000,
|
|
"sender_name": ""
|
|
}
|
|
```
|