Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Welcome to the Unlockd SDK documentation.
Unlockd is a permissionless protocol designed to facilitate secure and efficient borrowing and lending against tokenized Real-World Assets (RWAs) and financial assets. Our platform leverages advanced AI to provide the safest and most cost-effective way for users to obtain instant loans and earn yield.
Unlockd is a permissionless liquidity protocol that enables users to act as depositors or borrowers. Built on Ethereum and Polygon and expanding to multiple Layer 2 networks, Unlockd employs a Peer-to-Pool model, offering a more efficient and fair system compared to traditional Peer-to-Peer models.
Permissionless Borrowing: Users can borrow against their tokenized assets without needing permission, benefiting from instant loans and fair interest rates, using just a wallet.
Efficient Yield Generation: Lenders can contribute liquidity to pools and receive auto-compounding yields in USDC.
AI-Driven Risk Engine: Our advanced AI ensures accurate asset valuations and optimal, market-adjusting loan conditions tailored to individual needs.
Unlockd simplifies the process of leveraging tokenized assets for financial operations. By providing a seamless, secure, and efficient platform for borrowing and lending, Unlockd empowers users to unlock the full potential of their digital and real-world assets in a permissionless way.
Developers: Integrate Unlockd’s functionalities into applications, enhancing them with advanced borrowing and lending features.
Lenders: Earn stable, auto-compounding yields by contributing liquidity to Unlockd’s pools.
Borrowers: Access instant loans against tokenized assets without complex verification processes.
Explore the rest of the documentation to learn more about how Unlockd works and how you can start leveraging our powerful SDK to integrate these capabilities into your projects.
The Unlockd SDK is a powerful tool designed to enable seamless integration of blockchain-based borrowing functionalities into your applications. It allows developers to leverage the Unlockd protocol, providing users with the ability to collateralize their assets and obtain loans in various cryptocurrencies against tokenized Real-World Assets (RWAs).
Permissionless Integration: The Unlockd SDK offers a simple and intuitive interface for integrating our permissionless protocol into your applications.
Comprehensive Asset Management: Easily manage user assets, including collateralization and loan origination processes.
AI-Powered Valuations: Utilize advanced AI to ensure accurate asset valuations and optimal loan conditions tailored to individual needs.
Full Custody & Ownership: Borrowers maintain full custody and ownership of their collateralized assets through their Unlockd Account.
Proven Security: The protocol adheres to rigorous security standards and has been audited by multiple firms to ensure the highest level of safety.
The Unlockd SDK is particularly beneficial for partners whose assets are already supported and whitelisted within the Unlockd protocol. Here are some key use cases:
Native Borrowing Integration: Implement borrowing against tokenized assets directly within your product interface, offering users seamless access to liquidity.
Enhanced Marketplaces: For marketplaces or platforms allowing the purchase of assets, offer financing options at the point of purchase, enabling a Buy Now, Pay Later (BNPL) experience for your users.
Custom Financial Solutions: Develop custom financial solutions that leverage Unlockd's RWA-backed borrowing capabilities to provide innovative services to your user base.
By integrating the Unlockd SDK, you can unlock new financial possibilities for your users, providing them with secure and efficient access to liquidity backed by tokenized Real-World Assets.
Explore the rest of the documentation to learn more about how to integrate the Unlockd SDK and start leveraging these powerful functionalities in your applications.
Proven Security: The protocol adheres to rigorous security standards and has been audited by multiple firms to ensure the highest level of safety.
This section includes some code examples that demonstrate how to use the Unlockd SDK in different scenarios.
$ npm install @verislabs/unlockd-sdk$ bower install @verislabs/unlockd-sdk$ yarn add @verislabs/unlockd-sdkimport { UnlockdSDK } from '@unlockd/sdk';
const unlockd = new UnlockdSDK();
async function borrowAssets() {
try {
const token = await unlockd.authenticate();
const amount = 1000000000000000000; // 1 ETH in wei
const assets = [
{
collection: '0x1234567890123456789012345678901234567890',
tokenId: '1',
},
];
const signature = {
v: 28,
r: '0x1234567890123456789012345678901234567890123456789012345678901234',
s: '0x1234567890123456789012345678901234567890123456789012345678901234',
deadline: 1649327432,
};
const result = await unlockd.action.borrow(token, amount, assets, signature);
console.log('Borrow successful:', result);
} catch (error) {
console.error('Error:', error);
}
}
borrowAssets();import { UnlockdApi} from "@unlockdfinance/unlockd-ts";
const api= new UnlockdApi()
const address='0x0000000000000000000000000000000000000000'
const message = await api.signatureMessage(address)
//Sign message from your wallet o with Pk
//...
const signedMessage='...'
const authToken = await api.validateMessage(signedMessage)import {liquidationPrice} from "@unlockdfinance/unlockd-ts";
const oneEth = BigInt(1e18)
const params = {
debt: (BigInt(50) * oneEth),
liquidationThreshold: BigInt(8500)
}
const result = liquidationPrice(params)import {ActionRequest, UnlockdApi} from "@unlockdfinance/unlockd-ts";
const api= new UnlockdApi()
const params: ActionRequest = {
loanId: '0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0',//Optional
nfts: [{collection: '0x1234567890abcdefABCDEF1234567890abcdefAB', tokenId: 'testTokenId'}]//Optional
}
const authToken = await api.borrowSignature(authToken, params)To buy an asset instantly, use the buy function from the buyNow module:
Refer to the Buy Now Module documentation for more details on the buy function.

