We discuss possible solutions on the source of CKBytes for storing UDT / DOB in the context of B2C / C2C scenarios.
Instant Convert
Instant convert (service) is used in a payment / transfer workflow. A user is expected to pay UDT(e.g., USDI) to a particular third party instant convert service. In exchange, the third part service provides matching amount of CKBytes to the user, so a CKB transaction can be fulfilled. In a sense, a user can focus on their UDT, ignoring requirements for CKBytes with the help of the instant convert service.
Notice that the name “instant convert” is deliberately chosen. Some call a similar setup as flash swap, we intentionally use a different name for several reasons:
- Flash swap is commonly used to refer to a smart contract in Ethereum. I fear that this might lead to wrong assumptions that only a smart contract can do the job. As we will see here, instant convert can simply be implemented as an off-chain module, while a more sophisticated one can choose to introduce a CKB script. Either way, the CKB script will be an optional enhancement, not a required component.
 - Binance has a 
convertfeature, which is actually the inspiration of our design here. 
To avoid confusion, we will precisely define the full workflow of the instant convert service.
Assuming a user (possibly with the help of a dapp) builds the following, incomplete CKB transaction:
One interpretation of the CKB transaction, is that a user buys a DOB with 100 USDI. The transaction is incomiplete because 288 CKBytes are required for the 2 output cell, only 144 CKBytese are available from input cells.
A user can send this incompleted transaction to an instant convert service, with additional information optionally attached:
- The specific UDT type a user is willing to pay for required CKBytes. This is simply for current example, since the incomplete transaction only uses USDI. For more complicated cases, multiple UDT types might coexist in a single incomplete transaction. For the moment, we will assume that users will pick one type of UDT to pay.
 - The worse price a user is willing to pay for UDT-CKB trading pair, or the maximum UDT a user is willing to pay per CKByte. There are multiple options to obtain this information:
- Like a AMM swap, a user specifies the worst-case price, and instant convert service aims to find the best price satisfying this worse-case scenario.
 - The instant conver service first gives a quote by supplementing cells making the incomplete transaction complete(we shall see in a second how this works), the user can choose to accept the quote via a signature, or reject the quote by discarding the complete transaction provided by the instant convert service.
 
 
Given the incomplete transaction, the instant convert service can add more input / output cells, the missing CKBytes will be supplement by input cells from the instant convert service. Certain output cells provided by the users will be modified by the instant convert service, so as to deduct the UDTs used to pay for the CKBytes. A complete CKB transaction will then be formed:
Cells with solid lines come from the original incomplete transaction sent by the user(bold text denotes modification part), while cells with dashed lines are inserted by the instant convert service. Here the instant convert service is willing to trade the user 144 CKBytes at a total price of 0.5 USDI. 2 input cells added by instant convert service to provide CKBytes, 2 outputs cells are also added by the instant convert service: one collects the traded USDI, one provides and collects all changed CKBytes. A more optimal service can definitely combine the 2 separate cells into a single one.
The instant convert service would sign signatures required by all input cells provided itself. A user, after receiving this complete transaction, can choose to sign his / her own input cells, assuming he / she accepts the price. The user can then send the fully signed transaction to CKB network, when committed, an instant convert action is completed.
The instant convert service can be seen as a general service suitable for many different cases. It’s just that some cases urgently need it, while others are less dependent on it.
There are variants to certain parts of the full workflow above:
Instant Convert Discussion 1: transaction fees
Transaction fees required by CKB are completed neglected in the above discussion. 2 solutions exist for it:
- A user can provide transaction fees in his / her incomplete transaction
 - The instant convert service can provide the transaction fees by trading slightly more CKBytes for the user. In a way, one can see this as a user only pays transaction fees with UDT.
 
