This guide is for diagnosing and operating the backend nodes in this repo after they have been built or deployed. It focuses on the failures that show up most often in this codebase:
- startup and configuration failures
- stream-registration failures
- routing failures
- deposit and refill issues
- payment-path failures
It complements the usage guide in backend-usage.md and the implementation guide in backend-implementation.md.
Before going deep into a specific failure, verify the basics in this order:
- The server process is running and listening on the expected ports.
- The node profile points at the correct chain RPC and contract addresses.
- The storage backend is reachable and writable.
- Peer streams are connected.
- Routing exists for the intended token and destination.
- The channel exists and has enough usable balance.
Useful references:
On a development machine, the most common false failure is a stale local chain process from a previous test run still listening on the default e2e ports.
Representative failure:
listen tcp 127.0.0.1:8545: bind: address already in use
Checks:
- Inspect the default local chain ports before trusting a new e2e failure.
- Confirm whether the listener is a stale
gethfrom an earlier AgentPay run. - Stop only the stale local test process, then rerun the suite.
Useful command:
lsof -nP -iTCP:8545-8546 -sTCP:LISTENThis matters most for test/e2e, which starts its own local chain and expects 127.0.0.1:8545 and 127.0.0.1:8546 to be free.
Representative failure:
duplicate symbol '__cgo_set_stacklo'duplicate symbol '_x_cgo_init'ld: 19 duplicate symbolsclang: error: linker command failed with exit code 1
What this usually means:
- This is a local toolchain problem, not necessarily a repo regression.
- We reproduced it on macOS amd64 with the local
go1.25.5toolchain. - In that environment, even a trivial Go program containing
import "C"fails to link with the same duplicate-symbol error.
Checks:
- Confirm the local toolchain with
go version. - If the host is macOS amd64 and the default toolchain is
go1.25.5, rerun the build or test withGOTOOLCHAIN=go1.24.9. - If you want the workaround for the whole shell session, run
export GOTOOLCHAIN=go1.24.9first. - After switching toolchains, rerun a narrow check such as
go build ./serverorgo test ./test/e2e -run '^TestE2E$/^e2e-grp2$/^sendCondPayWithErc20$'.
If the duplicate-symbol failure is gone and the next error changes, continue with the normal repo-specific troubleshooting flow.
The backend exposes three main operator surfaces:
- Main gRPC server on
-port - Admin gRPC server on
-adminrpc - Admin HTTP gateway and Prometheus metrics on
-adminweb
An OSP may also expose an optional pay-centric WebAPI gRPC listener on -webapigrpc. In phase 1 that listener intentionally runs without TLS transport credentials, so it should be bound only to 127.0.0.1:<port> or another private interface used by a colocated same-host caller.
The normal operator tool is tools/osp-cli. Prefer it over ad hoc RPC calls because the repo already documents the stable command patterns there.
Useful operator commands:
./osp-cli -adminhostport localhost:8190 -querypeerosps
./osp-cli -adminhostport localhost:8190 -querydeposit -depositid <deposit-id>
./osp-cli -profile <profile.json> -storedir <store> -dbview channel -peer <peer-addr>
./osp-cli -profile <profile.json> -storedir <store> -dbview pay -payid <pay-id>
./osp-cli -profile <profile.json> -storedir <store> -dbview route -dest <dest-addr> -token <token-addr>
./osp-cli -profile <profile.json> -onchainview channel -cid <cid>
./osp-cli -profile <profile.json> -onchainview pay -payid <pay-id>Start with server/server.go, where the process validates its flags and storage selection.
Common causes:
- both
-storedirand-storesqlwere set -selfrpcis malformed-webapigrpcis already in use or bound to the wrong interface- keystore cannot be read or decrypted
- chain RPC endpoint is unreachable
- storage backend cannot be opened
Representative log messages from the code:
specify only one of -storedir, -storesqlinvalid self-RPCfailed to listen on OSP WebAPI grpcCannot setup SQL storeCannot setup local storeDialETH failed
Checks:
- Ensure exactly one storage mode is selected.
- Confirm the profile file is valid against the schema in common/profile.go.
- Verify the keystore path and password handling. For local tests,
-nopasswordis commonly used. - Verify the chain RPC endpoint in the profile's
Ethereum.Gatewayfield. - If
-webapigrpcis set, verify the bind target is loopback/private and the port is free.
Known-good local example:
AGENTPAY_INSECURE_TLS=1 go run ./server/server.go \
-profile $AGENTPAY_MANUAL_ROOT/profile/o1_profile.json \
-ks ./testing/env/keystore/osp1.json \
-port 10001 \
-adminrpc localhost:11001 \
-adminweb localhost:8190 \
-storedir $AGENTPAY_MANUAL_ROOT/store \
-rtc ./test/manual/rt_config.json \
-nopasswordIf you enable -webapigrpc, prefer a loopback bind such as -webapigrpc 127.0.0.1:12000. That listener is designed for a same-host client process and should not be treated as a public ingress.
Phase-1 OSP WebAPI subscriptions are intentionally single-subscriber and best-effort.
What this means operationally:
- Only one active
SubscribeIncomingPaymentssubscriber and one activeSubscribeOutgoingPaymentssubscriber are supported at a time. - A slow subscriber may miss events because the listener uses bounded non-blocking buffering.
- Polling RPCs such as
GetIncomingPaymentStatus,GetIncomingPaymentInfo, andGetOutgoingPaymentStatusremain the source of truth for correctness checks.
If an integration cares about final state rather than observability, re-read payment status or info instead of assuming the stream is lossless.
The storage path is derived differently for local and SQL modes.
- In
-storedirmode, the actual SQLite path becomes<storedir>/<ethaddr>/sqlite/celer.db. - In
-storesqlmode, the node uses the configured SQL database directly.
See setupKVStore(...) in cnode/cnode.go.
Checks:
- Make sure you are querying the store for the node's actual ETH address, not only the parent directory.
- When using SQLite, confirm you are pointing
osp-cli -storedirat the node-specific store directory when running DB views. - When using SQL, verify the exact database name and credentials used by the server match the CLI invocation.
The admin entry point is RegisterStream(...) in server/server.go, which calls CNode.RegisterStream(...) in cnode/cnode.go.
Representative failures:
celer stream already existsRegisterStream failed: grpcDial ... failedRegisterStream failed: CelerStream failedwaitRecvWithTimeout failedno celer streampeer not online
What these usually mean:
celer stream already exists: the server already has a live or remembered stream for that peer and RPC address.grpcDial ... failed: the target host or port is wrong, the peer process is not listening, or TLS/networking is broken.- When the target is
localhostor127.0.0.1, a dial timeout can also mean the process is using the built-in self-signed localhost certificate withoutAGENTPAY_INSECURE_TLS=1on the dialing side. waitRecvWithTimeout failed: the transport connected, but the auth handshake did not complete.peer not onlineorno celer stream: later traffic depends on a stream that was never established or was dropped.
Checks:
- Verify the peer gRPC port, not the admin port, is being passed to
-peerhostport. - Confirm the peer ETH address matches the profile and keystore used by that peer.
- Check whether the stream already exists before retrying the same registration.
- Use
-querypeerospsto see what the node currently believes about peer OSPs.
Example command:
./osp-cli -adminhostport localhost:8190 \
-registerstream \
-peer 00290a43e5b2b151d530845b2d5a818240bc7c70 \
-peerhostport localhost:10002If stream registration succeeds once and fails later, remember that the server installs a retry callback and may reconnect automatically after transient failures.
Routing lookup is implemented in route/forwarder.go. The common terminal error is no route to destination, but routing problems also surface indirectly as send failures or unreachable peers.
Checks:
- Confirm a channel exists either directly to the destination or to an access OSP for that token.
- Query the route table with
osp-cli -dbview route. - Confirm the token address used in the send matches the token address used in the channel and route tables.
- If you expect OSP routing, verify the node is registered as a router on-chain.
Example:
./osp-cli -profile <profile.json> -storedir <store> \
-dbview route -dest <destination-addr> -token <token-addr>The route controller logs this warning from route/controller.go:
NOT able to join the OSP network because this node is not registered on-chain as a router
That means the node process is healthy, but the on-chain RouterRegistry does not show it as an active router.
Recovery:
./osp-cli -profile <profile.json> -ks <keystore.json> -register -nopasswordThen restart the node or wait for the route-controller logic to observe the registry state.
Checks:
- Confirm
-locis enabled if this process is expected to listen to on-chain events. - Confirm the profile points to the expected
RouterRegistrycontract. - In multi-OSP setups, ensure peers have exchanged streams and routing broadcasts.
- Verify the runtime network actually has open channels for the token you are testing.
Deposit jobs are tracked by the processor in deposit/deposit.go and queried through admin RPC in server/server.go.
Possible states include:
QUEUEDAPPROVING_ERC20TX_SUBMITTINGTX_SUBMITTEDSUCCEEDEDFAILED
Checks:
- Query the deposit job explicitly.
- If the token is ERC20, look for an approval phase before the ledger deposit.
- Confirm the deposit signer has funds and is the expected keystore.
- Confirm the process is running as an event listener if you expect server-side job polling to progress automatically.
Commands:
./osp-cli -adminhostport localhost:8190 -querydeposit -depositid <deposit-id>
./osp-cli -profile <profile.json> -storedir <store> -dbview deposit -depositid <deposit-id>If the job is missing entirely, the admin query may return deposit job not found.
During send-path execution, messager/send_cond_pay_request.go computes free balance from the working simplex state and on-chain balance view. The common error is balance not enough.
Checks:
- Inspect the channel with
osp-cli -dbview channeland confirm free balance, not only total deposited balance. - If this is an OSP, check whether refill thresholds in the runtime config are causing automatic refill behavior.
- Confirm there are not too many unresolved pending payments consuming available capacity.
Relevant runtime config examples:
Admin sends use SendToken(...) in server/server.go. Common immediate failures are:
Can't parse amount.Can't parse dst.Can't parse token address.no celer streamno route to destinationbalance not enoughinvalid pay resolve deadline
Checks:
- Validate the receiver and token addresses before retrying.
- Confirm the stream and route exist.
- Confirm wall-clock time and
rtconfigpayment-timeout settings are consistent with the send path. Pay deadlines are unix timestamps (seconds) since the contracts switched toblock.timestamp-based windows; off-chain code usestime.Now().Unix()and thertconfig.MaxPaymentTimeoutcap is also in seconds. - Confirm the destination is reachable on the intended network.
This comes from the simplex sliding-window protocol in the message handlers. It typically means one of the following:
- request loss or replay
- sender and receiver disagree on the last co-signed simplex state
- a later request arrived before the expected base sequence was acknowledged
Relevant code paths:
Checks:
- Look for earlier ACK, NACK, reconnect, or dropped-stream events for the same peer.
- Inspect channel state with
osp-cli -dbview channeland compare simplex sequence numbers across both peers. - If this happened after a restart or network interruption, re-establish the stream and retry the operation.
Common errors include:
payment not foundchannel not foundchannel simplex state not found
These generally point to one of three situations:
- wrong store or wrong node profile is being queried
- the channel/payment was never created on this node's side
- the operator is looking at ingress/egress state on the wrong hop
Checks:
- Query the payment by pay ID in the node that should own ingress or egress state.
- Query the channel by peer and token or by CID.
- Confirm that the profile and storage path used by
osp-climatch the exact node instance you are debugging.
For most operational issues, use this order instead of trying random retries:
- Confirm the process, ports, and profile are correct.
- Confirm storage access and the correct store path.
- Re-register missing peer streams.
- Re-check route table state.
- Re-check channel state and free balance.
- Re-check deposit status and on-chain state.
- Retry the payment or channel operation only after the earlier layers look correct.
This sequence matches the way the backend is structured: transport first, then routing, then channel state, then payment state.
- Startup and admin surfaces: server/server.go
- Core node initialization: cnode/cnode.go
- Stream auth and registration: cnode/auth.go and cnode/cnode.go
- Routing lookup and controller: route/forwarder.go and route/controller.go
- Payment send path: messager/send_cond_pay_request.go
- Payment receive and settlement handlers: handlers/msghdl
- Deposit job processing: deposit
- Shared errors: common/errs.go