# Implementing Custom Handlers

### Building Your First RPC Handler from Scratch

[← Back to Building and Integrating Custom RPC Handlers](/core-dev-bootcamp/module07.md)

***

## Introduction

Now that you understand the RPC architecture and request flow from [**Building and Integrating Custom RPC Handlers**](/core-dev-bootcamp/module06.md), it's time to **build your own custom handler**. This is where theory meets practice—you'll write actual C++ code that integrates seamlessly with Rippled's RPC system.

**Prerequisites**: Make sure you've completed this topics:

* [RPC Handler Architecture](/core-dev-bootcamp/module06/rpc-handler-architecture.md)
* [Request and Response Flow](/core-dev-bootcamp/module06/request-response-flow.md)
* [Authentication and Authorization](/core-dev-bootcamp/module06/authentication-authorization.md)
* [Error Handling and Validation](/core-dev-bootcamp/module06/error-handling-validation.md)

In this section, you'll learn the step-by-step process of creating a custom RPC handler, from defining the function signature to handling edge cases and returning properly formatted responses.

***

## Handler Implementation Checklist

Before you start coding, here's what you need to prepare:

✅ **Define the handler's purpose** — What query or operation will it perform? ✅ **Identify required parameters** — What inputs does it need from the client? ✅ **Determine permission level** — What role should be required (USER, ADMIN, etc.)? ✅ **Specify ledger requirements** — Does it need current, validated, or specific ledgers? ✅ **Plan error scenarios** — What could go wrong, and how will you handle it? ✅ **Design the response format** — What data structure will you return?

***

## Step-by-Step Implementation Guide

### Step 1: Create the Handler File

Create a new file in the handlers directory:

**File**: `src/xrpld/rpc/handlers/MyCustomHandler.cpp`

```cpp
//------------------------------------------------------------------------------
/*
    This file is part of rippled: https://github.com/ripple/rippled
    Copyright (c) 2024 Ripple Labs Inc.

    Permission to use, copy, modify, and/or distribute this software for any
    purpose  with  or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.
*/
//==============================================================================

#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>

namespace ripple {

// Forward declaration
Json::Value doMyCustomCommand(RPC::JsonContext& context);

} // namespace ripple
```

***

### Step 2: Define the Handler Function

Implement the handler with the standard signature:

```cpp
Json::Value doMyCustomCommand(RPC::JsonContext& context)
{
    // Create the result object
    Json::Value result;

    // Step 1: Validate input parameters
    if (!context.params.isMember(jss::account)) {
        return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
    }

    // Step 2: Parse and validate account
    std::string const accountStr = context.params[jss::account].asString();

    auto const account = parseBase58<AccountID>(accountStr);
    if (!account) {
        return rpcError(rpcACT_MALFORMED, "Invalid account format");
    }

    // Step 3: Get ledger to query
    std::shared_ptr<ReadView const> ledger;
    auto const ledgerResult = RPC::lookupLedger(ledger, context);

    if (!ledger) {
        return ledgerResult;
    }

    // Step 4: Query the ledger
    auto const sleAccount = ledger->read(keylet::account(*account));

    if (!sleAccount) {
        return rpcError(rpcACT_NOT_FOUND, "Account not found");
    }

    // Step 5: Build the response
    result[jss::account] = accountStr;
    result[jss::ledger_index] = ledger->info().seq;
    result[jss::validated] = ledger->isImmutable();

    // Add custom data
    result["balance"] = to_string(sleAccount->getFieldAmount(sfBalance));
    result["sequence"] = sleAccount->getFieldU32(sfSequence);

    return result;
}
```

***

### Step 3: Register the Handler

Add your handler to the central table:

**File**: `src/xrpld/rpc/handlers/Handlers.cpp`

```cpp
{
    "my_custom_command",
    {
        &doMyCustomCommand,
        Role::USER,
        RPC::NEEDS_CURRENT_LEDGER
    }
}
```

***

### Step 4: Declare in Header (Optional)

For better code organization:

**File**: `src/xrpld/rpc/handlers/Handlers.h`

```cpp
Json::Value doMyCustomCommand(RPC::JsonContext&);
```

***

## Understanding the JsonContext

The `JsonContext` object is your gateway to Rippled's internals:

### Essential Fields

```cpp
struct JsonContext {
    // Request parameters from the client
    Json::Value params;

    // Application instance (access to all services)
    Application& app;

    // Resource consumption tracking
    Resource::Consumer& consumer;

    // Caller's permission level
    Role role;

    // Current or requested ledger view
    std::shared_ptr<ReadView const> ledger;

    // Network operations interface
    NetworkOPs& netOps;

    // Ledger management interface
    LedgerMaster& ledgerMaster;

    // API version (for backward compatibility)
    unsigned int apiVersion;
};
```

