How to run a local rippled/xrpld node in standalone mode for development and integration testing — and why a freshly started node answers temDISABLED to almost everything until you activate amendments.
For a production node connected to the live network see the Mainnet Node Guide.
TL;DR:
rippled -a --startalone is not enough. A bare standalone node starts with (almost) no amendments enabled, soAMMCreate,NFTokenMint,MPTokenIssuanceCreateand most modern transaction types fail withtemDISABLED. Use the ready-made Docker stands from.ci-config/in this repository, or configure amendment activation yourself as described below.
The repository ships two ready-to-use Docker Compose stands. Both include a ledger-acceptor sidecar that closes a ledger every 4 seconds.
docker compose -f .ci-config/docker-compose.ci.yml up -d
# run integration tests
dotnet test Tests/Xrpl.Tests/Xrpl.Tests.csproj --settings test.runsettings --filter "TestI"
docker compose -f .ci-config/docker-compose.ci.yml downAmendments that exist only on the rippled develop branch (e.g. BatchV1_1, PermissionDelegationV1_1) are absent from release images. The second stand builds a pinned nightly xrpld and enables them at genesis:
# both stands use the same ports — stop the first one before starting this one
docker compose -f .ci-config/docker-compose.ci.yml down
docker compose -f .ci-config/docker-compose.batchv11.yml up -d --build
dotnet test Tests/Xrpl.Tests/Xrpl.Tests.csproj --settings test.runsettings --filter "TestIBatch|TestIDelegateSet"
docker compose -f .ci-config/docker-compose.batchv11.yml down| Port | Purpose |
|---|---|
| 5005 | JSON-RPC (admin) |
| 5006 | JSON-RPC (admin, used by the ledger-acceptor) |
| 6006 | WebSocket (admin) — integration tests connect here (ws://localhost:6006) |
| 6007 | WebSocket with admin_user/admin_password — used only by TestIAdminCredentials |
All ports are published on loopback only (127.0.0.1): the stand is reachable from the same machine and not from the network. That is deliberate — every stanza in the config sets admin = 0.0.0.0, so each port hands the admin role (stop, connect, feature, validation_seed) to anyone who can reach it, and only 6007 asks for credentials. If you genuinely need to reach the node from another host, widen the binding in the compose file as a conscious decision, not to make something work.
Port 6007 ([port_ws_admin_auth]) exists to prove that admin commands over WS open up only when ClientOptions.AdminUser/AdminPassword are set. rippled carries those credentials inside the request JSON — it never checks a Basic header on the ws handshake (its user/password port settings apply to plain HTTP JSON-RPC only). No other test uses this port.
Every feature gated by an amendment (AMM, NFTs, MPTs, Batch, …) checks at transaction level whether the amendment is enabled in the ledger. On mainnet amendments are enabled by validator voting; in standalone mode there is no voting — no validators, no majorities, so nothing ever activates on its own.
A bare rippled -a --start creates a genesis ledger where only amendments with VoteBehavior::DefaultYes (a handful of fixes) are active. Everything else is inactive, and the node answers:
{ "engine_result": "temDISABLED", "engine_result_message": "The transaction requires logic that is currently disabled." }This is not a bug in your transaction — the node simply does not have the feature turned on.
# status of one amendment (admin RPC)
curl -s -X POST http://localhost:5005/ -d '{"method":"feature","params":[{"feature":"AMM"}]}'
# => { "...": { "enabled": true|false, "supported": true, ... } }
# full list of enabled amendments straight from the ledger
curl -s -X POST http://localhost:5005/ -d '{"method":"ledger_entry","params":[{"index":"7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4","ledger_index":"validated"}]}'7DB0788C…D6EF4 is the well-known index of the Amendments ledger object. If your amendment's id is not in its Amendments array — you will get temDISABLED.
An amendment id is sha512half of its name:
import hashlib
hashlib.sha512(b"AMM").digest()[:32].hex().upper()
# 8CC0774A3BF66D1D22E76BBDA8E8A232E6B6313834301B3B23E8601196AE6455The config section that does this depends on the rippled version:
Plain amendment names, one per line. See .ci-config/rippled.cfg for a full working set aligned with 3.2.0:
[features]
AMM
Clawback
MPTokensV1
NFTokenMintOffer
...Started with rippled -a --start, the node enables everything listed at genesis.
Pitfalls:
- An unknown name (a typo, or an amendment this binary doesn't have) makes the node crash at startup with
Unknown feature: X in config file. - Retired amendments (pre-2.0 ones like
MultiSign,Escrow,DepositAuth,fix1368, …) are permanently baked into the protocol and must not be listed — newer binaries reject them as unknown.
On the develop branch the [features] config section no longer registers genesis up-votes. Genesis amendments come from the [amendments] section with lines of the form <hash> <name>:
[amendments]
8CC0774A3BF66D1D22E76BBDA8E8A232E6B6313834301B3B23E8601196AE6455 AMM
9F287AED3CDB50A7BD1ACEC24296A30C9B5230CCD136219317AC790E3B884377 BatchV1_1
...See .ci-config/rippled.batchv11.cfg for a complete example. Note that the feature admin RPC cannot activate an amendment on a running standalone node either — with VoteBehavior::DefaultNo an un-vetoed amendment still votes "no", and there is no voting anyway. The [amendments] section at --start is the only way.
- On
develop, rippled was renamed toxrpld; the nightly apt channel atrepos.ripple.compublishes thexrpldpackage. - Pin the version (
ARG XRPLD_VERSIONin.ci-config/Dockerfile.nightly): the build-timestamp format shrank from 14 to 12 digits in mid-2026, so Debian version ordering ranks older builds above newer ones and an unpinnedapt install xrpldfetches a stale build.
In standalone mode ledgers do not close by themselves. Every transaction stays in the open ledger until you call:
curl -s -X POST http://localhost:5006/ -d '{"method":"ledger_accept"}'The compose stands run a ledger-acceptor sidecar doing this every 4 seconds, so SubmitAndWait works out of the box. If you run a node manually and forget this, transactions will sit in terQUEUED/pending state forever.
There is no faucet in standalone mode. The genesis account holds all 100 billion XRP:
| Address | rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh |
| Seed | snoPBrXtMeMyMHUVTgbuqAfg1SUTb (the well-known masterpassphrase seed) |
Fund test wallets with a regular Payment from it — see Tests/Xrpl.Tests/Integration/Utils.cs (FundAccount) for a reference implementation with retries.
XrplWallet master = XrplWallet.FromSeed("snoPBrXtMeMyMHUVTgbuqAfg1SUTb", null, "secp256k1");Note the genesis key is secp256k1, not Ed25519.
| Symptom | Cause | Fix |
|---|---|---|
temDISABLED on AMM/NFT/MPT/Batch/… |
Amendment not enabled on the node | Activate at genesis: [features] (≤3.2.x) or [amendments] (develop); use the .ci-config stands |
Node crashes: Unknown feature: X in config file |
Name typo, amendment missing from this binary, or a retired amendment listed | Fix the name; match the list to the binary version; remove retired amendments |
Node crashes: Invalid entry 'X' in [amendments] |
[amendments] lines must be <hash> <name>, not bare names |
Prefix each name with its sha512half hash |
actNotFound / terNO_ACCOUNT |
Test account never funded | Send a Payment from the genesis account first |
Transaction stuck, SubmitAndWait times out |
No one calls ledger_accept |
Run the ledger-acceptor sidecar (included in the compose stands) |
feature RPC shows "vetoed": false but "enabled": false forever |
Un-vetoing is not voting; standalone has no voting at all | Amendments activate only via the genesis config sections |
| Amendment-gated integration tests are skipped (inconclusive) | AmendmentGuard detected the amendment is inactive on this node |
Expected on the release stand; run the nightly stand for those tests |
Amendment-dependent test classes (e.g. TestIBatch, TestIDelegateSet) use Tests/Xrpl.Tests/Integration/AmendmentGuard.cs: ClassInitialize reads the Amendments ledger object and marks the class inconclusive when the required amendment is absent. This keeps CI (release image) green while the same tests run for real against the nightly stand. To gate a new test class, add the amendment id constant to AmendmentGuard and call Assert.Inconclusive from TestInitialize when inactive.