tbDEX is a protocol for discovering liquidity and exchanging assets such as fiat money, real world goods, stablecoins or bitcoin. The tbDEX protocol utilizes Decentralized Identitifers (DIDs) and Verifiable Credentials (VCs) to establish the provenance of identity in the real world. The protocol has no opinion on anonymity as a feature or consequence of transactions. Instead, it allows willing counterparties to negotiate and establish the minimum information acceptable for an exchange.
This specification defines the message and resource formats that make up the tbDEX messaging protocol which is composed of two key concepts: Resources and Messages.
Version: Draft
Note
This specification will continue to be in a Draft state until there are two separate PFIs deployed and providing liquidity to individuals or other institutions
- Resources
- Messages
- Common Traits
- tbDEX conversation sequence
- Jargon Decoder
- Stored Balances
- Additional Resources
tbDEX Resources are published by PFIs and generally used as a part of the discovery process. They are not part of the message exchange, i.e Alice cannot reply to a Resource.
There exist both publicly available and protected resources. Publicly available ones can be listed by any caller without authorization. Protected resources may only be accessible to specific DIDs.
All tbdex resources are JSON objects which can include the following top-level properties:
Field | Required (Y/N) | Description |
---|---|---|
metadata |
Y | An object containing fields about the resource |
data |
Y | The actual resource content (e.g. an offering) |
signature |
Y | signature that verifies the authenticity and integrity of the resource |
The metadata
object contains fields about the resource and is present in every tbdex resource.
Field | Required (Y/N) | Description |
---|---|---|
from |
Y | The authors's DID |
kind |
Y | the data property's type. e.g. offering |
id |
Y | The resource's ID |
createdAt |
Y | ISO 8601 timestamp |
updatedAt |
N | ISO 8601 timestamp |
protocol |
Y | Version of the protocol in use (x.x format). The protocol version must remain consistent across messages in a given exchange. Messages sharing the same exchangeId MUST also have the same protocol version. Protocol versions are tracked in this repo |
The actual resource content. This will always be a JSON object. The Resource Kinds section specifies the content for each individual resource type
see here for more details
An Offering
is a resource created by a PFI to define requirements for a given currency pair offered for exchange.
PFI -> world: "Here are the currency pairs i have to offer. These are the constraints of my offer in terms of how much you can buy, what credentials I need from you, and what payment methods you can use to pay me the payin currency, and what payment methods I can use to pay you the payout currency."
field | data type | required | description |
---|---|---|---|
description |
string | Y | Brief description of what is being offered. |
payoutUnitsPerPayinUnit |
DecimalString |
Y | Number of payout units alice would get for 1 payin unit. An indicative rate. |
payin |
PayinDetails |
Y | Details and options associated to the payin currency |
payout |
PayoutDetails |
Y | Details and options associated to the payout currency |
requiredClaims |
PresentationDefinitionV2 |
N | Claim(s) required when submitting an RFQ for this offering. |
cancellation |
CancellationDetails |
Y | Details about PFI's cancellation policy |
field | data type | required | description |
---|---|---|---|
currencyCode |
string | Y | ISO 4217 currency code string |
min |
DecimalString |
N | Minimum amount of currency that the offer is valid for |
max |
DecimalString |
N | Maximum amount of currency that the offer is valid for |
methods |
PayinMethod[] |
Y | A list of payment methods to select from |
field | data type | required | description |
---|---|---|---|
currencyCode |
string | Y | ISO 4217 currency code string |
min |
DecimalString |
N | Minimum amount of currency that the offer is valid for |
max |
DecimalString |
N | Maximum amount of currency that the offer is valid for |
methods |
PayoutMethod[] |
Y | A list of payment methods to select from |
field | data type | required | description |
---|---|---|---|
kind |
string | Y | Unique string identifying a single kind of payment method i.e. (i.e. DEBIT_CARD , BITCOIN_ADDRESS , SQUARE_PAY ) |
name |
string | N | Payment Method name. Expected to be rendered on screen. |
description |
string | N | Blurb containing helpful information about the payment method. Expected to be rendered on screen. e.g. "segwit addresses only" |
group |
string | N | The category for which the given method belongs to e.g. Mobile Money vs. Direct Bank Deposit |
requiredPaymentDetails |
JSON Schema | N | A JSON Schema containing the fields that need to be collected in the RFQ's selected payment methods in order to use this payment method. |
fee |
DecimalString |
N | Fee charged to use this payment method. absence of this field implies that there is no additional fee associated to the respective payment method |
min |
DecimalString |
N | minimum amount required to use this payment method. |
max |
DecimalString |
N | maximum amount allowed when using this payment method. |
field | data type | required | description |
---|---|---|---|
enabled |
boolean | Y | Whether cancellation is enabled for this offering |
termsUrl |
URI |
N | A link to a page that describes the terms of cancellation |
terms |
string | N | A human-readable description of the terms of cancellation in plaintext |
Important
kind
should be a unique identifier of an individual payment method
Important
the presence of min
or max
in a payment method takes precedence over min
or max
defined at the PaymentDetails
level.
Important
If requiredPaymentDetails
is omitted, then any RFQs submitted for the respective offering must also omit paymentDetails
.
field | data type | required | description |
---|---|---|---|
kind |
string | Y | Unique string identifying a single kind of payment method i.e. (i.e. DEBIT_CARD , BITCOIN_ADDRESS , SQUARE_PAY ) |
estimatedSettlementTime |
uint | Y | estimated time taken to settle an order. expressed in seconds |
name |
string | N | Payment Method name. Expected to be rendered on screen. |
description |
string | N | Blurb containing helpful information about the payment method. Expected to be rendered on screen. e.g. "segwit addresses only" |
group |
string | N | The category for which the given method belongs to e.g. Mobile Money vs. Direct Bank Deposit |
requiredPaymentDetails |
JSON Schema | N | A JSON Schema containing the fields that need to be collected in the RFQ's selected payment methods in order to use this payment method. |
fee |
DecimalString |
N | Fee charged to use this payment method. absence of this field implies that there is no additional fee associated to the respective payment method |
min |
DecimalString |
N | minimum amount required to use this payment method. |
max |
DecimalString |
N | maximum amount allowed when using this payment method. |
Important
estimatedSettlementTime
is used to provide a rough estimate for the expected latency between receiving a payin and settling the respective payout.
In simpler terms: "how much time would you estimate between receiving my payin and the payout landing where I specified?"
Some payment methods should be consistent across PFIs and therefore have reserved kind
values. PFIs may provide, as a feature, stored balances, which are effectively the PFI custodying assets or funds on behalf of their customer. Customers can top up this balance and their transactions can draw against this balance.
kind | description |
---|---|
STORED_BALANCE |
Represents a customer's balance with the PFI. See Stored Balances for information on usage |
{
"metadata": {
"from": "did:ex:pfi",
"kind": "offering",
"id": "offering_01ha82y8d0fhstg95hhfjwmgxf",
"createdAt": "2023-09-13T20:15:22.528Z",
"protocol": "1.0"
},
"data": {
"description": "Selling BTC for USD",
"payoutUnitsPerPayinUnit": "0.00003826",
"payin": {
"currencyCode": "USD",
"methods": [
{
"kind": "DEBIT_CARD",
"requiredPaymentDetails": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"cardNumber": {
"type": "string",
"description": "The 16-digit debit card number",
"minLength": 16,
"maxLength": 16
},
"expiryDate": {
"type": "string",
"description": "The expiry date of the card in MM/YY format",
"pattern": "^(0[1-9]|1[0-2])\\/([0-9]{2})$"
},
"cardHolderName": {
"type": "string",
"description": "Name of the cardholder as it appears on the card"
},
"cvv": {
"type": "string",
"description": "The 3-digit CVV code",
"minLength": 3,
"maxLength": 3
}
},
"required": [
"cardNumber",
"expiryDate",
"cardHolderName",
"cvv"
],
"additionalProperties": false
}
}
]
},
"payout": {
"currencyCode": "BTC",
"max": "999526.11",
"methods": [
{
"kind": "BTC_ADDRESS",
"estimatedSettlementTime": 3600,
"requiredPaymentDetails": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"btcAddress": {
"type": "string",
"description": "your Bitcoin wallet address"
}
},
"required": [
"btcAddress"
],
"additionalProperties": false
}
}
]
},
"requiredClaims": {
"id": "7ce4004c-3c38-4853-968b-e411bafcd945",
"input_descriptors": [
{
"id": "bbdb9b7c-5754-4f46-b63b-590bada959e0",
"constraints": {
"fields": [
{
"path": [
"$.type"
],
"filter": {
"type": "string",
"const": "YoloCredential"
}
}
]
}
}
]
},
"cancellation": {
"enabled": true,
"termsUrl": "http://example.com/refund_policy"
}
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2syc1QyZUtvQWdUUTdzWjY3YTdmRDMzR21jYzZ1UXdaYmlxeWF5Rk1hYkhHI3o2TWtrMnNUMmVLb0FnVFE3c1o2N2E3ZkQzM0dtY2M2dVF3WmJpcXlheUZNYWJIRyJ9..9EBTL3VcajsQzSNOm8GElhcwvYcFGaRp24FTwmC845RCF84Md-ZB-CxdCo7kEjzsAY8OaB55XFSH_8K9vedhAw"
}
A Balance
is a protected resource used to communicate the amounts of each currency held by the PFI on behalf of its customer.
field | data type | required | description |
---|---|---|---|
currencyCode |
string | Y | ISO 4217 currency code string |
available |
DecimalString |
Y | The amount available to be transacted with |
{
"metadata": {
"from": "did:ex:pfi",
"kind": "balance",
"id": "balance_01ha82y8d0fhstg95hhfjwmgxf",
"createdAt": "2023-09-13T20:15:22.528Z",
"updatedAt": "2023-09-13T20:15:22.528Z",
"protocol": "1.0"
},
"data": {
/** ISO 4217 currency code or widely adopted cryptocurrency code as string */
"currencyCode": "USD",
/** same format used to represent currency values across messages */
"available": "400.00",
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2syc1QyZUtvQWdUUTdzWjY3YTdmRDMzR21jYzZ1UXdaYmlxeWF5Rk1hYkhHI3o2TWtrMnNUMmVLb0FnVFE3c1o2N2E3ZkQzM0dtY2M2dVF3WmJpcXlheUZNYWJIRyJ9..9EBTL3VcajsQzSNOm8GElhcwvYcFGaRp24FTwmC845RCF84Md-ZB-CxdCo7kEjzsAY8OaB55XFSH_8K9vedhAw"
}
A set of Verifiable Credentials issued to the PFI that can be consumed by any interested party in order to assess the reputability of the respective PFI.
Note
TODO: Fill out
Messages form exchanges between Alice and a PFI.
All tbdex messages are JSON objects which can include the following top-level properties:
Field | Required (Y/N) | Description |
---|---|---|
metadata |
Y | An object containing fields about the message |
data |
Y | The actual message content |
signature |
Y | signature that verifies the authenticity and integrity of the message |
privateData |
N | An ephemeral JSON object used to transmit sensitive data (e.g. PII) |
The metadata
object contains fields about the message and is present in every tbdex message.
Field | Required (Y/N) | Description |
---|---|---|
from |
Y | The sender's DID |
to |
Y | the recipient's DID |
kind |
Y | e.g. rfq , quote etc. This defines the data property's type |
id |
Y | The message's ID. See here for more details |
exchangeId |
Y | ID for a "exchange" of messages between Alice <-> PFI. Set by the first message in an exchange |
externalId |
N | Arbitrary ID for the caller to associate with the message. Different messages in the same exchange can have different IDs |
createdAt |
Y | ISO 8601 |
protocol |
Y | Version of the protocol in use (x.x format). The protocol version must remain consistent across messages in a given exchange. Messages sharing the same exchangeId MUST also have the same protocol version. Protocol versions are tracked under releases |
The actual message content. This will always be a JSON object. The Message Kinds section specifies the content for each individual message type
Message kinds other than RFQ may NOT have property privateData
. The RFQ privateData
section specifies the content of RFQ data
and privateData
.
{
"data": {
"offeringId": <OFFERING_ID>,
"claimsHash": <VERIFIABLE_CREDENTIAL_HASH>, <---- hash
"payin": {
"kind": "BTC_ADDRESS",
"amount": "STR_VALUE",
"paymentDetailsHash": <OBJ_HASH> <---- hash
},
"payout": {
"kind": "MOMO_MPESA",
"paymentDetailsHash": <OBJ_HASH> <---- hash
}
},
"privateData": {
"salt": "1234567890",
"claims": [<VERIFIABLE_CREDENTIAL>], <---- actual
"payinMethod": {
"paymentDetails": <OBJ> <---- actual
},
"payoutMethod": {
"paymentDetails": <OBJ> <---- actual
}
}
}
see here for more details
See here for more details.
See here for more details
Currency amounts have type DecimalString
, which is string containing a decimal amount of major currency units. The decimal separator is a period .
. The currency symbol must be omitted.
Alice -> PFI: "Give me a Quote against that Offering, and here is how much payin currency I want to trade for payout currency. Here are the credentials you're asking for, the payment method I want to pay in with, and the payment method I want you to pay out in."
field | data type | required | description |
---|---|---|---|
offeringId |
string | Y | Offering which Alice would like to get a quote for |
claimsHash |
string | N | Salted hash of the list the claims appearing in privateData.claims . Omit claimsHash to denote that the RFQ contains no claims, i.e. privateData.claims is omitted. |
payin |
SelectedPayinMethod |
Y | selected payin amount, method, and details |
payout |
SelectedPayoutMethod |
Y | selected payout method, and details |
field | data type | required | description |
---|---|---|---|
amount |
DecimalString |
Y | Amount of payin currency you want in exchange for payout currency |
kind |
string | Y | Type of payment method (i.e. DEBIT_CARD , BITCOIN_ADDRESS , SQUARE_PAY ) |
paymentDetailsHash |
string | N | A salted hash of privateData.payin.paymentDetails . Omit paymentDetailsHash when there are no payment details. |
field | data type | required | description |
---|---|---|---|
kind |
string | Y | Type of payment method (i.e. DEBIT_CARD , BITCOIN_ADDRESS , SQUARE_PAY ) |
paymentDetailsHash |
object | N | A salted hash of privateData.payout.paymentDetails |
Often times, an RFQ will contain PII or PCI data either within the claims
being presented or within paymentDetails
of payin
or payout
(e.g. card details, phone numbers, full names etc).
In order to prevent storing this sensitive data with the message itself, an RFQ may have property privateData
which holds the raw values for certain fields, while the RFQ's data
property holds salted hashes of the fields in privateData
. privateData
is NOT signed over, so the signature integrity of an RFQ can be verified even if privateData
is not present.
Each property in privateData
has a corresponding property in data
with the suffix Hash
. The salted hash of the property in privateData
MUST match the value of the corresponding property in data
. The following table enumerates all properties in privateData
which have a corresponding property in data
.
privateData property |
data property |
description |
---|---|---|
payin.paymentDetails |
payin.paymentDetailsHash |
The salted hash of payin.paymentDetails must match payin.paymentDetailsHash , unless payint.paymentDetailsHash is omitted. If payin.paymentDetailsHash is omitted, then payin.paymentDetails must also be omitted. |
payout.paymentDetails |
payout.paymentDetailsHash |
The salted hash of payout.paymentDetails must match payout.paymentDetailsHash , unless payout.paymentDetailsHash is omitted. If payout.paymentDetailsHash is omitted, then payout.paymentDetails must also be omitted. |
claims |
claimsHash |
The salted hash of claims MUST match claimsHash , unless claimsHash is omitted. If claimsHash is omitted, then claims must also be omitted. |
The salted hash is produced by creating a digest of a JSON array containing a salt and the cleartext value that appears in privateData
. For example, to produce Rfq.data.payin.paymentDetailsHash
, create a digest of [salt, Rfq.privateData.payin.paymentDetails]
. The RECOMMENDED minimum length of the randomly-generated portion of the salt is 128 bits. The salt is placed in privateData.salt
. The salt MUST be present when privateData
is present.
The privateData
field is ephemeral and MUST only be present when the message is initially sent to the intended recipient.
This table enumerates the structure of PrivateData
field | data type | required | description |
---|---|---|---|
salt |
string | Y | Randomly generated salt used for hashing PrivateData fields |
claims |
string[] | N | an array of claims that fulfill the requirements declared in an Offering. If the creator of the RFQ wants to include no claims, then claims MUST be omitted rather than being an empty list. |
payin |
PrivatePaymentDetails |
N | A container for the unhashed payin.paymentDetails |
payout |
PrivatePaymentDetails |
N | A container for the unhashed payout.paymentDetails |
field | data type | required | description |
---|---|---|---|
paymentDetails |
object | N | An object containing the properties defined in an Offering's requiredPaymentDetails json schema. If data.payin/payout.paymentDetailsHash is omitted, then privateData.payin/payout.paymentDetails respectively must also be omitted. |
{
"metadata": {
"from": "did:key:z6Mks4N5XdrE6VieJsgH8SMSRavmTox74RqoroW7bZzBLQBi",
"to": "did:ex:pfi",
"kind": "rfq",
"id": "rfq_01ha835rhefwmagsknrrhvaa0k",
"exchangeId": "rfq_01ha835rhefwmagsknrrhvaa0k",
"createdAt": "2023-09-13T20:19:28.430Z",
"protocol": "1.0"
},
"data": {
"offeringId": "abcd123",
"payin": {
"amount": "200.00",
"kind": "DEBIT_CARD",
"paymentDetailsHash": "<HASH_PRIVATE_PAYIN_METHOD_PAYMENT_DETAILS>"
},
"payout": {
"kind": "BTC_ADDRESS",
"paymentDetailsHash": "<HASH_PRIVATE_PAYOUT_METHOD_PAYMENT_DETAILS>"
},
"claimsHash": "<HASH_PRIVATE_CLAIMS_0>"
},
"privateData": {
"salt": "1234567890",
"payin": {
"paymentDetails": {
"cardNumber": "1234567890123456",
"expiryDate": "12/22",
"cardHolderName": "Ephraim Bartholomew Winthrop",
"cvv": "123"
}
},
"payout": {
"paymentDetails": {
"btcAddress": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
}
},
"claims": [
"eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtzNE41WGRyRTZWaWVKc2dIOFNNU1Jhdm1Ub3g3NFJxb3JvVzdiWnpCTFFCaSIsInN1YiI6ImRpZDprZXk6ejZNa3M0TjVYZHJFNlZpZUpzZ0g4U01TUmF2bVRveDc0UnFvcm9XN2JaekJMUUJpIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaWQiOiIxNjk0NjM2MzY4NDI5IiwidHlwZSI6IllvbG9DcmVkZW50aWFsIiwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rczRONVhkckU2VmllSnNnSDhTTVNSYXZtVG94NzRScW9yb1c3Ylp6QkxRQmkiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA5LTEzVDIwOjE5OjI4LjQyOVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa3M0TjVYZHJFNlZpZUpzZ0g4U01TUmF2bVRveDc0UnFvcm9XN2JaekJMUUJpIiwiYmVlcCI6ImJvb3AifX19.cejevPPHPGhajd1oP4nfLpxRMKm801BzPdqjm9pQikaMEnh1WZXrap2j_kALZN_PCUddNtW1R_YY18UoERRJBw"
]
},
"signature": "<SIGNATURE_OVER_METADATA_AND_DATA>"
}
PFI -> Alice: "OK, here's your Quote that describes how much payout currency you will receive based on your RFQ. Here's the total fee in payin currency associated with the payment methods you selected. This quote expires at X time."
field | data type | required | description |
---|---|---|---|
expiresAt |
datetime | Y | When this quote expires. Expressed as ISO8601 |
payoutUnitsPerPayinUnit |
string | Y | The exchange rate to convert from payin currency to payout currency. Expressed as an unrounded decimal string |
payin |
QuoteDetails |
Y | the amount of payin currency that the PFI will receive |
payout |
QuoteDetails |
Y | the amount of payout currency that Alice will receive |
field | data type | required | description |
---|---|---|---|
currencyCode |
string | Y | ISO 4217 currency code string |
subtotal |
DecimalString |
Y | The amount of currency paid for the exchange, excluding fees |
fee |
DecimalString |
N | The amount of currency paid in fees |
total |
DecimalString |
Y | The total amount of currency to be paid in or paid out. It is always a sum of subtotal and fee |
{
"metadata": {
"from": "did:ex:pfi",
"to": "did:key:z6MkoreC1URMQEVKm8wxzhVFntLzvPXYbtH8CEFNKUcSkLWT",
"exchangeId": "rfq_01ha83f661fs2avj6qgdhxpg28",
"kind": "quote",
"id": "quote_01ha83f663e3e88fshb06h6g78",
"createdAt": "2023-09-13T20:24:37.315Z",
"protocol": "1.0"
},
"data": {
"expiresAt": "2023-09-13T23:11:17.315Z",
"payoutUnitsPerPayinUnit": "16.654345",
"payin": {
"currencyCode": "USD",
"subtotal": "200.00",
"fee": "0.20",
"total": "200.20",
},
"payout": {
"currencyCode": "BTC",
"subtotal": "0.000016",
"total": "0.000016",
}
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29yZUMxVVJNUUVWS204d3h6aFZGbnRMenZQWFlidEg4Q0VGTktVY1NrTFdUI3o2TWtvcmVDMVVSTVFFVkttOHd4emhWRm50THp2UFhZYnRIOENFRk5LVWNTa0xXVCJ9..R_BBKJoWifPFh10GJ1ij2gCCxND1CdzKbiOgPCIha__0GvRy0rHYCi18-TY7jNARaQ94RHXHYIsCRm2MuOPACw"
}
Alice -> PFI: I'm happy with the quote and I want to execute the transaction"
{
"metadata": {
"from": "did:key:z6MkvUm6mZxpDsqvyPnpkeWS4fmXD2jJA3TDKq4fDkmR5BD7",
"to": "did:ex:pfi",
"exchangeId": "rfq_01ha83pkgnfxfv41gpa44ckkpz",
"kind": "order",
"id": "order_01ha83pkgsfk6t1kxg7q42s48j",
"createdAt": "2023-09-13T20:28:40.345Z",
"protocol": "1.0"
},
"data": {},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3ZVbTZtWnhwRHNxdnlQbnBrZVdTNGZtWEQyakpBM1RES3E0ZkRrbVI1QkQ3I3o2TWt2VW02bVp4cERzcXZ5UG5wa2VXUzRmbVhEMmpKQTNUREtxNGZEa21SNUJENyJ9..tWyGAiuUXFuVvq318Kdz-EJJgCPCWEMO9xVMZD9amjdwPS0p12fkaLwu1PSLxHoXPKSyIbPQnGGZayI_v7tPCA"
}
PFI -> Alice: "Here's how to pay us, and how to let us pay you."
field | data type | required | description |
---|---|---|---|
payin |
PaymentInstruction |
Y | Object that describes how to pay the PFI (e.g. BTC address, payment link) |
payout |
PaymentInstruction |
Y | Object that describes how be paid by the PFI (e.g. BTC address, payment link) |
field | data type | required | description |
---|---|---|---|
link |
String | N | Link to allow Alice to pay PFI, or be paid by the PFI |
instruction |
String | N | Instruction on how Alice can pay PFI, or how Alice can be paid by the PFI |
{
"metadata": {
"from": "did:ex:pfi",
"to": "did:key:z6MkoreC1URMQEVKm8wxzhVFntLzvPXYbtH8CEFNKUcSkLWT",
"exchangeId": "rfq_01ha83f661fs2avj6qgdhxpg28",
"kind": "orderinstructions",
"id": "orderinstructions_01ha83f663e3e88fshb06h6g78",
"createdAt": "2023-09-13T20:24:37.315Z",
"protocol": "1.0"
},
"data": {
"payin": {
"link": "https://example-pfi.com/payin?currency=usd&amount=200.20"
},
"payout": {
"instruction": "BTC will be paid to the provided BTC address"
}
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29yZUMxVVJNUUVWS204d3h6aFZGbnRMenZQWFlidEg4Q0VGTktVY1NrTFdUI3o2TWtvcmVDMVVSTVFFVkttOHd4emhWRm50THp2UFhZYnRIOENFRk5LVWNTa0xXVCJ9..R_BBKJoWifPFh10GJ1ij2gCCxND1CdzKbiOgPCIha__0GvRy0rHYCi18-TY7jNARaQ94RHXHYIsCRm2MuOPACw"
}
PFI -> Alice: "Here's the status of your order."
Note There can be many
OrderStatus
messages in a tbdex exchange. 1 for every status change that a PFI wants to inform Alice of.
field | data type | required | description |
---|---|---|---|
status |
Status |
Y | Current status of Order that's being executed |
details |
string | N | Additional details about the status |
Status value |
description |
---|---|
PAYIN_PENDING | PFI is awaiting payment from Alice. |
PAYIN_INITIATED | Payment from Alice has been initiated. |
PAYIN_SETTLED | Payment from Alice has been settled into PFI's account. |
PAYIN_FAILED | Payment from Alice failed. |
PAYIN_EXPIRED | Payment from Alice has not been received before Quote's expiresAt datetime. |
PAYOUT_PENDING | Payment to Alice is pending further processing. |
PAYOUT_INITIATED | Payment to Alice has been initiated. |
PAYOUT_SETTLED | Payment to Alice has been settled out of PFI's account. |
PAYOUT_FAILED | Payment to Alice failed. |
REFUND_PENDING | Refund of Alice's payin is pending further processing. |
REFUND_INITIATED | Refund of Alice's payin has been initiated. |
REFUND_FAILED | Refund of Alice's payin failed. |
REFUND_SETTLED | Refund of Alice's payin has settled out of PFI's account. |
{
"metadata": {
"from": "did:ex:pfi",
"to": "did:key:z6Mkgz7DnRNKVHdF8XavhU6gCcGKBbAUz3FwRjci4bsZQ5U5",
"exchangeId": "rfq_01ha83s5cmeecsm2qg7cvrc9hc",
"kind": "orderstatus",
"id": "orderstatus_01ha83s5crff3bmvq3t000cz91",
"createdAt": "2023-09-13T20:30:04.184Z",
"protocol": "1.0"
},
"data": {
"orderStatus": "COMPLETED"
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2d6N0RuUk5LVkhkRjhYYXZoVTZnQ2NHS0JiQVV6M0Z3UmpjaTRic1pRNVU1I3o2TWtnejdEblJOS1ZIZEY4WGF2aFU2Z0NjR0tCYkFVejNGd1JqY2k0YnNaUTVVNSJ9..RvOeT5cSurl3I5EO-wg3wVXaNd3mxHhjDUQQj1sNiHz-9u2dI3Hc0ALM-YA6fYLVW9N6XDlSpAAdQd7yZl7rDg"
}
PFI -> Alice: "Can't fulfill what you sent me for whatever reason (e.g. RFQ is erroneous, don't have enough liquidity etc.)" or "Your exchange is completed"
A Close
indicates a terminal state; no messages are valid after a Close
.
A Close
can be sent by the PFI at any point during the exchange, to indicate the PFI will not continue the exchange.
A Close
can ONLY be sent by PFI.
Field | Data Type | Required | Description |
---|---|---|---|
reason |
string | N | an explanation of why the exchange is being closed/completed |
success |
boolean | N | indicates whether or not the exchange successfully completed |
{
"metadata": {
"from": "did:ex:pfi",
"to": "did:key:z6MkvUm6mZxpDsqvyPnpkeWS4fmXD2jJA3TDKq4fDkmR5BD7",
"exchangeId": "rfq_01ha83pkgnfxfv41gpa44ckkpz",
"kind": "close",
"id": "close_03ha83trerk6t9tkg7q42s48j",
"createdAt": "2023-09-13T20:28:40.345Z",
"protocol": "1.0"
},
"data": {
"reason": "Rejecting RFQ",
"success": false
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3ZVbTZtWnhwRHNxdnlQbnBrZVdTNGZtWEQyakpBM1RES3E0ZkRrbVI1QkQ3I3o2TWt2VW02bVp4cERzcXZ5UG5wa2VXUzRmbVhEMmpKQTNUREtxNGZEa21SNUJENyJ9..tWyGAiuUXFuVvq318Kdz-EJJgCPCWEMO9xVMZD9amjdwPS0p12fkaLwu1PSLxHoXPKSyIbPQnGGZayI_v7tPCA"
}
Alice -> PFI: "I would like to back out of the exchange." or "I want a refund".
A Cancel
indicates that Alice does not wish to further propagate the exchange.
A Cancel
can be sent by Alice after sending an RFQ, after receiving a Quote, and after sending an Order.
A Cancel
can ONLY be sent by Alice.
If a Cancel
is sent by Alice after she submits an Order
, that means she would want a refund of her payin.
There is no guarantee of whether Cancel
will be honored by the PFI - it depends on their cancellation policy, which is outlined in their Offering
resource
field | data type | required | description |
---|---|---|---|
reason |
string | N | Reason for cancellation |
{
"metadata": {
"from": "did:ex:pfi",
"to": "did:key:z6Mkgz7DnRNKVHdF8XavhU6gCcGKBbAUz3FwRjci4bsZQ5U5",
"exchangeId": "rfq_01ha83s5cmeecsm2qg7cvrc9hc",
"kind": "cancel",
"id": "cancel_01ha83s5crff3bmvq3t000cz91",
"createdAt": "2023-09-13T20:30:04.184Z",
"protocol": "1.0"
},
"data": {
"reason": "wrong delivery address"
},
"signature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2d6N0RuUk5LVkhkRjhYYXZoVTZnQ2NHS0JiQVV6M0Z3UmpjaTRic1pRNVU1I3o2TWtnejdEblJOS1ZIZEY4WGF2aFU2Z0NjR0tCYkFVejNGd1JqY2k0YnNaUTVVNSJ9..RvOeT5cSurl3I5EO-wg3wVXaNd3mxHhjDUQQj1sNiHz-9u2dI3Hc0ALM-YA6fYLVW9N6XDlSpAAdQd7yZl7rDg"
}
Traits shared amongst both Resources and Messages are defined in this section
IDs are TypeIDs that are generated by the sender. The prefix for a given id MUST be the same as metadata.kind
e.g.
- Offering ID:
offering_7zzzzzzwzyf678004shr000zqf
- RFQ ID:
rfq_7zzzzzzzqke7nr003hxc001mey
Note
TODO: Discuss using digest as the ID as an alternative
A digest is a representation of data in a condensed form. When used in cryptographic contexts, this condensed form provides a way to verify the integrity and authenticity of data without having to compare the data in its entirety.
to construct the digest of any message or resource, perform the following steps:
- Initialize payload to be a json object that contains the
metadata
anddata
properties whose values are the respective metadata and data values of the message or resource for which the digest is being computed - JSON serialize payload using the JSON Canonicalization Scheme (JCS) as defined in RFC-8785
- compute the sha256 hash of the serialized payload
- base64url encode the hash without padding as defined in RFC-7515
-
Consistency: JSON does not guarantee property order. Two semantically identical JSON objects can have their properties serialized in different orders. This can lead to two different serialized strings for the same data. When performing cryptographic operations, such as creating a digital signature or a hash, even the slightest difference in input data results in a drastically different output. Therefore, a consistent, canonical form of the data is essential.
-
Interoperability: Different implementations and libraries serialize JSON differently. JCS ensures that different systems and libraries produce the same serialized output for the same input, thus facilitating interoperability.
-
Avoidance of Data Ambiguity: JSON allows for multiple valid representations of the same data (e.g., the use of whitespace, number representations). This can cause ambiguity in interpreting or processing such data, especially in cryptographic contexts where precision is paramount. JCS defines a single, unambiguous representation for any given JSON document.
-
Security: In the world of security, particularly with cryptographic signatures, the principle of "what you see is what you sign" is critical. If there's any ambiguity in the serialized data form, it can lead to potential vulnerabilities or issues. By ensuring a canonical format, JCS mitigates potential attack vectors related to JSON's flexibility.
-
Compatibility with Existing JSON Parsers: JCS is designed to work seamlessly with existing JSON parsers. This makes it relatively straightforward to integrate into systems already using JSON without requiring significant changes to the existing infrastructure.
- Widely Recognized and Adopted: SHA256, which is part of the SHA-2 (Secure Hash Algorithm 2) family, is widely recognized and adopted in various cryptographic applications and protocols. SHA256 is standardized by the National Institute of Standards and Technology (NIST) in the U.S. Being a standard means it has undergone extensive review and evaluation by experts in the field.
- Security: As of today, SHA256 has no known vulnerability to collision attacks, preimage attacks, or second preimage attacks.
- A collision attack is when two different inputs produce the same hash.
- A preimage attack is when, given a hash, an attacker finds an input that hashes to it.
- A second preimage attack is when, given an input and its hash, an attacker finds a different input that produces the same hash.
- Output Size: SHA256 provides a fixed hash output of 256 bits (32 bytes). This size strikes a balance between efficiency and security
When sending a SHA-256 hash (or any binary data) over the wire, it's common to use an encoding that translates the binary data into a set of characters that can be safely transmitted over systems that might not handle raw binary well.
Base64url-encoded data is safe for transmission over most protocols and systems since it only uses printable ASCII characters. It is widely supported across several programming languages.
Note A raw SHA256 hash is 32 bytes. When base64 encoded it becomes a 44 byte string
A message or resource signature is a detached compact JWS as defined in RFC-7515 (Detached content defined in appendix F).
Note
Why detached JWS? Detached signatures are employed to facilitate scenarios where the payload (i.e., the data being signed) is already available or transmitted separately. By using a detached signature, the original payload remains unaltered and can be transmitted or stored in its native format without being embedded in the signature itself. This approach avoids redundancy but also allows recipients to independently verify the integrity and authenticity of the payload using the detached signature and the signer's public key.
Signatures are computed using a private key whose public key is present as a verification method with an assertion method verification relationship when resolving the DID of the sender.
To construct the signature of a message or resource, perform the following steps:
- Construct the JWS Header as defined here in RFC7515. The header MUST include the following properties:
alg
kid
- the fully qualified assertion method ID
- Use the message or resource's digest as the JWS Payload
- Compute the JWS as defined here in RFC7515
- detach the payload as defined here in RFC7515
- set the value of the
signature
property of the respective message or resource to the resulting compact detached JWS
sequenceDiagram
actor Alice
actor PFI1
actor PFI2
actor PFIn
PFI1->>Alice: Offering
PFI2->>Alice: Offering
PFIn->>Alice: Offering
Alice->>PFI1: RFQ (Request For Quote)
PFI1->>Alice: Quote
Alice->>PFI1: Order
PFI1->>Alice: OrderInstructions
Alice->>PFI1: Alice completes payin per OrderInstructions
PFI1->>Alice: OrderStatus
PFI1->>Alice: OrderStatus
PFI1->>Alice: OrderStatus
PFI1->>Alice: Close
Quick explanation of terms used.
Term | Definition |
---|---|
PFI | Partipating Financial Institution: typically this is some kind of company that allows you to obtain a specified currency in exchange for another (e.g. BTC -> KES) |
payin | a method/technology used by the sender to transmit funds to the PFI. |
payout | a method/technology used by the PFI to transmit funds to the recipient. e.g. Mobile Money |
payout currency | currency that the PFI is paying out to Alice. Alice will receive the payout currency from the PFI. |
payin currency | currency the PFI will accept in exchange for the payin currency. The PFI will receive the payin currency from Alice. |
Stored balances are assets or funds custodied by a PFI on behalf of a customer. They are relevant for PFIs which provide options for institutional top ups, or which are custodial. Supporting stored balances is entirely optional. The intent is to provide the ability for PFIs who choose to do so.
If a PFI offers STORED_BALANCE
as a payout kind, they MUST necessarily have a respective offering withSTORED_BALANCE
as a payin kind.
A USD -> USDC institutional top-up offering could appear as:
{
"payinCurrency": "USD",
"payoutCurrency": "USDC",
"payinMethods": [{
"kind": "WIRE_TRANSFER"
}],
"payoutMethods": [{
"kind": "STORED_BALANCE"
}],
"requiredClaims": ["KnownBusinessCredential"]
}
A USD -> USDC retail customer offering could appear as:
{
"payinCurrency": "USD",
"payoutCurrency": "USDC",
"payinMethods": [{
"kind": "WIRE_TRANSFER"
}],
"payoutMethods": [{
"kind": "STORED_BALANCE"
}],
"requiredClaims": ["KnownCustomerCredential"]
}
A USDC -> KES off ramp offering could appear as:
{
"payinCurrency": "USDC",
"payoutCurrency": "KES",
"payinMethods": [{
"kind": "STORED_BALANCE"
}],
"payoutMethods": [{
"kind": "MOMO_MPESA"
}],
"requiredClaims": ["SanctionsCredential"]
}
Resource Name | Description |
---|---|
CODEOWNERS | Outlines the project lead(s) |
CODE_OF_CONDUCT.md | Expected behavior for project contributors, promoting a welcoming environment |
CONTRIBUTING.md | Developer guide to build, test, run, access CI, chat, discuss, file issues |
GOVERNANCE.md | Project governance |
LICENSE | Apache License, Version 2.0 |
tbDEX whitepaper | tbDEX whitepaper |