Either one suits the need, personally I feel the second one is more natural.
Instant Convert Discussion 2: parallelism in instant convert service
There is an issue of parallelism in the above describe workflow. The input cells added to a transaction by the instant convert service will effectively be “locked”, and remain unusable for a while till the transaction is commited on chain. If we don’t lock those input cells, 2 users might retrieve the same input cell in 2 separate transactions from the same instant convert service. Only one of those 2 transactions can land on chain, the other will end up with a double-spent error. There are several mitigations to this problem:
- In the most naive solution, a batch of cells can be provided by the instant convert service. Assuming that 3000 cells are prepared by the instant convert service, and that a user has 30 seconds after receiving to sign and submit the transaction to CKB’s P2P network(instant convert service can listen for potential transactions in the P2P network even before the transaction is commited on chain), an instant convert service will be able to process 100 convert actions per second.
 - We can move one step forward if we can remove the assumption that a user submits the transaction: the instant convert service only returns the completed, but unsigned transaction to a user. When a user accepts the quote, the user sends signature back to the instant convert service, only at this time, the instant convert service signs its own input cells and submit the fully signed transaction to CKB’s network. In this design, an instant convert service can track submitted transactions, and further compress the locked time for a cell: assuming transaction X consumes input cell A, generate output cell B, when user 1 returns the signature for transaction X, the instant service can immediately consume cell B in transaction Y, without waiting for transaction X to be committed on chain. Given the same amount of cells, the instant convert service will be able to process more requests per second. However this solution has 2 caveats:
- Censorship might be a concern to an extent, since the instant convert service submits a transaction to CKB.
 - A user might receive a transaction with input cells that are unknown(since the depended transaction might not be committed to CKB yet) at the time. There are mitigations(we can send a chain of transactions to the user), but it certainly requires work at the wallet side.
 
 - Another assumption of the above workflow, is that a cell from the instant convert service, can be used in one instant convert action at once. In fact, a cell from the instant convert service might process enough CKBytes(e.g., 10000 CKBytes) for multiple instant convert actions at once. Assuming certain lock scripts might be used by users of the instant convert service, open transactions can be introduced, so a user submits a signed open transaction rather than an incomplete transaction. The instant convert service can batch multiple open transactions together and process their instant convert requests together. This will be a much more performant solution. But it also has its own caveats:
- Similar to option 2, open transactions also have concerns of censorship.
 - Open transactions would require users to use a particular lock in advance. Work at wallet side is also likely required.
 
 - A less popular solution also exists: instead of being a service that takes user requests, an instant convert service can simply prepares a series of cells using special lock scripts. The lock scripts can be unlocked when instant convert conditions are met(users have provided enough UDTs for the traded CKBytes). This way instant convert actions will be converted to a series of actions doable by the users alone: locating cells on chain, preparing transactions, signing and submitting to CKB. There are certain caveats in this design as well:
- A tradeoff between the number of cells and parallelism;
 - An SDK shall be used by most users, so users pick cells in a randomized fashion. Otherwise double-spent issues might be quite common;
*. On chain cells will not have a way to know latest price between UDT and CKBytes. So likely an oracle or other mechanisms be required to determine the price, this might further complicate the workflow. 
 
For now, we have built an implementation that:
- Handles transaction fee at the instant service level
 - Chooses option 2 for the parallelism issue
 
I do want to mention that different solutions might exist for different scenarios. And there would well be other solutions fitting different use cases. Just to take an analogy: it is useless debating whether apple or orange is more tasteful. Our current implementation only picks one of the possible designs, we hope it can bring inspiration to more solutions.
Later we shall explain more details on our first implementation. For now, let’s recap on different payment options.
Payment Options for Different Use Cases
One observation is that there is unlikely a one-size-for-all UDT payment solution for CKB, this is due to CKB’s own very nature. Different solutions might exist and suit different use cases. Here let’s do a recap of UDT payment options for different use cases, with instant convert service in mind.
B2C
When a merchant is involved, the priority should be performance, a UDT payment solution for merchants using CKB, should aim at higher level of parallelism. Since machines will handle most of the action at one end, user experience will be less of a concern.
UDT payment with invoice
Users typicall use one UDT A(e.g., USDI) to buy UDT B, DOB, or other off-chain services. CKBytes will be required to store UDT B, DOB, cells representing off-chain services, and also changes of UDT A.
For one thing, instant convert service can definitely be used. Users can just pay slightly more UDT A, in exchange for all CKByte requirement in the complete transaction.
In real world, merchants have already utilized the concept of invoices extensively. We can introduce invoice to CKB world, where each invoice is likely to contain a merchant side signature, and we can then implement the full workflow:
- For DOB and other off-chain services, the purchased good can first be put in a cell with a special lock script. When user requires a purchase operatioin, an invoice will be generated by merchant, the user can construct an transaction with the cell containing the good, the invoice might contain signatures used to unlock the cell containing the good. This particular cell can be designed with slightly more CKBytes to supplement user-side requirement.
- Since each individual good can be wrapped in different cells, there is less likely to be parallelism issue. In a real world, when a purchase action is initiated, the merchant is also less likely to generate invoice for the same good for another user till a timeout period is reached.
 
 - For UDTs, there can also be an invoice. The merchant can prepare in advance 1000 cells for example. Those cells will also utilize special locks. When a purchase action is initiated, the merchant also generates an invoice, the invoice can be utilized by users to unlock one of those 1000 cells(could be decided by the merchant, or could be randomly picked by the user), the cell can then be used to provide enough CKBytes for users of such a transaction to use.
