Lesson 8: Your First Hook - Accept All Transactions
Learning Objectives
Write a minimal Hook that compiles and deploys successfully
Understand every component of the basic Hook structure
Add trace logging to observe Hook execution
Extract and display transaction information
Deploy and test your Hook on testnet
You've learned the philosophy, architecture, C fundamentals, and API. Now we write code. This lesson is practical—follow along in Hooks Builder. Type the code yourself rather than copying; you'll learn more.
- Minimal Hook (just accept)
- Logging Hook (add trace)
- Informative Hook (extract transaction data)
- Complete first Hook (production-ready structure)
The smallest Hook that compiles and works:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
accept(SBUF("OK"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
Let's understand each line:
Includes the Hooks API definitions
Provides all functions (accept, rollback, etc.)
Provides macros (SBUF, GUARD, etc.)
Provides sfCodes (sfAccount, sfAmount, etc.)
Function signature for main Hook entry point
int64_treturn type (64-bit integer)reservedparameter (currently unused, for future)This function runs when transactions affect your account
Accept the transaction
SBUF("OK")expands to pointer and length of "OK"0is the return codeExecution stops here—nothing after runs
Return value (won't be reached after accept)
Convention: return 0 for success
Callback function (runs if emitted transactions fail)
Required even if empty
We just return 0 (nothing to do)
Exercise: Deploy the Minimal Hook
- Open Hooks Builder (hooks.xrpl.org)
- Create or select a funded testnet account
- Clear the editor and type the minimal Hook:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
accept(SBUF("OK"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
- Click "Compile"
- Verify "Compilation successful" message
- Click "Deploy"
- Wait for transaction confirmation
Send a payment to your Hook account:
Create or use a second testnet account
Send 10 XRP to your Hook account
Transaction should succeed
Check transaction in explorer—should show Hook execution
Payment arrives at your account
Hook fires (hook() function runs)
accept() is called
Transaction proceeds to ledger
Your account receives the XRP
Let's see what's happening:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
// Log that we're executing
trace(SBUF("Hook executing!"), 0);
// Accept the transaction
accept(SBUF("Transaction accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
New line explained:
trace()outputs debug information- First two params: message (via SBUF)
- Third param: a number to display (0 here)
- Output appears in Hooks Builder console
- Replace your code with the logging version
- Compile and deploy
- Send another payment
- Check the Output tab in Hooks Builder
- You should see "Hook executing!" in the logs
The minimal Hook technically works, but it's missing the initial guard. Let's add it properly:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
_g(1, 1); // Guard for main function body
trace(SBUF("Hook executing!"), 0);
accept(SBUF("Transaction accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
```
- First param: guard ID (1)
- Second param: max iterations (1)
- This tells the system: "This code path runs at most once"
- Good practice even in simple Hooks
Let's display the payment amount:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
_g(1, 1);
trace(SBUF("Hook executing!"), 0);
// Get the payment amount (in drops)
int64_t amount_drops = otxn_amount();
trace(SBUF("Amount in drops:"), amount_drops);
// Convert to XRP for display
int64_t amount_xrp = amount_drops / 1000000;
trace(SBUF("Amount in XRP:"), amount_xrp);
accept(SBUF("Transaction accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
New concepts:
Gets the XRP amount from the transaction
Returns value in drops (1 XRP = 1,000,000 drops)
Returns negative for IOU payments
Simple integer division to get XRP
Note: loses fractional XRP (integer division)
For display only—keep drops for calculations
Let's also log who sent the payment:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
_g(1, 1);
trace(SBUF("Hook executing!"), 0);
// Get amount
int64_t amount_drops = otxn_amount();
trace(SBUF("Amount in drops:"), amount_drops);
// Get sender account ID
uint8_t sender[20];
int64_t sender_len = otxn_field(SBUF(sender), sfAccount);
if (sender_len == 20) {
trace(SBUF("Sender found, first byte:"), sender[0]);
} else {
trace(SBUF("Could not get sender"), sender_len);
}
accept(SBUF("Transaction accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
New concepts:
Buffer to hold account ID
Account IDs are always 20 bytes
otxn_field()extracts a field from the transactionsfAccountis the source accountReturns number of bytes written (should be 20)
Always check return values!
20 means we got the account ID successfully
Different transactions trigger Hooks. Let's identify them:
#include "hookapi.h"
int64_t hook(uint32_t reserved) {
_g(1, 1);
trace(SBUF("Hook executing!"), 0);
// Get transaction type
int64_t type = otxn_type();
trace(SBUF("Transaction type:"), type);
// Get amount (only meaningful for payments)
int64_t amount = otxn_amount();
trace(SBUF("Amount:"), amount);
accept(SBUF("Transaction accepted"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
- 0 = Payment
- 3 = AccountSet
- 7 = OfferCreate
- 20 = TrustSet
- 22 = SetHook
Here's a well-structured first Hook with all the basics:
/**
* First Hook - Accept All Transactions
*
* This Hook accepts all transactions while logging useful information.
* It demonstrates:
* - Basic Hook structure
* - Transaction data extraction
* - Proper error handling
* - Debug logging
*/
#include "hookapi.h"
// Transaction type constants (for readability)
#define TT_PAYMENT 0
#define TT_ACCOUNT_SET 3
#define TT_TRUST_SET 20
int64_t hook(uint32_t reserved) {
// Main function guard
_g(1, 1);
// ============================================
// SECTION 1: Log entry
// ============================================
trace(SBUF("=== Hook Execution Start ==="), 0);
// ============================================
// SECTION 2: Get transaction type
// ============================================
int64_t txn_type = otxn_type();
trace(SBUF("Transaction type:"), txn_type);
// ============================================
// SECTION 3: Get sender account
// ============================================
uint8_t sender_accid[20];
int64_t sender_len = otxn_field(SBUF(sender_accid), sfAccount);
if (sender_len != 20) {
trace(SBUF("Warning: Could not get sender"), sender_len);
} else {
trace(SBUF("Sender first byte:"), sender_accid[0]);
}
// ============================================
// SECTION 4: Get amount (if payment)
// ============================================
if (txn_type == TT_PAYMENT) {
int64_t amount = otxn_amount();
if (amount < 0) {
trace(SBUF("IOU payment (not XRP)"), 0);
} else {
trace(SBUF("XRP amount (drops):"), amount);
// Calculate XRP (integer part only)
int64_t xrp = amount / 1000000;
trace(SBUF("XRP amount (whole):"), xrp);
}
}
// ============================================
// SECTION 5: Get transaction ID
// ============================================
uint8_t txn_id[32];
int64_t id_len = otxn_id(SBUF(txn_id), 0);
if (id_len == 32) {
trace(SBUF("Transaction ID first byte:"), txn_id[0]);
}
// ============================================
// SECTION 6: Accept the transaction
// ============================================
trace(SBUF("=== Hook Execution End ==="), 0);
accept(SBUF("Transaction processed successfully"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
// Callback for emission failures
// Nothing to do in this simple Hook
return 0;
}
- Always document what your Hook does
- List what it demonstrates
- Future you will thank present you
- Define constants instead of magic numbers
- Makes code self-documenting
- Easier to maintain
- Clear visual separation
- Easy to navigate
- Good for debugging
- Don't assume all transactions are payments
- Handle different types appropriately
Test cases to verify:
XRP Payment:
Different Amount:
From Different Account:
Non-Payment Transaction:
- Missing `#include "hookapi.h"`
- Missing semicolon somewhere
- Check the line before the error
- Missing GUARD() in a loop
- Or missing _g() at function start (some versions)
- Hook binary invalid
- Re-compile and check for warnings
- Account has master key disabled
- Use a different account
- Check you're sending TO the Hook account
- Check the Hook was deployed successfully
- Look at account info in explorer
- Trace output appears in Hooks Builder console
- Not visible in regular explorer
- Make sure you're testing from Hooks Builder
- Check trace output for what happened
- Look for rollback messages
- Verify Hook compiled latest version
Now that you have a working Hook, try these extensions:
Add code to log the destination account:
// Get destination account (for payments)
if (txn_type == TT_PAYMENT) {
uint8_t dest_accid[20];
int64_t dest_len = otxn_field(SBUF(dest_accid), sfDestination);
if (dest_len == 20) {
trace(SBUF("Destination first byte:"), dest_accid[0]);
}
}
Log the current ledger sequence:
// Get current ledger
int64_t ledger = ledger_seq();
trace(SBUF("Current ledger:"), ledger);Check if someone is sending to themselves:
// Check for self-payment
if (txn_type == TT_PAYMENT && sender_len == 20 && dest_len == 20) {
int same = 1;
for (int i = 0; GUARD(20), i < 20; ++i) {
if (sender_accid[i] != dest_accid[i]) {
same = 0;
break;
}
}
if (same) {
trace(SBUF("Self-payment detected!"), 0);
}
}
✅ Minimal Hooks work. Even the simplest Hook compiles, deploys, and executes.
✅ Trace logging enables understanding. You can see exactly what your Hook does.
✅ The API works as documented. otxn_amount(), otxn_field(), etc. behave predictably.
⚠️ Performance of trace() in production. Consider removing trace calls for efficiency.
⚠️ Behavior across all transaction types. We've mainly tested payments.
🔴 Deploying without testing. Always test on testnet first.
🔴 Assuming successful deployment. Verify Hook appears in account info.
🔴 Ignoring return values. Check that field extractions succeed.
Your first Hook is working. It doesn't do anything useful yet—just accepts and logs—but it's a foundation. Every complex Hook starts here: accept, log, extract data. Master this pattern and you can build anything the API supports.
Assignment: Create an enhanced version of the first Hook with additional features.
Requirements:
Your Hook must:
Accept all transactions
Log transaction type
Log amount (for payments)
Log sender account ID (all 20 bytes as separate trace calls for first 3 bytes)
Log destination account ID (if payment)
Log current ledger sequence
Log transaction ID (first 4 bytes)
Detect if transaction is XRP vs IOU
Detect self-payments
Log different messages for different transaction types (at least 3 types)
Clear comments explaining each section
Defined constants for transaction types
Proper error handling for all field extractions
Clean, readable structure
Complete Hook source code
Screenshot of successful compilation
Screenshot of deployment transaction
Screenshots showing trace output for at least 3 different test transactions
Functionality completeness (40%)
Code quality and structure (25%)
Error handling (15%)
Testing evidence (20%)
Time investment: 2-3 hours
Value: Working foundation for all future Hook development
Knowledge Check
Question 1 of 4What happens if you forget to call accept() or rollback() in your Hook?
- Hooks Builder example library
- GitHub: XRPL-Labs/xrpl-hooks examples
- XRPL documentation on transaction types
- sfcodes.h in hooks-toolkit
For Next Lesson:
Now that you can accept everything, it's time to reject some things. Lesson 9 covers guards and loops—essential for conditional logic and iteration.
End of Lesson 8
Total words: ~4,200
Estimated completion time: 55 minutes reading + 2-3 hours for deliverable
Key Takeaways
Start minimal, add incrementally.
Get something working, then add features.
Use trace() liberally during development.
Remove or reduce for production.
Check all return values.
Field extraction can fail; handle gracefully.
Structure your code clearly.
Comments, sections, and constants improve maintainability.
Test after every change.
Recompile, redeploy, and verify behavior. ---