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

pending

Recorded locally, not yet broadcast

broadcast

Sent to the network, awaiting confirmation

confirmed

Included on chain and final

failed

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

max_per_transaction

10

Largest single payment, in USDC

max_per_window

50

Total spend allowed in the rolling window

window_seconds

3600

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.