- In a way, one can think that the merchant also handles part of instant convert service’s job via invoices.
 
 
UDT payment with open transaction
Similar to previous discussion, if we can assume certain locks will be used by users. Open transactions can also be used as a purchase action for more performant processing. That being said, censorship and the assumption of lock scripts will still be a concern.
Invoice vs. open transaction
In a sense, invoice and open transaction are quite alike, sometimes even the same thing from an implementation perspective. Here we distinguish the 2 names in 2 different scenarios:
- When a user creates an open transaction, we call it open transaction directly
 - When a merchant creates an open transaction, we call it an invoice
 
C2C
Between users, the top priority should always be user experience. We should prioritize solutions that ensure that when a user signs a transaction, the action will most likely to be succeeded. Unlike machines which can dumply sign a lot of signatures, repeated signing should be avoided at all costs.
Performance and fees are less of a concern between users.
DOB
Transferring of DOBs is quite simple, we can even see it as a solved problem: we can consider the required CKBytes to be part of the DOB. When transferring a DOB, one transfers the DOB and all the CKBytes together to the recipient. More than necessary CKBytes can be included in the initial creation phase: 1 more CKByte is typically enough for many, many transfer operations.
UDT
The transferring of UDTs between users has always been the biggest headache in CKB. Here we provide 2 solutions:
- When possible, the instant convert service provides the simplest solution: when Alice transfers USDI to Bob, Alice should consult an instant convert service to trade enough CKBytes by paying USDI.
 - Open transaction remains another option, but it requires some strong assumptions:
- Alice should use lock scripts that support open transaction
 - Bob should have enough CKBytes as recipent
 - Bob should commit the open transaction at receiving it.
 
 - Maybe fiber provides a better option
 
Summary
In summary:
- Instant convert service fits all scenarios
 - Merchants can introduce invoices. It’s very likely merchants have invoices already.
 - When it’s possible to assume lock scirpts, open transaction can be viable and performant solution.
 - Transferring of UDTs between users has always been the biggest headache, we might want to just use instant convert service, or rely on fiber.
 - Transferring of DOBs between users is a solved problem. CKBytes in this case are just part of the DOB.
 
ckb-udt-convert-serviceensure
We have implemented an instant convert service. As mentioned above, there might be multiple different implementations of the instant convert service, each with different design priorities suiting different purposes. What we have here, is simply one of the many possible choices.
There are 3 parties for the instant convert service:
- Convert service: our constructed instant convert service server
 - App: a wallet, or an app that will communicate with the instant convert service
 - User: actual (possibly) human user of the app or the wallet.
 
Public API
ckb-udt-convert-service exposes a JSONRPC 2.0 compliant API server. 2 APIs: initiate and confirm are provided for instant convert actions.
HTTP POST method must be used for the JSONRPC server.
Initiate
App side creates an instant convert action via initiate API. The request payload looks like following:
{
  "id": 42,
  "jsonrpc": "2.0",
  "method": "initiate",
  "params": [
    {
      // This JSON object contains a (incomplete) CKB transaction
      "cell_deps": ...,
      "header_deps": ...,
      "inputs": ...,
      "outputs": ...,
      "outputs_data": ...,
      "version": ...,
      "witnesses": ...,
    },
    [1, 3]
  ]
}
2 required parameters are used by the initiate JSONRPC reuqest:
- An incomplete transaction created from user actions by the app side. The CKBytes from outputs cells will exceed the CKBytes from input cells in this transaction.
 - An array of numeric indices. This parameters marks the output cells from the incompleted transaction, which contains UDTs that can be paid for CKBytes.
 
PS: in our first iteration, the instant convert service only provides CKBytes, and charge a particular predefined UDT types. Later we might expand this service for more scenarios:
- The 3rd, 4th parameters can be provided in the form of CKB scirpts, containing the UDT to pay and UDT to trade for
 - When a CKB script has full-zero 
