A ROS 1 (Noetic) package that turns robot capabilities into paid API endpoints - using Solana’s x402 protocol for pay-to-access control — and includes tools for making and verifying payments to external services.
Ros-X402-Node is an early-stage Python project in the AI payments / x402 ecosystem, focused on humanoid-robot, robotics, ros, solana. It currently has 11 GitHub stars and 1 forks, and sits alongside related tools like Task-router-x402, tdm-sdk, dna-x402.
rospy_x402 provides a ROS 1 (Noetic) integration that exposes robot capabilities over a configurable REST API and enforces access through the Solana-based x402 payment protocol. It includes:
x402_ex_server) that loads endpoints from JSON configuration and requires successful x402 payments before invoking robot actions.x402_buy_service ROS service that performs paid calls to external HTTP services, supporting both incoming and outgoing Solana payments.rospy_x402.x402 Python library that encapsulates Solana RPC interactions, transaction submission, and payment session validation.All runtime logs and comments are in English, while the ROS package name follows the project requirements.
Full robot stack (KYR + x402 + VR teleop), RAID configuration, and common commands — metapackage br_bringup: ../br_bringup/README.md. Agent index (whole workspace): ../br_bringup/DEV_AI.md. This package (agents): DEV_AI.md.
.env (if SOLANA_PRIVATE_KEY is set) or prompts once on startup; the key is kept only in RAM.HELIUS_API_KEY is set in .env, the node uses Helius mainnet RPC; otherwise the public endpoint or ~solana_rpc_endpoint is used.teleop/help, optional push allowlist on the same REST port — see DOC/RAID_INTEGRATION.md./home/ubuntu/ros_ws in the examples below).python3-nacl (installed via apt or pip).solana (installed via pip) for outgoing payments. If absent, the node can still receive payments but cannot send them.HELIUS_API_KEY is set in .env, otherwise https://api.mainnet-beta.solana.com. Override via ~solana_rpc_endpoint when launching.sudo apt-get install python3-nacl
pip3 install --user solana python-dotenv requests
Add ~/.local/bin to your PATH if you use the --user flag for pip.
From the workspace root:
cd /home/ubuntu/ros_ws
catkin build rospy_x402
source devel/setup.bash
The package depends on message_generation, so ensure your workspace is sourced before building to pick up generated service headers.
.env for RPC and key (recommended)Copy .env.example to .env (e.g. in the package config/ or in the current working directory) and set:
HELIUS_API_KEY — Helius API key. If set, the node uses https://mainnet.helius-rpc.com/?api-key=... as the default RPC (no need to pass _solana_rpc_endpoint).SOLANA_PRIVATE_KEY — Base58 Solana wallet private key. If set, the node does not prompt for the key on startup.ROSPY_X402_CONFIG (optional) — Path to endpoints JSON. Overrides the default config/endpoints.example.json when no ~config_path rosparam is set.Keep .env out of version control (cp .env.example .env and add .env to .gitignore; the package already ignores .env). Restrict file permissions: chmod 600 .env.
cp .env.example config/.env
# Edit config/.env: set HELIUS_API_KEY and SOLANA_PRIVATE_KEY
rosrun rospy_x402 x402_ex_server.py
# With .env: uses Helius (if key set) and no key prompt (if SOLANA_PRIVATE_KEY set)
# Override: _config_path:=/path/to/endpoints.json _solana_rpc_endpoint:=https://...
If SOLANA_PRIVATE_KEY is not set, the node prompts once for a base58-encoded Solana private key. The key is kept only in memory and is used to:
If you do not pass the ~service_receiver_account parameter, the derived public key is used by default.
~config_path (string): Path to the JSON configuration that defines REST endpoints. Defaults to the bundled config/endpoints.example.json.~solana_rpc_endpoint (string): Solana RPC URL. Default: Helius mainnet if HELIUS_API_KEY is in .env, else https://api.mainnet-beta.solana.com.~service_receiver_account (string): Optional explicit Solana address to receive payments. Defaults to the public key derived from the private key.~service_asset_symbol (string): Asset ticker (default SOL).~service_payment_window_sec (int): Number of seconds to wait for an incoming payment (default 300).~service_poll_interval_sec (float): Polling frequency (seconds) for payment confirmation (default 5.0).config/endpoints.example.json illustrates a single POST endpoint:
{
"listen_host": "0.0.0.0",
"listen_port": 18080,
"require_https": false,
"endpoints": [
{
"path": "/health",
"http_method": "GET",
"description": "External integration health check.",
"ros_action": {
"type": "python_call",
"module": "rospy_x402.health",
"callable": "get_health_status"
}
},
{
"path": "/api/v1/robot/move_demo",
"http_method": "POST",
"description": "Trigger a sample motion via MotionManager for demonstration purposes.",
"ros_action": {
"type": "python_call",
"module": "rospy_x402.demo_actions",
"callable": "move_demo"
},
"x402_pricing": {
"amount": 0.00005,
"asset_symbol": "SOL",
"receiver_account": "",
"payment_window_sec": 180
}
}
]
}
ros_action: Defines how to invoke the robot functionality. The example calls rospy_x402.demo_actions.move_demo, which internally uses MotionManager and defaults to the wave action group (you can override it by sending {"demo_name": "<your_action>"} in the request body).x402_pricing: Enables payment enforcement. receiver_account can be omitted to use the node’s default account.facilitator_url: Optional URL of an external facilitator service; omit it to rely on the built-in on-chain verification.base_url: Optional base URL for this server (e.g. https://robot.example.com). Used for discovery and 402 resource.url. If omitted, derived from the Host header.x402_network: Optional CAIP-2 network identifier (e.g. solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp). Used in x402 V2 accepts[].network.x402_pricing entirely to expose a free endpoint.Paid endpoints and discovery conform to x402 V2 (x402scan/Bazaar compatible). Discovery is available at GET /.well-known/x402 and returns all endpoints as resources with accepts for paid ones.
X402Client.create_payment_session() and returns HTTP 402 with an x402 V2 JSON payload: x402Version: 2, accepts[] (with amount, payTo, maxTimeoutSeconds, asset, and extra.reference), and optional resource and extensions.bazaar.accepts[0].payTo and retries the call with the X-X402-Reference header set to accepts[0].extra.reference (or x402_reference in the body).X402Client.verify_payment():transfer instruction whose destination matches the receiver and whose lamports exceed the expected amount.200 OK.Because verification uses confirmed Solana transactions, the process is non-custodial and does not rely on any off-chain authority.
Note: In the example configuration the
receiver_accountfields are left blank. At runtime the server fills them with the public key derived from the private key you enter on startup (or the value provided via~service_receiver_account). Replace them with explicit addresses only if you need to override that behavior.
Some deployments prefer delegating payment tracking to a third-party service (“facilitator”) that aggregates metrics, provides fiat ramps, or proxies blockchain queries. By default the server performs on-chain verification itself—no facilitator is used unless you explicitly define one. To opt in, add the following key at the root of your configuration file:
"facilitator_url": "https://facilitator.example.com/api/v1/payments"
The x402 server surfaces the configured URL (via X402RestServer.facilitator_url) so integrators can:
If facilitator_url is omitted, the node continues to verify payments directly against the Solana RPC endpoint configured at startup.
The snippets below assume you are on Solana Devnet. Replace URLs, keys, and amounts to match your environment.
solana config set --url https://api.devnet.solana.com
solana-keygen new --outfile ~/devnet.json
solana airdrop 2 "$(solana-keygen pubkey ~/devnet.json)"
source /opt/ros/noetic/setup.bash
source /home/ubuntu/ros_ws/devel/setup.bash
rosrun rospy_x402 x402_ex_server.py \
_config_path:=/home/ubuntu/ros_ws/src/rospy_x402/config/endpoints.example.json \
_solana_rpc_endpoint:=https://api.devnet.solana.com
When prompted, paste the base58-encoded private key from ~/devnet.json. The node logs the derived public key:
[INFO] Loaded x402 key. Public key: <YOUR_PUBLIC_KEY>
Send an HTTP request without paying:
curl -X POST http://localhost:18080/api/v1/robot/move_demo \
-H 'Content-Type: application/json' \
-d '{}'
The response has HTTP 402 and JSON similar to:
{
"message": "Payment session created. Complete payment to continue.",
"reference": "bf9a6f7b4bbd4cbab226f0ddc5cf96d1",
"receiver": "<YOUR_PUBLIC_KEY>",
"amount": 0.00005,
"asset": "SOL",
"expires_in_sec": 180
}
Pay the required amount (0.00005 SOL) to the listed receiver address:
solana transfer <YOUR_PUBLIC_KEY> 0.00005 --allow-unfunded-recipient
Repeat the HTTP request; after the transfer is confirmed, the response becomes:
{
"status": "ok",
"data": {
"status": "completed",
"action": "wave"
}
}
curl -s http://localhost:18080/health | jq
Sample response (values trimmed for brevity):
{
"status": "ready",
"message": "Ready for commands",
"availableMethods": [
{
"path": "/health",
"httpMethod": "GET",
"description": "External integration health check.",
"rosAction": {
"module": "rospy_x402.health",
"callable": "get_health_status"
},
"parameters": {},
"pricing": null
},
{
"path": "/api/v1/robot/move_demo",
"httpMethod": "POST",
"description": "Trigger a sample motion via MotionManager for demonstration purposes.",
"rosAction": {
"module": "rospy_x402.demo_actions",
"callable": "move_demo"
},
"parameters": {},
"pricing": {
"amount": 0.00005,
"assetSymbol": "SOL",
"receiverAccount": "<derived from private key>",
"paymentWindowSec": 180
},
"metadata": {
"requestSchema": {
"type": "object",
"description": "Optional payload allowing you to pick a custom action group.",
"required": [],
"properties": {
"demo_name": {
"type": "string",
"description": "Action group name passed to MotionManager.run_action (defaults to 'wave')."
}
}
}
}
},
{
"path": "/api/v1/robot/buy_cola_demo",
"httpMethod": "POST",
"description": "Paid demo endpoint that drives the robot forward for 5 seconds to deliver cola cans.",
"rosAction": {
"module": "rospy_x402.demo_actions",
"callable": "buy_cola_demo"
},
"parameters": {},
"pricing": {
"amount": 0.000075,
"assetSymbol": "SOL",
"receiverAccount": "<derived from private key>",
"paymentWindowSec": 240
},
"metadata": {
"requestSchema": {
"type": "object",
"required": [
"quantity",
"buyer",
"seller"
],
"properties": {
"quantity": {
"type": "integer",
"minimum": 1,
"description": "Number of cola cans to purchase."
},
"buyer": {
"type": "object",
"required": [
"lat",
"lng"
],
"properties": {
"lat": {
"type": "number"
},
"lng": {
"type": "number"
}
},
"description": "Latitude/longitude of the buyer."
},
"seller": {
"type": "object",
"required": [
"lat",
"lng"
],
"properties": {
"lat": {
"type": "number"
},
"lng": {
"type": "number"
}
},
"description": "Latitude/longitude of the seller."
}
}
}
}
},
{
"path": "/api/v1/robot/shoot_demo",
"httpMethod": "POST",
"description": "Paid endpoint that fires the robot fire control system once.",
"rosAction": {
"module": "rospy_x402.demo_actions",
"callable": "shoot_demo"
},
"parameters": {},
"pricing": {
"amount": 0.001,
"assetSymbol": "SOL",
"receiverAccount": "<derived from private key>",
"paymentWindowSec": 180
},
"metadata": {
"requestSchema": {
"type": "object",
"description": "No payload required; call with an empty JSON object to fire once.",
"required": [],
"properties": {}
}
}
}
],
"location": {
"lat": 22.298412,
"lng": 114.167582
}
}
availableMethods now reflects the live endpoint configuration (path, HTTP method, pricing, default arguments). Coordinates are still mocked randomly around Hong Kong—update rospy_x402.health.get_health_status() to subscribe to a GPS topic and feed real values to integrators.
The cola demo requires a slightly higher payment (0.000075 SOL) and validates the payload:
Initiate the request:
curl -X POST http://localhost:18080/api/v1/robot/buy_cola_demo \
-H 'Content-Type: application/json' \
-d '{
"quantity": 2,
"buyer": {"lat": 55.7558, "lng": 37.6173},
"seller": {"lat": 55.7562, "lng": 37.6181}
}'
Until the payment is settled, the response is HTTP 402 with the payment session description (similar to the move_demo example above) but with amount: 0.000075.
After transferring 0.000075 SOL to the provided receiver and the transaction is confirmed, repeat the request. The success payload resembles:
{
"status": "ok",
"data": {
"order": {
"item": "cola_can",
"quantity": 2
},
"buyer": {
"lat": 55.7558,
"lng": 37.6173
},
"seller": {
"lat": 55.7562,
"lng": 37.6181
},
"movement": {
"type": "forward",
"durationSec": 5,
"velocityPreset": 2,
"stepAmplitude": 0.02
},
"status": "completed"
}
}
The shoot demo publishes a single "fire" command to the firecontroll topic. It requires a larger payment of 0.001 SOL.
curl -X POST http://localhost:18080/api/v1/robot/shoot_demo \
-H 'Content-Type: application/json' \
-d '{}'
As with the other paid endpoints, the first call returns HTTP 402 with a payment session description (this time with amount: 0.001). After transferring the funds, repeat the request with the X-X402-Reference header to receive:
{
"status": "ok",
"data": {
"status": "fired",
"topic": "firecontroll",
"payload": "fire"
}
}
rosservice call /x402_buy_service "endpoint: 'https://httpbin.org/post'
method: 'POST'
payload: '{\"demo\": true}'
headers_json: '{\"Content-Type\": \"application/json\"}'
amount: 0.00005
asset_symbol: 'SOL'
payer_account: '<RECIPIENT_PUBLIC_KEY>'"
Because payer_account is provided, the node pays the remote party and attaches the Solana transaction signature to the response:
success: True
response_body: "{\n \"json\": {\n \"demo\": true\n },\n \"url\": \"https://httpbin.org/post\"\n}"
error_message: ''
payment_signature: '5Z...abc'
If payer_account is left empty, the service behaves like the REST endpoint: it creates a payment session and waits for the caller to transfer funds to the robot’s account.
x402_buy_serviceService definition (srv/x402_buy_service.srv):
| Field | Type | Description |
|---|---|---|
endpoint |
string |
URL or IP of external service to call |
method |
string |
HTTP method (GET, POST, …) |
payload |
string |
Raw request body |
headers_json |
string |
JSON object with custom headers |
amount |
float64 |
Payment amount in SOL (or selected asset) |
asset_symbol |
string |
Asset ticker (defaults to SOL) |
payer_account |
string |
Optional Solana address to pay (robot sends funds if set) |
Response fields:
success (bool): Indicates whether payment and external call succeeded.response_body (string): Raw response from the external service (on success).error_message (string): Explanation of failure.payment_signature (string): Solana transaction signature (incoming or outgoing payment).payer_account empty): The service creates a payment session and waits for the caller to transfer funds. Provide the session reference (returned in error payload) to your client so it can pay.payer_account set): The node immediately sends funds to the provided address using the loaded private key. This requires the solana Python package and sufficient balance.getpass and stored only in memory. Restarting the node requires re-entering the key.To discover and configure outgoing payments (robot pays external x402 services), use the x402-bazaar console utility:
# List discoverable x402 resources (default: CDP Coinbase discovery API)
x402-bazaar search --limit 20
x402-bazaar search -n 5 --offset 10 # pagination
# Search by name/type: client-side filter on description, URL, metadata (e.g. LLM, sentiment, storage)
x402-bazaar search -n 200 -f "sentiment" # match "sentiment" in description or URL
x402-bazaar search -n 500 --filter "LLM" # use larger -n to scan more of the catalog
# Optional: pass through to discovery API if supported
x402-bazaar search -n 20 -q "keyword" # --query to API
x402-bazaar search --type http --network base
# Shortcuts: show details or generate config for result N (1-based) from the list
x402-bazaar search -n 10 -s 3 # list 10, then show 402 details for #3
x402-bazaar search -n 10 -c 1 -o cfg.json # list 10, then generate config for #1
x402-bazaar search -n 100 -f "sentiment" -s 1 # filter then show first match
# Show payment requirements for a resource URL
x402-bazaar show https://api.example.com/v1/paid-action
# Generate config snippet and rosservice call example
x402-bazaar configure https://api.example.com/v1/paid-action -o bazaar_config.json
After configure, use the printed rosservice call /x402_buy_service ... with the indicated endpoint, amount, and payer_account (payTo) to let the robot pay and call that service. See DOC/X402_PROTOCOL.md for the exact exchange schema.
To run the x402 node separately (handy for debugging):
roslaunch rospy_x402 x402_ex_server.launch
Or with overrides:
roslaunch rospy_x402 x402_ex_server.launch config_path:=/path/to/endpoints.json
The node is not included in ainex_bringup by default. Add it to your bringup or custom launch file if you want it to start with the robot.
rospy_x402/src/rospy_x402/x402/. You can reuse these modules for non-ROS projects.config/endpoints.example.json into your own configuration, then pass its path via ~config_path..env not loaded / still prompts for key / uses public RPC: Place .env in the package root (rospy_x402/.env) or in config/.env. When run via roslaunch, the node uses cwd=package root, so .env next to .env.example is found. Ensure python-dotenv is installed: pip install python-dotenv.rosrun and roslaunch for the same node at the same time.solana package: Outgoing payments raise PaymentSubmissionError. Install solana via pip.payment_window_sec or adjust poll_interval_sec if the network is slow.message_generation, message_runtime, and Python dependencies are installed, then re-source devel/setup.bash.Task-router-x402 is a Node.js/Express service for orchestrating robots and agents with x402 payment integration.
Open contract-facing SDK for payable routes and gateway-backed integrations. Thin public surface for authorize, checkout, and session flows.
DNA — Payment rails for AI agents. x402 micropayment protocol on Solana. Netting, transfer, stream settlement. Receipt anchoring on-chain.