### Accessing Services

```cpp
// Get the current ledger
auto currentLedger = context.ledgerMaster.getCurrentLedger();

// Get network info
auto serverState = context.netOps.getOperatingMode();

// Access configuration
auto const& config = context.app.config();

// Get transaction pool
auto& txPool = context.app.openLedger();
```

***

## Input Validation Patterns

Proper input validation is critical for security and reliability:

### Validate Required Fields

```cpp
if (!context.params.isMember(jss::account)) {
    return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
}

if (!context.params[jss::account].isString()) {
    return rpcError(rpcINVALID_PARAMS, "'account' must be a string");
}
```

### Parse Account Addresses

```cpp
auto const account = parseBase58<AccountID>(
    context.params[jss::account].asString()
);

if (!account) {
    return rpcError(rpcACT_MALFORMED, "Invalid account address");
}
```

### Validate Numeric Parameters

```cpp
if (context.params.isMember("limit")) {
    if (!context.params["limit"].isUInt()) {
        return rpcError(rpcINVALID_PARAMS, "'limit' must be a positive integer");
    }

    unsigned int limit = context.params["limit"].asUInt();

    if (limit == 0 || limit > 1000) {
        return rpcError(rpcINVALID_PARAMS, "'limit' must be between 1 and 1000");
    }
}
```

### Validate Currency Codes

```cpp
if (context.params.isMember("currency")) {
    std::string const currencyStr = context.params["currency"].asString();

    if (!to_currency(currencyStr)) {
        return rpcError(rpcINVALID_PARAMS, "Invalid currency code");
    }
}
```

***

## Ledger Access Patterns

Most handlers need to access ledger data:

### Using RPC::lookupLedger

The standard way to get a ledger:

```cpp
std::shared_ptr<ReadView const> ledger;
auto const result = RPC::lookupLedger(ledger, context);

if (!ledger) {
    return result;  // Return the error response
}

// Now you can safely use 'ledger'
```

This helper function:

* Parses `ledger_index` or `ledger_hash` from params
* Handles special values like `"validated"`, `"current"`, `"closed"`
* Returns appropriate error if ledger not found
* Populates the `ledger` shared pointer

### Manual Ledger Selection

For advanced use cases:

```cpp
std::shared_ptr<ReadView const> ledger;

if (context.params.isMember(jss::ledger_index)) {
    auto const ledgerIndex = context.params[jss::ledger_index].asUInt();
    ledger = context.ledgerMaster.getLedgerBySeq(ledgerIndex);
} else {
    // Default to current ledger
    ledger = context.ledgerMaster.getCurrentLedger();
}

if (!ledger) {
    return rpcError(rpcLGR_NOT_FOUND, "Ledger not found");
}
```

***

## Reading Ledger Objects

Once you have a ledger, you can query its state:

### Read an Account

```cpp
auto const sleAccount = ledger->read(keylet::account(accountID));

if (!sleAccount) {
    return rpcError(rpcACT_NOT_FOUND, "Account not found");
}

// Access account fields
STAmount balance = sleAccount->getFieldAmount(sfBalance);
std::uint32_t sequence = sleAccount->getFieldU32(sfSequence);
AccountID account = sleAccount->getAccountID(sfAccount);
```

### Read Trust Lines

```cpp
auto const sleRippleState = ledger->read(
    keylet::line(accountID, issuerID, currency)
);

if (sleRippleState) {
    STAmount balance = sleRippleState->getFieldAmount(sfBalance);
    STAmount limit = sleRippleState->getFieldAmount(sfHighLimit);
}
```

### Read Offers

```cpp
auto const sleOffer = ledger->read(keylet::offer(accountID, sequence));

if (sleOffer) {
    STAmount takerPays = sleOffer->getFieldAmount(sfTakerPays);
    STAmount takerGets = sleOffer->getFieldAmount(sfTakerGets);
}
```

### Iterate Directory

```cpp
auto const dir = ledger->read(keylet::ownerDir(accountID));

if (dir) {
    for (auto const& index : dir->getFieldV256(sfIndexes)) {
        auto const sle = ledger->read(keylet::child(index));
        // Process each owned object
    }
}
```

***

## Response Construction

Build well-structured JSON responses:

### Basic Response

```cpp
Json::Value result;
result[jss::account] = to_string(accountID);
result[jss::ledger_index] = ledger->info().seq;
result[jss::validated] = ledger->isImmutable();

return result;
```