code hash,dataashash_type, and an emptyargs, we can consider that the UDT speicifed is actually CKBYtes. 
Theorecitally, an instant convert service with those additional parameters can be used to trade between any 2 token types. But it remains a question if we should need such complexity.
The instant convert service will perform the following actions for an initiate request:
- Calculate the required CKBytes, including the CKBytes required by output cells, and the CKBytes required for transaction fees.
 - An external service(such as Binance API) can be consulted for the current price of UDT against CKB. The instant convert service might adjust the price with some incentive percent for itself, and then calculate the final UDTs to charge from the user for the required CKBytes.
- When a user does not provide enough UDTs to charge in the incomplete transaction, the instant convert service would return with an error.
 
 - The instant convert service finds a usable live cell (named fund cell) in its fund pool. In our current implementation, all fund cells provide CKBytes, while at the same time, collects UDTs.
- In the extreme case, an instant convert service might not have an available fund cell to serve the request. In this sense, the instant convert service should return with an error. At the same time, it should also tries to notify ops team of the instant convert service for intervention.
 
 - The instant convert service then fulfill the incomplete transation with the fund cell into a complete transaction. It also modifies output cells from the original incomplete transaction, charging required UDTs, and collect those UDTs in its own output fund cell.
 
An API response like following will be returned by the instant convert service to the app side:
{
  "id": 42,
  "jsonrpc": "2.0",
  "result": {
    "valid_until": "2025-07-24T01:42:58.100Z",
    "transaction": {
      // This JSON object contains a complete, unsigned CKB transaction
      "cell_deps": ...,
      "header_deps": ...,
      "inputs": ...,
      "outputs": ...,
      "outputs_data": ...,
      "version": ...,
      "witnesses": ...,
    },
    "ask_tokens": "0x123124",
    "bid_tokens": "0xaaffd132"
  }
}
In addition to the complete transaction(unsigned) to present to the user and sign, some more data are also returned:
valid_untilcontains an ISO8601 formatted string, containing the expiration time of the instant convert service. Each instant convert action has an expiration time due to 2 reasons:- Prices quoted by the instant convert service is not always valid. Prices fluctuate, a quote is only valid for some amount of time.
 - Due to parallelism concerns, locked fund cells are also valid only for some amount of time, before they will be available to use by others.
 
ask_tokensandbid_tokensare returned as a convenience so users can learn about the quote price directly. App side can also choose to deduct the quote price from the returned transaction directly. Assuming the instant convert service chargesUSDI, providesCKB.ask_tokenswill contain the chargedUSDIamount, whilebid_tokenswill contain the provided CKBytes.
Confirm
When app side receives reply from the initiate request, it should present the whole transaction for the user to confirm. When confirmed, the app side should sign input cells it inserted for the users. The signatures should be put in the designated place in the witnesses field. The app side then send the complete, partly-signed transaction via confirm request to the instant convert service. The request payload looks like following:
{
  "id": 42,
  "jsonrpc": "2.0",
  "method": "initiate",
  "params": [
    {
      // This JSON object contains a complete, user-signed CKB transaction
      "cell_deps": ...,
      "header_deps": ...,
      "inputs": ...,
      "outputs": ...,
      "outputs_data": ...,
      "version": ...,
      "witnesses": ...,
    }
  ]
}
The instant convert service performs the following tasks for the confirm request:
- The instant convert service first checks if the app side or the user unexpectedly changed anything from the transaction. Basically, the app side should only be able to modify witness field to insert signatures after 
initiaterequest. If any modifications are detected, errors will be returned, and the processing ends. - The instant convert service should check if the instant convert action times out. If so, errors will be returned, and the processing ends.
 - The instant convert service signs its own input cells, and send the complete, signed transaction to CKB network.
 
