Lending Protocol Guide (XLS-66d)
This guide explains how to use the XRPL Lending Protocol with the XrplCSharp SDK. The lending protocol enables on-ledger collateralized loans managed through loan brokers and vaults.
Note: The Lending Protocol requires the
LendingProtocolamendment (XLS-66d). This feature is in draft and subject to change. Requires rippled 3.1.0+.
Table of Contents
- Overview
- Key Concepts
- Transaction Types
- Step-by-Step: Setting Up a Loan Broker
- Step-by-Step: Creating and Managing a Loan
- CounterpartySignature (LoanSet Co-Signing)
- Ledger Objects
- Number Type
- Common Errors
- Best Practices
Overview
The XRPL Lending Protocol introduces on-ledger collateralized lending with the following architecture:
Broker (Lender) Borrower
┌──────────────────────┐ ┌──────────────────────┐
│ │ LoanSet │ │
│ Vault ◄── Deposit │ ◄──────────► │ Receives principal │
│ Cover ◄── Deposit │ (co-signed)│ Repays via LoanPay │
│ LoanBroker ── Loan │ │ │
└──────────────────────┘ └──────────────────────┘
A broker (lender) creates a vault to hold lending assets, sets up a loan broker with lending parameters, and issues loans to borrowers. The borrower co-signs the loan agreement and repays through periodic payments.
Key Concepts
Vault
A vault holds the assets available for lending. Created via VaultCreate, it stores XRP or IOU tokens that the broker can lend out. Before creating a loan broker, you must create and fund a vault.
Loan Broker
A LoanBroker is a ledger object that represents a lending entity. It references a vault and defines lending parameters such as cover rates, management fees, and debt limits. Created via LoanBrokerSet.
Cover
The broker must deposit cover (collateral from the broker's side) into the loan broker. Cover protects borrowers and ensures the broker has skin in the game. Managed via LoanBrokerCoverDeposit and LoanBrokerCoverWithdraw.
Loan
A Loan is a ledger object representing an active loan between a broker and a borrower. Created via LoanSet (requires co-signing by both parties). The loan tracks principal, interest rates, payment schedule, and outstanding balances.
CounterpartySignature
LoanSet is a special transaction that requires two signatures: the broker (submitter) signs the transaction normally, and the borrower (counterparty) provides a CounterpartySignature. Both parties sign the same transaction preimage.
Number Type
Loan-specific numeric fields (e.g., PrincipalRequested, DebtMaximum) use the XRPL Number type — a 12-byte format consisting of an 8-byte signed mantissa and a 4-byte signed exponent. This is different from the standard Amount type used for payments.
Transaction Types
| Transaction | Purpose | Who Submits |
|---|---|---|
LoanBrokerSet |
Create or update a loan broker | Broker |
LoanBrokerDelete |
Delete a loan broker | Broker |
LoanBrokerCoverDeposit |
Deposit cover into a broker | Broker |
LoanBrokerCoverWithdraw |
Withdraw cover from a broker | Broker |
LoanBrokerCoverClawback |
Clawback cover from a holder | Broker |
LoanSet |
Create a new loan (co-signed) | Broker + Borrower |
LoanDelete |
Delete a fully repaid loan | Broker |
LoanManage |
Manage loan state (default/impair/unimpair) | Broker |
LoanPay |
Make a payment on a loan | Borrower |
Step-by-Step: Setting Up a Loan Broker
1. Create a Vault
The broker first creates a vault to hold lending assets:
using Xrpl.Models.Transactions;
using Xrpl.Models.Common;
using Xrpl.Sugar;
using static Xrpl.Models.Common.Common;
VaultCreate vaultTx = new VaultCreate
{
Account = walletBroker.ClassicAddress,
Asset = new IssuedCurrency { Currency = "XRP" },
};
vaultTx = await client.Autofill(vaultTx);
TransactionSummary vaultResult = await client.SubmitAndWait(vaultTx, walletBroker, true);
// Extract VaultID from metadata
string vaultId = GetCreatedObjectId(vaultResult, LedgerEntryType.Vault);
2. Deposit Assets into the Vault
Fund the vault so the broker has assets to lend:
VaultDeposit depositTx = new VaultDeposit
{
Account = walletBroker.ClassicAddress,
VaultID = vaultId,
Amount = new Currency { Value = "100000000", CurrencyCode = "XRP" }, // 100 XRP
};
depositTx = await client.Autofill(depositTx);
await client.SubmitAndWait(depositTx, walletBroker, true);
3. Create the Loan Broker
Create a broker that references the funded vault:
LoanBrokerSet brokerTx = new LoanBrokerSet
{
Account = walletBroker.ClassicAddress,
VaultID = vaultId,
};
brokerTx = await client.Autofill(brokerTx);
TransactionSummary brokerResult = await client.SubmitAndWait(brokerTx, walletBroker, true);
string brokerId = GetCreatedObjectId(brokerResult, LedgerEntryType.LoanBroker);
4. Configure Broker Parameters (Optional)
Update the broker with lending parameters:
LoanBrokerSet updateTx = new LoanBrokerSet
{
Account = walletBroker.ClassicAddress,
VaultID = vaultId,
CoverRateMinimum = 15000, // 150% minimum cover rate
CoverRateLiquidation = 12000, // 120% liquidation threshold
ManagementFeeRate = 100, // 1% management fee (basis points / 100)
};
updateTx = await client.Autofill(updateTx);
await client.SubmitAndWait(updateTx, walletBroker, true);
5. Deposit Cover
Deposit cover into the broker to enable loan issuance:
LoanBrokerCoverDeposit coverTx = new LoanBrokerCoverDeposit
{
Account = walletBroker.ClassicAddress,
LoanBrokerID = brokerId,
Amount = new Currency { Value = "50000000", CurrencyCode = "XRP" }, // 50 XRP
};
coverTx = await client.Autofill(coverTx);
await client.SubmitAndWait(coverTx, walletBroker, true);
Step-by-Step: Creating and Managing a Loan
1. Create a Loan (LoanSet)
LoanSet requires co-signing by both the broker and the borrower. See CounterpartySignature for the full signing flow.
LoanSet loanTx = new LoanSet
{
Account = walletBroker.ClassicAddress,
LoanBrokerID = brokerId,
Counterparty = walletBorrower.ClassicAddress,
PrincipalRequested = "10000000", // Number type (not drops)
};
// Requires special co-signing — see CounterpartySignature section below
TransactionSummary result = await SubmitLoanSetWithCounterpartySig(
client, loanTx, walletBroker, walletBorrower);
string loanId = GetCreatedObjectId(result, LedgerEntryType.Loan);
2. Make a Loan Payment
The borrower makes payments on the loan:
LoanPay payTx = new LoanPay
{
Account = walletBorrower.ClassicAddress,
LoanID = loanId,
Amount = new Currency { Value = "10000000", CurrencyCode = "XRP" },
};
payTx = await client.Autofill(payTx);
TransactionSummary result = await client.SubmitAndWait(payTx, walletBorrower, true);
3. Delete a Fully Repaid Loan
After the loan is fully repaid, the broker can delete it:
LoanDelete deleteTx = new LoanDelete
{
Account = walletBroker.ClassicAddress,
LoanID = loanId,
};
deleteTx = await client.Autofill(deleteTx);
await client.SubmitAndWait(deleteTx, walletBroker, true);
Important: You cannot delete a loan that has outstanding obligations (
tecHAS_OBLIGATIONS). The loan must be fully repaid first.
4. Manage Loan State
The broker can mark a loan as defaulted, impaired, or restore it:
// Mark loan as defaulted
LoanManage manageTx = new LoanManage
{
Account = walletBroker.ClassicAddress,
LoanID = loanId,
Flags = LoanManageFlags.tfLoanDefault,
};
manageTx = await client.Autofill(manageTx);
await client.SubmitAndWait(manageTx, walletBroker, true);
LoanManage flags (mutually exclusive):
tfLoanDefault— mark loan as defaultedtfLoanImpair— mark loan as impairedtfLoanUnimpair— restore loan from impaired state
5. Delete the Loan Broker
When the broker has no outstanding loans:
LoanBrokerDelete deletebrokerTx = new LoanBrokerDelete
{
Account = walletBroker.ClassicAddress,
LoanBrokerID = brokerId,
};
deletebrokerTx = await client.Autofill(deletebrokerTx);
await client.SubmitAndWait(deletebrokerTx, walletBroker, true);
CounterpartySignature (LoanSet Co-Signing)
LoanSet is unique among XRPL transactions — it requires two signatures. The broker signs the transaction as normal (TxnSignature), and the borrower provides a CounterpartySignature — an inner STObject containing the borrower's SigningPubKey and TxnSignature.
The SDK provides LoanSigningHelper and XrplWallet.SignAsLoanCounterparty() with three signing patterns, analogous to Batch signing (V1/V2/V3).
Preparation (Common to All Patterns)
using Xrpl.Wallet;
// Autofill handles fee calculation (including CounterpartySignature overhead)
loanTx = await client.Autofill(loanTx);
// PrepareForSigning sets broker's SigningPubKey and removes signature fields
JsonObject prepared = LoanSigningHelper.PrepareForSigning(loanTx, brokerWallet);
V1 — Automatic (Both Keys Available Locally)
Use when both broker and borrower wallets are available on the same device:
SignatureResult result = LoanSigningHelper.SignLoanSet(prepared, brokerWallet, borrowerWallet);
await client.SubmitRequest(result.TxBlob);
V2 — Parallel (Keys on Separate Devices, Sign Independently)
Use when broker and borrower sign independently and a third party combines the signatures:
// Device A (broker): signs the transaction normally
var brokerDict = JsonSerializer.Deserialize<Dictionary<string, object>>(
prepared.ToJsonString(), XrplJsonOptions.Default);
SignatureResult brokerSig = brokerWallet.Sign(brokerDict);
// Device B (borrower): signs as counterparty (independent copy of the payload)
var borrowerDict = JsonSerializer.Deserialize<Dictionary<string, object>>(
prepared.ToJsonString(), XrplJsonOptions.Default);
SignatureResult counterpartySig = borrowerWallet.SignAsLoanCounterparty(borrowerDict);
// Combiner: merge both signatures into a single blob
SignatureResult combined = LoanSigningHelper.CombineLoanSignatures(
brokerSig.TxBlob, counterpartySig.TxBlob);
await client.SubmitRequest(combined.TxBlob);
V3 — Sequential (Borrower Signs First, Passes to Broker)
Use in the real-world scenario where the borrower signs first and sends the partially signed blob to the broker:
// Step 1: Borrower receives prepared tx JSON, signs as counterparty
var txDict = JsonSerializer.Deserialize<Dictionary<string, object>>(
prepared.ToJsonString(), XrplJsonOptions.Default);
SignatureResult withCounterparty = borrowerWallet.SignAsLoanCounterparty(txDict);
// withCounterparty.TxBlob is sent to the broker (e.g. via API, QR code, etc.)
// Step 2: Broker receives the partially signed blob, adds TxnSignature
SignatureResult fullySigned = LoanSigningHelper.BrokerSign(
withCounterparty.TxBlob, brokerWallet);
await client.SubmitRequest(fullySigned.TxBlob);
Important: Do not use
brokerWallet.Sign()on a partially signed LoanSet blob — it does not handleCounterpartySignaturecorrectly. Always useLoanSigningHelper.BrokerSign()for the V3 pattern.
Key Points
- Both parties sign the same preimage (the transaction serialized for signing, without any signature fields)
- The signing preimage uses the broker's
SigningPubKey(the submitting account) CounterpartySignatureis an STObject withisSigningField = false— it is excluded from the signing preimageAutofillautomatically calculates the correct fee for LoanSet (includes CounterpartySignature overhead)
Ledger Objects
The lending protocol creates the following ledger objects:
| Object | Description | Created By |
|---|---|---|
Vault |
Holds lending assets | VaultCreate |
LoanBroker |
Lending entity with parameters and cover | LoanBrokerSet |
Loan |
Active loan between broker and borrower | LoanSet |
LoanBroker Fields
| Field | Type | Description |
|---|---|---|
Account |
AccountID | Broker account |
Asset |
Issue | Primary lending asset |
Asset2 |
Issue | Secondary asset (collateral) |
CoverAvailable |
Number | Available cover amount |
AssetsAvailable |
Number | Available assets for lending |
AssetsTotal |
Number | Total assets in vault |
DebtTotal |
Number | Total outstanding debt |
DebtMaximum |
Number | Maximum allowed debt |
CoverRateMinimum |
UInt32 | Minimum cover rate (e.g. 15000 = 150%) |
CoverRateLiquidation |
UInt32 | Liquidation threshold |
ManagementFeeRate |
UInt16 | Fee rate (0-10000 basis points) |
Loan Fields
| Field | Type | Description |
|---|---|---|
Account |
AccountID | Borrower account |
Counterparty |
AccountID | Broker account |
LoanBrokerID |
Hash256 | Reference to loan broker |
PrincipalRequested |
Number | Original loan amount |
PrincipalOutstanding |
Number | Remaining principal |
TotalValueOutstanding |
Number | Total amount owed |
InterestRate |
UInt32 | Annual interest rate |
PaymentInterval |
UInt32 | Seconds between payments |
PaymentTotal |
UInt32 | Total number of payments |
PaymentRemaining |
UInt32 | Remaining payments |
StartDate |
UInt32 | Loan start (Ripple epoch) |
Querying Loan State
Use account_objects to retrieve loans owned by an account:
using Xrpl.Models.Methods;
var request = new AccountObjectsRequest(walletBorrower.ClassicAddress);
var response = await client.AccountObjects(request);
foreach (var obj in response.AccountObjectList)
{
if (obj.LedgerEntryType == LedgerEntryType.Loan)
{
Console.WriteLine($"Loan: {obj}");
}
}
Number Type
Loan fields use the XRPL Number type instead of Amount. The Number type is a 12-byte format:
- 8 bytes — signed int64 mantissa (big-endian)
- 4 bytes — signed int32 exponent (big-endian)
The actual value = mantissa × 10^exponent.
Normalization
Non-zero values are normalized so that the mantissa is in the range [10^18, long.MaxValue]. Zero is represented as mantissa=0, exponent=Int32.MinValue.
Example
The value 10000000000000 (10^13) is normalized to:
- Mantissa:
1000000000000000000(10^18) - Exponent:
-5 - Binary:
0x0DE0B6B3A7640000 FFFFFFFB(12 bytes)
In Transaction Models
Number fields are represented as string in C# models (e.g., PrincipalRequested = "10000000"). The binary codec handles normalization and serialization automatically.
Common Errors
| Error Code | Cause | Solution |
|---|---|---|
tecINSUFFICIENT_FUNDS |
Broker vault lacks funds for the loan | Deposit more assets into the vault via VaultDeposit |
tecHAS_OBLIGATIONS |
Cannot delete a loan with outstanding balance | Fully repay the loan via LoanPay before deleting |
tecNO_ENTRY |
Referenced LoanBrokerID or LoanID not found | Verify the ID is correct and the object exists |
tecNO_PERMISSION |
Action not allowed (e.g., overpayment without flag) | Check that the account has the required permissions |
tecINSUFFICIENT_PAYMENT |
Payment amount too small | Increase the payment amount |
temBAD_SIGNER |
Missing or invalid CounterpartySignature | Ensure borrower co-signs the LoanSet (see co-signing section) |
telINSUF_FEE_P |
Fee too low after adding CounterpartySignature | Re-run Autofill or increase the fee before submit |
invalid SerialIter geti32 |
Number type encoding error | Ensure Number fields are 12 bytes (8 mantissa + 4 exponent) |
Best Practices
Fund the vault before creating loans — create the vault, deposit assets (
VaultDeposit), create the broker (LoanBrokerSet), deposit cover (LoanBrokerCoverDeposit), then create loans.Autofill handles LoanSet fee —
Autofillautomatically calculates the correct fee includingCounterpartySignatureoverhead. No manual fee adjustment is needed.Filter by LedgerEntryType when extracting IDs —
GetCreatedObjectIdshould filter by specific type (LedgerEntryType.Vault,LedgerEntryType.LoanBroker,LedgerEntryType.Loan) to avoid picking upDirectoryNodeor other created entries.Fully repay before deleting —
LoanDeletefails withtecHAS_OBLIGATIONSif any balance remains. UseLoanPayto clear the debt first.Use reasonable PrincipalRequested values — ensure the vault has sufficient assets to cover the requested principal. The value is in the Number type format.
Validate all results — always check
TransactionResultfor success:if (result.Meta?.TransactionResult != "tesSUCCESS") throw new Exception($"Transaction failed: {result.Meta?.TransactionResult}");LoanManage flags are mutually exclusive — only set one of
tfLoanDefault,tfLoanImpair, ortfLoanUnimpairper transaction.LoanPay flags are mutually exclusive —
tfLoanOverpayment,tfLoanFullPayment, andtfLoanLatePaymentcannot be combined. Note thattfLoanOverpaymentmay require specific broker/loan configuration to be permitted.Testing on standalone — LendingProtocol amendment must be enabled on rippled 3.1.0+:
{ "command": "feature", "feature": "LendingProtocol", "vetoed": false }