Payments
Send from the command line
ros2 service call /transfer/send robopay_interfaces/srv/Transfer \
"{from_address: '<your-address>', to_address: '<their-address>', amount: '0.05', asset: 'USDC'}"
The call returns as soon as the transaction is broadcast, with its hash and a
status of broadcast. Confirmation happens in the background.
Send from your own node
from robopay_interfaces.srv import Transfer
class Buyer(Node):
def __init__(self):
super().__init__("buyer")
self.pay_client = self.create_client(Transfer, "/transfer/send")
def pay(self, payee, amount):
request = Transfer.Request()
request.from_address = self.my_address
request.to_address = payee
request.amount = amount # a string, e.g. "0.05"
request.asset = "USDC"
future = self.pay_client.call_async(request)
future.add_done_callback(self._paid)
def _paid(self, future):
response = future.result()
self.get_logger().info(f"payment {response.status}: {response.tx_hash}")
Amounts are strings, not floats, so that 0.1 stays exactly 0.1.
Preview first
transfer/preview runs every check transfer/send would, without signing or
spending anything: balance, fee estimate, and spending caps. Use it before
committing to a job, or to show an operator what a payment would do.
Idempotency
Every payment carries an idempotency key. If you set one, retrying the call with the same key returns the original payment rather than sending a second one. This makes it safe to retry after a timeout, a dropped connection, or a restart. If you leave it empty, the node generates one per call, which protects against internal retries but not against your code calling twice.
For anything triggered by the physical world, derive the key from the event
itself, for example delivery-<order_id>. Then the same delivery can never be
paid twice, no matter how often the trigger fires.
Payment lifecycle
State |
Meaning |
|---|---|
|
Recorded locally, not yet broadcast |
|
Sent to the network, awaiting confirmation |
|
Included on chain and final |
|
Rejected by the chain; no money moved |
The background resolver moves payments from broadcast to a final state, and
recovers anything left in pending after a crash by checking the chain.
Spending caps
Caps are on by default and enforced inside the node, at the point of signing. No ROS client can bypass them.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Largest single payment, in USDC |
|
|
Total spend allowed in the rolling window |
|
|
Length of the rolling window |
A payment that would break either cap is rejected before it is signed, with an error saying which cap and by how much. Escrow deposits count toward the caps the same way transfers do.
Set caps to what the robot actually needs. A bug, a runaway trigger, or a compromised node then costs at most the cap, not the balance.