After processing, the instant convert service sends the complete, signed transaction back to the app side:
{
  "id": 42,
  "jsonrpc": "2.0",
  "result": {
    "transaction": {
      // This JSON object contains a complete, signed CKB transaction
      "cell_deps": ...,
      "header_deps": ...,
      "inputs": ...,
      "outputs": ...,
      "outputs_data": ...,
      "version": ...,
      "witnesses": ...,
    }
  }
}
The instant convert service will send the transaction to CKB. But for extra security and faster broadcast, the app side might also choose also to send the transaction to CKB network.
Public facing vs ACL
Theorecially, the instant convert service can be a public facing service. But essentially,  we are talking about a 2-phase-commit which will lock consumable resource in the first step. A DDoS attack might be initiated on the instant convert service, where a lot of initiate requests are sent, but no confirm requests are sent. This way the instant convert service might continously be in a state that it has enough fund cells but no usable fund cells. So for the time being, our current instant convert service implementation is more safe behind an ACL, and only be invoked by authroized parties.
Internals
Here we discuss some internals of our instant convert service
Fund cells / pool
Our instant convert service will maintain a series of fund cells in the fund pool. Each fund cell is a cell holding a particular UDT type(USDI in our example), and more than enough CKBytes. At initial creation, each fund cell will hold 0 UDT. When a fund cell is used in an instant convert service, it would pay some CKBytes, and collects certain amount of UDTs. At certain time, a fund cell might hold so few CKBytes that it cannot fulfill an instant convert action, such fund cells with insufficent CKBytes will be named insufficient cells. To ensure that our instant convert service have enough CKBytes to serve requests, ops team of the instant convert service are expectedly to send CKBytes to the fund pool, this is done by creating empty CKB cells(meaning CKB cells with no type script and empty data) using the same lock script as fund cells in the ful pool. Periodically, our instant convert service will colllect insufficient cells and empty cells with only CKBytes, and use those cells to recreate new, usable fund cells. In the same process, any UDTs from insufficent cells will be moved to designated collecting addresses, for ops team to trade them back to CKBytes in Binance or other services.
Deployment
Our current instant convert service is a simple node.js server using Redis as a dependency. The deployment of the instant convert service, is actually quite simple. We will provide deployment and running steps in the README part of the repository when it has matured.
A Real Running Example
We have run the instant convert service in testnet for real with this CKB address. We can analyze the running details of our instant convert service by following the transactions in chronological order:
- The ops team of the instant convert service deposits 1000 CKB into the fund pool in 0x9b1fa219ac3c7bb99b8f0622d6ef21df0b6bc28392bf1bbb9da64cfc49fd1274
 - The instant convert service server automatically detects the 1000 CKB deposit as a new empty CKB cell in the fund pool. In 0x5dfccff6afc9f2d6c1add08ed51c04fb2be0c2dd904b9212759e215ca5654c80 it converts the empty CKB cell with 1000 CKB into 2 fund cells with 500 CKB(a tunnable parameter in deployment configuration) each. Due to paying transaction fees, one fund cell has slightly less than 500 CKB.
 - We trigger 4 instant convert actions at user side in: 0xd4dd72613d33fea413244e8800e196a667f56c4096b39c9dc35c4dd0276435fe, 0xb30bea6f310c2d172f1099392adf58d0855168e6c422f5b6ab9609b1fc0607c2, 0x8180361deeb8f4a27ec720126a97e353d28b8d47061639c859a7485c74d0af36, 0x85119455b9c8b6dd6e5e86e88140c23242a51cba946efa82b7626d177df671d8
- The threshold between fund cells and insufficent cells is also a tunnable paramter in deployment configuration. In our current setup, any fund cell with less than 300 CKB becomes insufficent cell. So at this point, our instant convert service has run out of fund cells. Any new 
initiaterequest will result in a instant faliure. 
 - The threshold between fund cells and insufficent cells is also a tunnable paramter in deployment configuration. In our current setup, any fund cell with less than 300 CKB becomes insufficent cell. So at this point, our instant convert service has run out of fund cells. Any new 
 - The ops team of the instant convert service deposits 212 CKB into the fun pool in: 0x0f7685b3a1d8ed5fc8befba0cc3b7f2da270bac3f6160742db2351fd43bbe93b
 - The instant convert service server automatically detects that it has 3 cells: 2 insufficent cells each with ~215 CKB, a new empty CKB cell with 212 CKB. With those 3 cells it decides that it can create a new fun cell, so 0x7e31b0de06bdf6ad5b477f2c5cff164a527d9bd787bc4ccdddd88307ebf69119 is created, so a new fund cell is created for further instant convert actions. Another cell is also created so all collected USDIs are sent to collecting address.
 - Now we can have the user side trigger one new instant convert action: 0xbf7703ff05e60fb6b388f43d3995ae2a76b17079b3106d94a39b006205128830
 
For now we only collect UDTs such as USDIs in insufficent cells to collecting address. This way we maximize the usefulness of all fund cells. An off-line tasks might be created in the future, so we can run a cronjob at a time that people are less likely to use the instant convert service(like 3 am on Sunday), the cronjob randomly picks 30% of all fund cells, and build a transaction to collect all USDIs inside.