### Nested Objects

```cpp
Json::Value accountData;
accountData[jss::Account] = to_string(accountID);
accountData[jss::Balance] = to_string(balance);
accountData[jss::Sequence] = sequence;

result[jss::account_data] = accountData;
```

### Arrays

```cpp
Json::Value lines(Json::arrayValue);

for (auto const& line : trustLines) {
    Json::Value lineJson;
    lineJson["account"] = to_string(line.account);
    lineJson["balance"] = to_string(line.balance);
    lineJson["currency"] = to_string(line.currency);

    lines.append(lineJson);
}

result["lines"] = lines;
```

***

## Common Helper Functions

Rippled provides many utility functions to simplify handler implementation:

### Account Parsing

```cpp
// From RPC::detail::RPCHelpers.h
auto account = RPC::accountFromString(accountStr);
if (!account) {
    return rpcError(rpcACT_MALFORMED);
}
```

### Amount Parsing

```cpp
STAmount amount;
if (!amountFromJsonNoThrow(amount, params["amount"])) {
    return rpcError(rpcINVALID_PARAMS, "Invalid amount");
}
```

### Currency/Issuer Extraction

```cpp
Currency currency;
if (!to_currency(currency, params["currency"].asString())) {
    return rpcError(rpcINVALID_PARAMS, "Invalid currency");
}
```

### Ledger Range Validation

```cpp
auto [minLedger, maxLedger] = RPC::getLedgerRange(context, params);
```

***

## Real-World Example: Custom Balance Checker

Let's implement a handler that returns XRP balance with reserve calculations:

```cpp
Json::Value doGetAccountBalance(RPC::JsonContext& context)
{
    // Validate account parameter
    if (!context.params.isMember(jss::account)) {
        return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
    }

    auto const account = parseBase58<AccountID>(
        context.params[jss::account].asString()
    );

    if (!account) {
        return rpcError(rpcACT_MALFORMED, "Invalid account address");
    }

    // Get ledger
    std::shared_ptr<ReadView const> ledger;
    auto const ledgerResult = RPC::lookupLedger(ledger, context);

    if (!ledger) {
        return ledgerResult;
    }

    // Read account state
    auto const sleAccount = ledger->read(keylet::account(*account));

    if (!sleAccount) {
        return rpcError(rpcACT_NOT_FOUND, "Account not found");
    }

    // Get balance
    STAmount balance = sleAccount->getFieldAmount(sfBalance);

    // Calculate reserves
    auto const& fees = ledger->fees();
    std::uint32_t ownerCount = sleAccount->getFieldU32(sfOwnerCount);

    XRPAmount baseReserve = fees.accountReserve(0);
    XRPAmount ownerReserve = fees.accountReserve(ownerCount);
    XRPAmount totalReserve = baseReserve + ownerReserve;

    // Build response
    Json::Value result;
    result[jss::account] = to_string(*account);
    result[jss::ledger_index] = ledger->info().seq;
    result[jss::validated] = ledger->isImmutable();
    result["balance"] = to_string(balance);
    result["available_balance"] = to_string(balance - totalReserve);
    result["base_reserve"] = to_string(baseReserve);
    result["owner_reserve"] = to_string(ownerReserve);
    result["owner_count"] = ownerCount;

    return result;
}
```

**Registration**:

```cpp
{
    "get_account_balance",
    {
        &doGetAccountBalance,
        Role::USER,
        RPC::NEEDS_CURRENT_LEDGER
    }
}
```

**Example Request**:

```json
{
    "method": "get_account_balance",
    "params": [{
        "account": "rN7n7otQDd6FczFgLdlqtyMVrn3NnrcVXs",
        "ledger_index": "validated"
    }]
}
```

**Example Response**:

```json
{
    "result": {
        "account": "rN7n7otQDd6FczFgLdlqtyMVrn3NnrcVXs",
        "ledger_index": 12345,
        "validated": true,
        "balance": "1000000000",
        "available_balance": "990000000",
        "base_reserve": "10000000",
        "owner_reserve": "0",
        "owner_count": 0
    }
}
```

***

### Conclusion

Implementing custom RPC handlers transforms your understanding of Rippled's architecture into practical skills. By following the standard function signature, validating inputs thoroughly, leveraging helper functions, and building structured responses, you create handlers that integrate seamlessly with the existing codebase. The step-by-step process—from file creation through registration to testing—provides a repeatable pattern for extending Rippled's API capabilities with your own functionality.

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module07/implementing-custom-handlers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