$ pnpm add @verislabs/unlockd-sdkexport type Signature = {
data: object
signature: { v: number; r: string; s: string; deadline: number }
}
/* data object
interface SignBuyNow {
asset: SignAsset;
assetLtv: BigInt;
assetLiquidationThreshold: BigInt;
from: string;
to: string;
data: string;
value: BigInt;
marketAdapter: string;
marketApproval: string;
marketPrice: BigInt;
underlyingAsset: string;
nonce: BigInt;
deadline: BigInt;
}
interface SignLoanConfig {
loanId: string;
aggLoanPrice: BigInt;
aggLtv: BigInt;
aggLiquidationThreshold: BigInt;
totalAssets: BigInt;
nonce: BigInt;
deadline: BigInt;
}
*/export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const buy = async (
amount: string,
signature: Signature,
options?: ClientOptions
)const result = await buy(amount, signature, options?);The Auction module provides functionality for participating in auctions for NFT collateral.
To place a bid in an auction, use the bid function from the auction module:
To redeem an auction, use the redeem function from the auction module:
To finalize an auction, use the finalize function from the auction module:
Refer to the Auction Module documentation for more details on the bid, redeem andfinalizefunctions.
The Action module allows you to perform actions such as borrowing assets and repaying loans.
To borrow assets using the SDK, use the borrow function from the action module:
To repay a borrowed loan, use the repay function from the action module:
Refer to the Action Module documentation for more details on the borrow and repay functions.
export type Signature = {
data: object
signature: { v: number; r: string; s: string; deadline: number }
}
/* data object
interface SignAuction {
loan: SignLoanConfig;
assets: string[];
assetPrice: BigInt;
assetLtv: BigInt;
endTime: BigInt;
nonce: BigInt;
deadline: BigInt;
}
interface SignLoanConfig {
loanId: string;
aggLoanPrice: BigInt;
aggLtv: BigInt;
aggLiquidationThreshold: BigInt;
totalAssets: BigInt;
nonce: BigInt;
deadline: BigInt;
}
*/Unlockd is dedicated to providing comprehensive support to ensure a seamless experience for our users, developers and partners. If you have any questions, need assistance, or are interested in exploring partnership opportunities, please reach out to us using the contact information below.
For technical support, troubleshooting, or any other inquiries related to the Unlockd protocol or SDK, please contact our support team. We are here to help you with any issues you may encounter and provide guidance on using our platform and tools effectively.
Email: [email protected]
If you are interested in partnering with Unlockd, have questions about our revenue-sharing program, or would like to discuss potential collaborations, please get in touch with our partnerships team. We are always looking to work with innovative projects and organizations to expand the ecosystem.
Email: [email protected]


import {healthFactor} from "@unlockdfinance/unlockd-ts";
const oneEth = BigInt(1e18)
const params = {
collateral: (BigInt(100) * oneEth),
debt: (BigInt(50) * oneEth),
liquidationThreshold: BigInt(8500)
}
const result = healthFactor(params)import { UnlockdApi} from "@unlockdfinance/unlockd-ts";
const api= new UnlockdApi()
const params = {
"nfts":
[
{
"collection": "0x1750d2e6f2fb7fdd6a751833f55007cf76fbb358",
"tokenId": "10"
}
],
"underlyingAsset": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9"
}
const response = await api.prices(params.nfts, params.underlyingAsset)
import {minimumToRepay} from "@unlockdfinance/unlockd-ts";
const params = {
initialLoans: [
{
valuation: 10000000n,
ltv: 5000n,
},
{
valuation: 10000000n,
ltv: 5000n,
},
],
indicesToDelete: [1],
totalDebt: 2n,
}
const result = minimumToRepay(params)export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const bid = async (
amountToPay: BigInt,
amountOfDebt: BigInt,
signature: Signature,
options?: ClientOptions
)const result = await bid(amountToPay, amountOfDebt, signature, options?);export const redeem = async (
signature: Signature,
options?: ClientOptions
)const result = await redeem(signature, options?);export const finalize = async (
claimOnUWallet: boolean,
orderId: string,
signature: Signature,
options?: ClientOptions
)const result = await finalize(claimOnUWallet, orderId, signature, options?);export type Nft = {
collection: string
tokenId: string
}export type Signature = {
data: object
signature: { v: number; r: string; s: string; deadline: number }
}
/* data object
interface SignAction {
loan: SignLoanConfig;
assets: string[];
underlyingAsset: string;
nonce: BigInt;
deadline: BigInt;
}
interface SignLoanConfig {
loanId: string;
aggLoanPrice: BigInt;
aggLtv: BigInt;
aggLiquidationThreshold: BigInt;
totalAssets: BigInt;
nonce: BigInt;
deadline: BigInt;
}
*/export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const borrow = async (
amount: BigInt,
assets: Array<Nft>,
signature: Signature,
options?: ClientOptions
)const result = await borrow(amount, assets[], signature, options?);export const repay = async (
amount: BigInt,
signature: Signature,
options?: ClientOptions
)const result = await repay(amount, signature, options?);The Sell Now module allows you to sell assets instantly at a fixed price.
The NFT Batch Transfer module allows you to send multiple NFTs to the Unlockd user wallet in a single transaction.
export type Nft = {
collection: string
tokenId: string
}To send NFTs to the Unlockd user wallet, use the sendNftsToWallet function from the nftBatchTransfer module:
Refer to the NFT Batch Transfer Module documentation for more details on the sendNftsToWallet function.
export type Signature = {
data: object
signature: { v: number; r: string; s: string; deadline: number }
}
/* data object
interface SignSellNow {
loan: SignLoanConfig;
assetId: string;
marketAdapter: string;
marketApproval: string;
marketPrice: BigInt;
underlyingAsset: string;
from: string;
to: string;
data: string;
value: BigInt;
nonce: BigInt;
deadline: BigInt;
}
interface SignLoanConfig {
loanId: string;
aggLoanPrice: BigInt;
aggLtv: BigInt;
aggLiquidationThreshold: BigInt;
totalAssets: BigInt;
nonce: BigInt;
deadline: BigInt;
}
*/When using the Unlockd SDK, follow these best practices to ensure a smooth integration and optimal performance:
Always wrap SDK method calls in try-catch blocks to handle errors gracefully.
Handle specific error cases based on the error code and display appropriate error messages to the user.
Log errors for debugging and monitoring purposes.
Keep your API key and secret secure and avoid exposing them in client-side code.
Use secure communication channels (e.g., HTTPS) when making API requests.
Validate and sanitize user input to prevent security vulnerabilities.
Minimize the number of API requests to improve performance and reduce latency.
Use caching mechanisms to store frequently accessed data and avoid unnecessary API calls.
Implement pagination or lazy loading for large datasets to load data incrementally.
Write unit tests to verify the correctness of your integration code.
Test error handling scenarios to ensure your application can handle errors gracefully.
Conduct thorough testing before deploying to production.
Regularly check for updates and new versions of the Unlockd SDK.
Review the changelog and update your integration code accordingly.
Stay informed about any deprecations or breaking changes in the SDK.
By following these best practices, you can ensure a reliable and efficient integration of the Unlockd SDK into your application.


import {availableToBorrow} from "@unlockdfinance/unlockd-ts";
const nfts = [
{
valuation: (BigInt(100) * oneEth).toString(),
ltv: '5000'
},
{
valuation: (BigInt(50) * oneEth).toString(),
ltv: '3000'
}
]
const result = availableToBorrow(nfts)export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const sell = async (
asset: Nft,
signature: Signature,
options?: ClientOptions
)const result = await sell(asset, signature, options?);export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const sendNftsToWallet = async (
nfts: {
contractAddress: string,
tokenId: string
}[],
options?: ClientOptions
): Promise<void>const result = await sendNftsToWallet(nfts[], signature, options?);To create a market item, use the create function from the market module:
Refer to the documentation for more details on the create function.
To cancel a market item, use the cancel function from the market module:
Refer to the Market Module documentation for more details on the cancel function.
To bid on a market item, use the marketBid function from the market module:
Refer to the documentation for more details on the marketBid function.
To claim a market item, use the claim function from the market module:
Refer to the Market Module documentation for more details on the claim function.
To cancel a claim on a market item, use the cancelClaim function from the market module:
Refer to the Market Module documentation for more details on the cancelClaim function.
To do a buy now on a market item, use the buyNow function from the market module:
Refer to the Market Module documentation for more details on the buyNow function.

Unlockd provides a unique opportunity for partners to integrate our liquidity solutions directly into their products. By leveraging the Unlockd SDK, partners can offer seamless RWA-backed borrowing functionalities to their users, with the added benefit of participating in a revenue-sharing program.
While the Unlockd Protocol is permissionless, Veris Labs, and eventually the Unlockd DAO, determine which assets are whitelisted and supported. These assets typically come from our tokenizing partners, who collaborate closely with Unlockd to ensure their assets are seamlessly integrated into our platform.
For development teams and organizations with supported assets, Unlockd offers tailored partnerships. These partnerships enable the integration of Unlockd's advanced borrowing and lending functionalities directly into their products. Partners can choose to brand the integrated solution as powered by Unlockd or offer it as a completely native experience.
The Revenue Sharing Program is designed to create a mutually beneficial relationship between Unlockd and its partners. Here's how it works:
Integration: Partners integrate Unlockd's functionalities using the Unlockd SDK.
Loan Origination: Loans originated through the partner's integration generate revenue from the interest rate spread.
Revenue Split: The generated revenue is split between the partner and Unlockd. The specific terms of the revenue split are tailored to each partnership.
Increased Revenue: Earn a share of the revenue generated from loans originated through your integration.
By partnering with Unlockd, you can unlock new revenue streams and offer your users innovative financial services. For more information on how to become a partner and participate in the Revenue Sharing Program, please .
The Wallet module provides functionality for creating and managing Unlockd abstract wallets.
export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'To create an Unlockd abstract wallet, use the createWallet function from the wallet module:
Refer to the Wallet Module documentation for more details on the createWallet function.
To retrieve the wallet address of the user, use the getWallet function from the wallet module:
Refer to the Wallet Module documentation for more details on the getWallet function.
Unlockd uses Reservoir to enhance its liquidation process for RWAs and NFTs. Reservoir provides a comprehensive aggregation of liquidity from various marketplaces, ensuring that Unlockd can liquidate assets quickly and efficiently at the best available price. By utilizing Reservoir's infrastructure, Unlockd benefits from:
Liquidity Aggregation: Access to aggregated bids and listings from multiple major marketplaces.
Upgrade Protection: Automatic updates and new features from Reservoir without additional integration work.
Order Distribution: Ability to distribute and cross-post orders across various marketplaces, maximizing exposure and liquidity.
This integration allows Unlockd to manage liquidations effectively, maintaining the value and efficiency of its protocol while ensuring debt recovery and minimizing risks of bad debt​.
Learn more about and the in our Documentation.
For any project with assets supported on Unlockd, particularly if you are building liquidity features using our SDK, integrating your protocol with Reservoir is highly recommended for optimal performance and efficiency.
Reservoir aggregates liquidity from multiple marketplaces, ensuring assets are liquidated at the best prices. This enhances the efficiency of Unlockd’s liquidation process, maximizing returns and minimizing undervalued sales.
By integrating their assets with Reservoir, partners ensure that when these assets are whitelisted as collateral, the Unlockd protocol can connect to Reservoir’s liquidity. This connection ensures the entire system works smoothly when integrating via the SDK.
But there's more:
Even independently of Unlockd, integrating Reservoir offers significant advantages for RWA issuers and marketplaces:
For RWA issuers, integrating with Reservoir allows access to a broad network of buyers, enhancing market reach and asset visibility. Streamlined management of listings and bids through Reservoir’s infrastructure simplifies operations and increases asset liquidity and value.
For marketplaces, integrating with Reservoir ensures access to aggregated liquidity from various sources, maximizing exposure and sale opportunities. This broadens the market for listed assets, ensuring they are sold efficiently and at the best possible prices, benefiting both the marketplace and its users.
export type CreateOrderInput = {
startAmount: BigInt
endAmount: BigInt
startTime: number
endTime: number
debtToSell: BigInt
}export enum OrderTypes {
ASC = 'ASC',
DESC = 'DESC',
NONE = 'none'
}export type Signature = {
data: object
signature: { v: number; r: string; s: string; deadline: number }
}
/* data object
interface SignMarket {
loan: SignLoanConfig;
assetId: string;
collection: string;
tokenId: BigInt;
assetPrice: BigInt;
assetLtv: BigInt;
nonce: BigInt;
deadline: BigInt;
}
interface SignLoanConfig {
loanId: string;
aggLoanPrice: BigInt;
aggLtv: BigInt;
aggLiquidationThreshold: BigInt;
totalAssets: BigInt;
nonce: BigInt;
deadline: BigInt;
}
*/export type ClientOptions = {
network?: Chain
}
export type Chain = 'mainnet' | 'sepolia' | 'localhost'export const create = async (
underlyingAsset: Address,
orderType: OrderType,
config: CreateOrderInput,
signature: Signature,
options?: ClientOptions
)const result = await create(underlyingAsset, orderType, config, signature, options?);export const cancel = async (
orderId: string,
options?: ClientOptions
)const result = await cancel(orderId, options?);export const marketBid = async (
orderId: string,
amountToPay: BigInt,
amountOfDebt: BigInt,
signature: Signature,
options?: ClientOptions
)const result = await marketBid(orderId, amountToPay, amountOfDebt, signature, options?);export const claim = async (
claimOnUWallet: boolean,
orderId: string,
signature: Signature,
options?: ClientOptions
)const result = await claim(claimOnUWallet, orderId, signature, options?);export const cancelClaim = async (
orderId: string,
signature: Signature,
options?: ClientOptions
)const result = await cancelClaim(orderId, signature, options?);export const buyNow = async (
claimOnUWallet: boolean,
orderId: string,
amountToPay: BigInt,
amountOfDebt: BigInt,
signature: Signature,
options?: ClientOptions
)const result = await buyNow(claimOnUWallet, orderId, amountToPay, amountOfDebt, signature, options?);Enhanced Offerings: Provide your users with advanced RWA-backed borrowing capabilities, enhancing your product's value proposition.
Flexibility: Choose whether to brand the integrated solution as powered by Unlockd or as a completely native experience.

export const createWallet = async (
options?: ClientOptions
):Promise<void>const result = await createWallet(options?);export const getWallet = async (
options?: ClientOptions
): Promise<any>const result = await getWallet(options?);'Buy Now, Pay Later' (BNPL) financing is a payment solution that allows users to purchase tokenized assets immediately while deferring payment indefinitely (thanks to Unlockd's open-ended loans).
By integrating BNPL functionalities using the Unlockd SDK, developers can provide a seamless and flexible financing option directly at the point of sale, backed by Real-World Assets.
User Selection: During the checkout process, users select the 'Buy Now, Pay Later' option as their preferred payment method.
Risk Assessment: Unlockd conducts a real-time appraisal of the user's tokenized assets to determine their value. This appraisal helps in setting an appropriate Loan-to-Value (LTV) ratio for the loan.
Down Payment Selection: Users are required to select a down payment amount, which may sometimes be a minimum required amount based on the value of the asset and the LTV ratio.
Seamless Integration: Easily incorporate BNPL options into existing checkout flows using the Unlockd SDK.
Enhanced Flexibility: Users enjoy the flexibility of deferred payments without immediate financial burden.
Improved Cash Flow for Merchants: Merchants receive full payment upfront, improving their cash flow and reducing financial risk.
Lower Risk: The use of tokenized assets as collateral reduces the risk for both users and the protocol, enabling better loan terms and interest rates.
This guide will walk you through the process of implementing a borrowing feature using the Unlockd SDK, allowing users to borrow against RWAs.
Before you begin, ensure you have the following set up:
A React-based project
The Unlockd SDK installed (@verislabs/unlockd-sdk)
Wagmi library and its dependencies (viem, react-query) for Ethereum interactions. You can or .
Basic understanding of React hooks and Ethereum transactions
Set up the React component with necessary state variables and hooks.
Implement login, connection and authentication
Create or refresh Unlockd wallet (Unlockd Account).
Implement BNPL transaction execution.
Implementing the Buy Now, Pay Later (buyNow in the SDK) functionality in a testnet environment is more complex due to the following reasons:
Two-wallet setup: You need one wallet to mint and list an NFT for sale on , and a second wallet to execute the purchase.
Testnet limitations: Reservoir does not allow token swaps on testnets, including Sepolia. This affects how BNPL transactions are structured in the test environment.
Initialize the main App component and set up all required state variables using React hooks. This includes states for managing user accounts, selected assets, and BNPL transaction details.
Set up functionality authenticating with Unlockd. This step calls the Unlockd SDK for obtaining an authentication token for the user's currently connected address by signing a blockchain message. The auth token is essential for user's interaction with the Unlockd SDK
Implement logic to create a new Unlockd wallet if the user doesn't have one, or refresh an existing one. This step is crucial for managing the user's assets and loans.
Set up the core BNPL functionality using the Unlockd SDK. This involves getting the buy now signature, calculating the loan terms, and executing the BNPL transaction.
Create and render the user interface components that tie all the functionality together. This includes elements for wallet connection, asset selection, and initiating the BNPL transaction.
Instant Payment: The merchant receives the full payment for the asset immediately from Unlockd's liquidity pool, ensuring they do not bear any financial risk.
Open-Ended Loan: The loan remains open-ended, allowing the user to repay at their convenience as long as the health factor of their collateralized asset remains within acceptable limits. There are no fixed repayment schedules.
Collateral Management: The user’s collateralized assets are held securely in their Unlockd Account. These assets remain in the user’s custody and ownership, with Unlockd intervening only to prevent unauthorized transactions during the loan period.
Health Factor Monitoring: Users need to maintain the health factor of their loan, which is continuously monitored. If the health factor drops below a certain threshold, the user may need to add more collateral or repay part of the loan to restore the balance.
Completion: Once the loan is fully repaid, the collateralized assets are released back to the user without any restrictions.
Currency differences: In the testnet implementation, we use WETH. However, on mainnet, USDC is typically used for RWA purchases.
These factors make recreating a full BNPL flow on testnet more challenging. Keep these differences in mind when testing and developing your BNPL integration, as the actual mainnet implementation will differ.
If you need any help, contact us. We actually like being bothered.
"use client";
import { useState, useEffect } from "react";
import {
useAccount,
useConnect,
useDisconnect,
useConfig,
useWriteContract,
} from "wagmi";
import {
signMessage,
readContract,
writeContract,
waitForTransactionReceipt,
} from "wagmi/actions";
import { sepolia } from "wagmi/chains";
import { injected } from "wagmi/connectors";
import { parseUnits, Address, isAddress } from "viem";
import {
UnlockdApi,
Chains,
UnderlyingsAsset,
borrow,
underlyingsAssets,
createWallet,
getWallet,
} from "@verislabs/unlockd-sdk";
interface NFTPriceData {
collection: Address;
tokenId: string;
underlyingAsset: Address;
price?: string;
}
const ALLOWED_COLLECTIONS: Address[] = [
"0xa6a9acfdd1f64ec324ee936344cdb1457bdbddf0",
"0x388043e55a388e07a75e9a1412fe2d64e48343a5",
];
// Choose the asset I want
// Validate if possible to buy from reservoir
// If yes: check if the user has a unlockdWallet: if not, create one, if yes: continue.
// Validate the user amount, the selected NFT, and get the buynow signature.
// BuyNow call getCalculations with the signature and the (minAmount,)
// BuyNow
function App() {
// Variables
const userAccount = useAccount();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const config = useConfig();
const [unlockdAccount, setUnlockdAccount] = useState<Address>();
const [token, setToken] = useState<string | null>(null);
const [selectedUnderlyingAsset, setSelectedUnderlyingAsset] =
useState<Address>(underlyingsAssets(Chains.Sepolia)[UnderlyingsAsset.USDC]);
const [selectedUnlockdNFTs, setSelectedUnlockdNFTs] = useState<
NFTPriceData[]
>([]);
const [borrowAmount, setBorrowAmount] = useState<string>("");
const [collection, setCollection] = useState<string>("");
const [tokenId, setTokenId] = useState<number>(0);
const [amount, setAmount] = useState<number>(0);
const api = new UnlockdApi(Chains.Sepolia);typescriptCopyconst api = new UnlockdApi(Chains.Sepolia); // The login should call the unlockd API using the signatureMessage method to get the message to sign.
// We get the message to sign, and then we trigger it to the user so he can sign it (signMessage).
// After the user signs the message, we validate it using the validateMessage method, and returning the auth token we will need to interact with the protocol.
const logIn = async () => {
if (!userAccount.address) {
return;
}
try {
const message = await api.signatureMessage(userAccount.address);
const signature = await signMessage(config, {
account: userAccount.address,
message: message.message,
});
const authToken = await api.validateMessage(
userAccount.address,
signature,
);
setToken(authToken.token);
} catch (error) {
console.error("Login failed:", error);
}
}; // In order to do a buyNow, you will need to own an unlockd wallet.
// The assets (nfts) will be locked in your unlockd wallet.
// If you don't have an unlockd wallet, it will be created.
const createOrRefreshUnlockdWallet = async () => {
try {
let unlockdWallet = await getWallet({ network: Chains.Sepolia });
if (!unlockdWallet) {
await createWallet({ network: Chains.Sepolia });
unlockdWallet = await getWallet({ network: Chains.Sepolia });
}
if (unlockdWallet) {
setUnlockdAccount(unlockdWallet);
}
} catch (error) {
console.error("Error with Unlockd wallet:", error);
}
}; const handleBuyNow = async () => {
if (selectedUnlockdNFTs.length === 0 || !token || !unlockdAccount) {
return;
}
try {
const nftsArray = selectedUnlockdNFTs.map((nft) => ({
collection: nft.collection,
tokenId: nft.tokenId,
}));
const params = {
nfts: nftsArray,
underlyingAsset: selectedUnderlyingAsset,
};
const signature = await api.borrowSignature(token, params);
await borrow(BigInt(parseUnits(borrowAmount, 6)), nftsArray, signature, {
network: Chains.Sepolia,
});
} catch (error) {
console.error("Borrow failed:", error);
}
};
useEffect(() => {
if (token) {
createOrRefreshUnlockdWallet();
}
}, [token]);
const isAuth = userAccount.isConnected && !!token; return (
<div>
<h1>Unlockd Borrow Example</h1>
{/* Connect wallet */}
{(() => {
if (!userAccount.isConnected) {
return (
<div>
<button onClick={() => connect({ connector: injected() })}>
Connect
</button>
</div>
);
}
return (
<div>
<p>Connected to {userAccount.address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
})()}
{/* Check current network */}
{(() => {
if (userAccount.isConnected && userAccount.chainId !== sepolia.id) {
return (
<div>
<p>Please switch to Sepolia network</p>
</div>
);
}
return null;
})()}
{/* Unlockd Log In */}
{(() => {
if (userAccount.isConnected && !isAuth) {
return (
<div>
<button onClick={logIn}>Log In to Unlockd</button>
</div>
);
}
return null;
})()}
{/* Select asset */}
{(() => {
if (isAuth) {
return (
<select
value={selectedUnderlyingAsset}
onChange={(e) =>
setSelectedUnderlyingAsset(e.target.value as Address)
}
>
{Object.entries(UnderlyingsAsset).map(([key, value]) => (
<option
key={key}
value={underlyingsAssets(Chains.Sepolia)[value]}
>
{key}
</option>
))}
</select>
);
}
})()}
{/* Create & display Unlockd wallet and the supported collections */}
{(() => {
if (isAuth) {
return (
<>
<h2>Unlockd Wallet</h2>
<p>Unlockd Wallet: {unlockdAccount || "Not created"}</p>
<button onClick={createOrRefreshUnlockdWallet}>
Create/Refresh Unlockd Wallet
</button>
{unlockdAccount && (
<>
<h3>NFT to buyNow</h3>
<div>
<input
type="text"
value={collection}
onChange={(e) => setCollection(e.target.value)}
placeholder="Collection"
/>
</div>
<div>
<input
type="number"
value={tokenId}
onChange={(e) => setTokenId(parseInt(e.target.value))}
/>
</div>
<div>
<input
type="number"
value={amount}
onChange={(e) => setAmount(parseInt(e.target.value))}
/>
</div>
<div>
<button onClick={handleBuyNow}>Buy Now</button>
</div>
</>
)}
</>
);
}
})()}
</div>
);
}
export default App;Borrowing against Real-World Assets (RWAs) is a powerful feature of the Unlockd protocol. By leveraging the Unlockd SDK, developers can enable users to collateralize their tokenized assets and obtain instant loans.
This feature can be applied to loans with a single asset or even a portfolio of assets from different classes.
If your product allows users to access an inventory or portfolio, you can natively display their Available To Borrow amount and allow them to bundle all items into a single or different loans without leaving your interface
This guide will walk you through the process of implementing a borrowing feature using the Unlockd SDK, allowing users to borrow against RWAs.
Before you begin, ensure you have the following set up:
A React-based project
The Unlockd SDK installed (@verislabs/unlockd-sdk)
Wagmi library and its dependencies (viem, react-query) for Ethereum interactions. You can or .
Basic understanding of React hooks and Ethereum transactions
Set up the React component with necessary state variables and hooks.
Implement login, connection and authentication
Fetch NFTs and prices from connected wallets.
Create or refresh Unlockd wallet.
In your component, first set up all required state variables (using useState, Context, or your own state manager) and wagmi hooks. This includes states for managing user accounts, NFT selections, approval statuses, and other essential data. For the Unlockd SDK you only need to initialize the api object.
Set up functionality authenticating with Unlockd. This step calls the Unlockd SDK for obtaining an authentication token for the user's currently connected address by signing a blockchain message. The auth token is essential for user's interaction with the Unlockd SDK
Implement logic to fetch NFTs (RWAs) from both the user's connected wallet and their Unlockd wallet, if the wallet was created before (see below).
This involves querying blockchain data for NFT balances and token IDs, then passing this data to the Unlockd API to retrive price data for all NFTs. In this example, we are quering the blockchain directly to obtain the NFTs data (using the multicall action from wagmi for efficiency) but you can obtain this data from any means (such as using the Alchemy SDK).
What you need to be aware of is the data structure expected by the Unlockd API when calling the prices method, as see in the priceParams.
Set up functionality to create a new Unlockd wallet (Unlockd Account) if the user doesn't have one, or refresh an existing one. This step is crucial for storing the assets that will be used as collateral for borrowing.
Create a mechanism for users to select one or multiple NFTs, either for transferring to the Unlockd wallet or for using as collateral when borrowing. This function is reused in selecting NFTs in both the user's wallet and the Unlockd wallet.
Set up the process for approving NFT collections and transferring selected NFTs to the Unlockd wallet. This includes checking the current approval status for all selected NFTs, sending approval transactions for each collection, and transferring the selected NFTs in batch to the Unlockd wallet.
Implement the core borrowing feature of the Unlockd protocol. This includes selecting NFTs from the Unlockd wallet, specifying borrow amounts, obtaining borrow signatures, and executing borrow transactions.
Create and render the user interface components that tie all the functionality together. This includes elements for wallet connection, NFT selection, approval buttons, transfer initiation, borrow amount input, and the borrow action button.
Implement NFT approval and transfer to Unlockd wallet.
Set up borrowing functionality against selected NFTs.
Render the user interface with all interactive elements.
"use client";
import { useState, useEffect } from "react";
import {
useAccount,
useConnect,
useDisconnect,
useConfig,
useWriteContract,
useReadContracts,
} from "wagmi";
import {
signMessage,
readContract,
writeContract,
waitForTransactionReceipt,
multicall,
getBalance,
} from "wagmi/actions";
import { sepolia } from "wagmi/chains";
import { injected } from "wagmi/connectors";
import { parseUnits, Address, isAddress } from "viem";
import {
UnlockdApi,
Chains,
UnderlyingsAsset,
borrow,
underlyingsAssets,
createWallet,
getWallet,
} from "@verislabs/unlockd-sdk";
import { nftAbi } from "../../abis/ERC721Abi";
import { nftBatchTransferAbi } from "../../abis/NftBatchTransferAbi";
interface NFTPriceData {
collection: Address;
tokenId: string;
underlyingAsset: Address;
price?: string;
}
const ALLOWED_COLLECTIONS: Address[] = [
"0xa6a9acfdd1f64ec324ee936344cdb1457bdbddf0",
"0x388043e55a388e07a75e9a1412fe2d64e48343a5",
];
const nftBatchTransferAddress =
"0xaba905eba39b9a55fd0f910a6415ba91c3e9353d" as Address;
// The idea is to create a simple example of how to use the Unlockd SDK to borrow against NFTs.
// The user can connect their wallet, log in to Unlockd, approve a collection, select NFTs to transfer to their Unlockd wallet, and borrow against them.
// We are not demonstrating frontend code, or handling states, just the logic to interact with the Unlockd SDK and create a borrow transaction.
function App() {
// Variables
const userAccount = useAccount();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const config = useConfig();
const [unlockdAccount, setUnlockdAccount] = useState<Address>();
const [token, setToken] = useState<string | null>(null);
const [selectedNFTs, setSelectedNFTs] = useState<NFTPriceData[]>([]);
const [selectedUnderlyingAsset, setSelectedUnderlyingAsset] =
useState<Address>(underlyingsAssets(Chains.Sepolia)[UnderlyingsAsset.USDC]);
const [approvalStatus, setApprovalStatus] = useState<
Record<Address, "not-approved" | "approved" | "pending">
>({});
const [injectedWalletNFTs, setInjectedWalletNFTs] = useState<NFTPriceData[]>(
[],
);
const [unlockdWalletNFTs, setUnlockdWalletNFTs] = useState<NFTPriceData[]>(
[],
);
const [selectedUnlockdNFTs, setSelectedUnlockdNFTs] = useState<
NFTPriceData[]
>([]);
const [borrowAmount, setBorrowAmount] = useState<string>("");
const api = new UnlockdApi(Chains.Sepolia);const api = new UnlockdApi(Chains.Sepolia); // The login should call the unlockd API using the signatureMessage method to get the message to sign.
// We get the message to sign, and then we trigger it to the user so he can sign it (signMessage).
// After the user signs the message, we validate it using the validateMessage method, and returning the auth token we will need to interact with the protocol.
const logIn = async () => {
if (!userAccount.address) {
return;
}
try {
const message = await api.signatureMessage(userAccount.address);
const signature = await signMessage(config, {
account: userAccount.address,
message: message.message,
});
const authToken = await api.validateMessage(
userAccount.address,
signature,
);
setToken(authToken.token);
} catch (error) {
console.error("Login failed:", error);
}
}; // In order to borrow, we 1st should check if the user has any Allowed Collections in his wallet. If yes: we shouldt get the balance of it, and then get the token ID of each NFT in order to provide the price. If no: it ends here.
// The fetchNftsAndPrices should be called from the injected wallet and the unlockd wallet, since both will hold assets.
const fetchNFTsAndPrices = async (
walletAddress: Address,
selectedUnderlyingAsset: Address,
setNFTs: React.Dispatch<React.SetStateAction<NFTPriceData[]>>,
) => {
if (!walletAddress || !isAddress(walletAddress)) {
console.error(`Invalid wallet address: ${walletAddress}`);
return;
}
if (!selectedUnderlyingAsset || !isAddress(selectedUnderlyingAsset)) {
console.error(
`Invalid selected underlying asset: ${selectedUnderlyingAsset}`,
);
return;
}
try {
const balanceContracts = ALLOWED_COLLECTIONS.map((collection) => ({
address: collection,
abi: nftAbi.abi,
functionName: "balanceOf",
args: [walletAddress],
} as const));
const balances = await multicall(config, { contracts: balanceContracts });
const tokenIdContracts = balances.flatMap((balance, index) => {
if (balance.status !== 'success') return [];
const numTokens = Number(balance.result || 0);
if (numTokens === 0) return [];
return Array.from({ length: numTokens }, (_, i) => ({
address: ALLOWED_COLLECTIONS[index],
abi: nftAbi.abi,
functionName: "tokenOfOwnerByIndex",
args: [walletAddress, BigInt(i)],
} as const));
});
const tokenIds = await multicall(config, { contracts: tokenIdContracts });
const priceParams = tokenIds
.filter((tokenIdResult) => tokenIdResult.status === 'success')
.map((tokenIdResult, index) => ({
collection: tokenIdContracts[index].address,
tokenId: tokenIdResult.result.toString(),
underlyingAsset: selectedUnderlyingAsset,
}));
const pricesResponse = await api.prices(priceParams);
const nftsPriceData = pricesResponse.map((result) => ({
collection: result.collection as Address,
tokenId: result.tokenId,
underlyingAsset: selectedUnderlyingAsset,
price: result.valuation,
}));
setNFTs(nftsPriceData);
} catch (error) {
console.error(
`Error fetching NFTs and prices for wallet ${walletAddress}:`,
error,
);
}
};{
collection: string, // NFT's collection address
tokenId: string, // NFT's ERC721 token id
underlyingAsset: string // the asset ETH or USDC you want to receive the price evaluation in
} // In order to create a loan, you will need to own an unlockd wallet.
// The assets (nfts) will be locked in your unlockd wallet.
// If you don't have an unlockd wallet, it will be created.
const createOrRefreshUnlockdWallet = async () => {
try {
let unlockdWallet = await getWallet({ network: Chains.Sepolia });
if (!unlockdWallet) {
await createWallet({ network: Chains.Sepolia });
unlockdWallet = await getWallet({ network: Chains.Sepolia });
}
if (unlockdWallet) {
setUnlockdAccount(unlockdWallet);
await fetchNFTsAndPrices(unlockdWallet, selectedUnderlyingAsset, setUnlockdWalletNFTs);
}
} catch (error) {
console.error("Error with Unlockd wallet:", error);
}
}; // Allows the user to select 1 or multiple NFTs to transfer to the unlockd wallet or to borrow agains them, if used from the unlockd wallet.
const toggleNFTSelection = (nft: NFTPriceData, isUnlockdWallet: boolean) => {
const setNFTs = isUnlockdWallet ? setSelectedUnlockdNFTs : setSelectedNFTs;
setNFTs((prev) =>
prev.some(
(item) =>
item.collection === nft.collection && item.tokenId === nft.tokenId,
)
? prev.filter(
(item) =>
!(
item.collection === nft.collection &&
item.tokenId === nft.tokenId
),
)
: [...prev, nft],
);
}; // This will validate if the user has approved the BatchNFTTransfer contract to transfer the NFTs from his injected wallet to the unlockd wallet.
// If the approvals exist you dont need to approve again. If not, the user SHOULD APPROVE.
// The SDK should provide a function to approve!
const handleApprove = async (collectionAddress: Address) => {
if (!userAccount.address) {
return;
}
try {
const isApproved = await readContract(config, {
address: collectionAddress,
abi: nftAbi.abi,
functionName: "isApprovedForAll",
args: [userAccount.address, nftBatchTransferAddress],
});
if (isApproved) {
setApprovalStatus((prev) => ({
...prev,
[collectionAddress]: "approved",
}));
return;
}
const response = await writeContract(config, {
address: collectionAddress,
abi: nftAbi.abi,
functionName: "setApprovalForAll",
args: [nftBatchTransferAddress, true],
});
setApprovalStatus((prev) => ({
...prev,
[collectionAddress]: "pending",
}));
const transactionReceipt = await waitForTransactionReceipt(config, {
hash: response,
});
if (transactionReceipt.status === "success") {
setApprovalStatus((prev) => ({
...prev,
[collectionAddress]: "approved",
}));
} else {
setApprovalStatus((prev) => ({
...prev,
[collectionAddress]: "not-approved",
}));
}
} catch (error) {
console.error("Approval failed:", error);
setApprovalStatus((prev) => ({
...prev,
[collectionAddress]: "not-approved",
}));
}
};
const { writeContract: batchTransfer } = useWriteContract();
// This function will transfer the selected NFTs from the injected wallet to the unlockd wallet.
// The approve of this contract and its usage is not mandatory.
// The important is the the unlockd wallet has the NFTs.
// This contract was developed and audited by us, so we use it, feel free to use it, or change to other preferred method.
const handleBatchTransfer = async () => {
if (!userAccount.address || !unlockdAccount || selectedNFTs.length === 0)
return;
try {
const nftTransfers = selectedNFTs.map((nft) => ({
contractAddress: nft.collection,
tokenId: BigInt(nft.tokenId),
}));
batchTransfer({
address: nftBatchTransferAddress,
abi: nftBatchTransferAbi.abi,
functionName: "batchTransferFrom",
args: [nftTransfers, unlockdAccount],
});
await fetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs);
await fetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs);
} catch (error) {
console.error("Batch transfer failed:", error);
}
};
// This function will create a borrow transaction.
// The user should select the NFTs he wants to borrow against (limit: 100), and the amount he wants to borrow.
// If the user has an unlockd wallet with allowed NFTs, he can borrow against them.
// We 1st generate an array of nfts (collection and tokenId), after, we build the params we need to call the backend for a borrow signature, by providing the nfts and the underlying asset.
// The backend will return a signature, and we will use it to call the borrow function from the SDK, adding the amount, nfts and signature.
// we use 6 decimals since the borrow amount is in USDC.
const handleBorrow = async () => {
if (selectedUnlockdNFTs.length === 0 || !token || !unlockdAccount) {
return;
}
try {
const nftsArray = selectedUnlockdNFTs.map((nft) => ({
collection: nft.collection,
tokenId: nft.tokenId,
}));
const params = {
nfts: nftsArray,
underlyingAsset: selectedUnderlyingAsset,
};
const signature = await api.borrowSignature(token, params);
await borrow(BigInt(parseUnits(borrowAmount, 6)), nftsArray, signature, {
network: Chains.Sepolia,
});
await fetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs);
} catch (error) {
console.error("Borrow failed:", error);
}
};
useEffect(() => {
if (
userAccount.isConnected &&
userAccount.address &&
isAddress(userAccount.address)
) {
fetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs);
}
}, [userAccount.isConnected, userAccount.address, selectedUnderlyingAsset]);
useEffect(() => {
if (token) {
createOrRefreshUnlockdWallet();
}
}, [token]);
const isAuth = userAccount.isConnected && !!token; return (
<div>
<h1>Unlockd Borrow Example</h1>
{/* Connect wallet */}
{(() => {
if (!userAccount.isConnected) {
return (
<div>
<button onClick={() => connect({ connector: injected() })}>
Connect
</button>
</div>
);
}
return (
<div>
<p>Connected to {userAccount.address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
})()}
{/* Check current network */}
{(() => {
if (userAccount.isConnected && userAccount.chainId !== sepolia.id) {
return (
<div>
<p>Please switch to Sepolia network</p>
</div>
);
}
return null;
})()}
{/* Unlockd Log In */}
{(() => {
if (userAccount.isConnected && !isAuth) {
return (
<div>
<button onClick={logIn}>Log In to Unlockd</button>
</div>
);
}
return null;
})()}
{/* Select asset */}
{(() => {
if (isAuth) {
return (
<select
value={selectedUnderlyingAsset}
onChange={(e) =>
setSelectedUnderlyingAsset(e.target.value as Address)
}
>
{Object.entries(UnderlyingsAsset).map(([key, value]) => (
<option
key={key}
value={underlyingsAssets(Chains.Sepolia)[value]}
>
{key}
</option>
))}
</select>
);
}
})()}
{/* Display collections */}
{(() => {
if (isAuth) {
return (
<>
{ALLOWED_COLLECTIONS.map((collectionAddress) => (
<div key={collectionAddress}>
<h3>Collection: {collectionAddress}</h3>
<button
onClick={() => handleApprove(collectionAddress)}
disabled={
approvalStatus[collectionAddress] === "approved" ||
approvalStatus[collectionAddress] === "pending"
}
>
{approvalStatus[collectionAddress] === "approved"
? "Approved"
: approvalStatus[collectionAddress] === "pending"
? "Approving..."
: "Approve Collection"}
</button>
<p>
Total NFTs in this collection:{" "}
{
injectedWalletNFTs.filter(
(nft) => nft.collection === collectionAddress,
).length
}
</p>
{injectedWalletNFTs
.filter((nft) => nft.collection === collectionAddress)
.map((nft) => (
<div key={`${nft.collection}-${nft.tokenId}`}>
<input
type="checkbox"
checked={selectedNFTs.some(
(item) =>
item.collection === nft.collection &&
item.tokenId === nft.tokenId,
)}
onChange={() => toggleNFTSelection(nft, false)}
/>
<span>Token ID: {nft.tokenId}</span>
<span>Price: {nft.price}</span>
</div>
))}
{injectedWalletNFTs.filter(
(nft) => nft.collection === collectionAddress,
).length === 0 && <p>No NFTs found in this collection</p>}
</div>
))}
</>
);
}
})()}
{/* Transfer NFTs to Unlockd wallet */}
{(() => {
if (isAuth) {
return (
<button
onClick={handleBatchTransfer}
disabled={selectedNFTs.length === 0 || !unlockdAccount}
>
Transfer Selected NFTs to Unlockd Wallet
</button>
);
}
})()}
{/* Create & display Unlockd wallet and the supported collections */}
{(() => {
if (isAuth) {
return (
<>
<h2>Unlockd Wallet</h2>
<p>Unlockd Wallet: {unlockdAccount || "Not created"}</p>
<button onClick={createOrRefreshUnlockdWallet}>
Create/Refresh Unlockd Wallet
</button>
{unlockdAccount && (
<>
<h3>NFTs in Unlockd Wallet</h3>
{unlockdWalletNFTs.map((nft) => (
<div key={`${nft.collection}-${nft.tokenId}`}>
<input
type="checkbox"
checked={selectedUnlockdNFTs.some(
(item) =>
item.collection === nft.collection &&
item.tokenId === nft.tokenId,
)}
onChange={() => toggleNFTSelection(nft, true)}
/>
<span>Collection: {nft.collection}</span>
<span>Token ID: {nft.tokenId}</span>
<span>Price: {nft.price}</span>
</div>
))}
<div>
<input
type="text"
value={borrowAmount}
onChange={(e) => setBorrowAmount(e.target.value)}
placeholder="Borrow Amount"
/>
<button
onClick={handleBorrow}
disabled={
selectedUnlockdNFTs.length === 0 || !borrowAmount
}
>
Borrow Against Selected NFT
</button>
</div>
</>
)}
</>
);
}
})()}
</div>
);
}
export default App