Condition-triggered payments

A common pattern: pay when something in the physical world becomes true. A parcel is detected in the drop box, the charger reports a full battery, a door sensor closes.

The danger is that conditions stay true. A naive node that pays on every message of a 10 Hz topic will pay ten times a second. robopay ships small, dependency-free primitives in robopay_core.triggers to prevent that.

The primitives

Primitive

What it does

EdgeTrigger

Fires once when a condition goes from false to true

RateLimit

Allows at most N events per time window

Cooldown

Enforces a minimum time between events

Debounce

Requires a condition to hold for a period before firing

They are plain Python objects with no ROS dependency, so they are easy to unit test.

Example

from std_msgs.msg import Bool
from robopay_core.triggers import EdgeTrigger, RateLimit

class PayOnDelivery(Node):
    def __init__(self):
        super().__init__("pay_on_delivery")
        self.trigger = EdgeTrigger()
        self.limit = RateLimit(max_events=5, window_seconds=60)
        self.create_subscription(Bool, "/delivery_confirmed", self.on_msg, 10)

    def on_msg(self, msg):
        if self.trigger.fired(msg.data) and self.limit.allow():
            self.pay()

robopay_examples/pay_on_condition is a complete version of this node, with parameters for addresses and amount.

Layers of protection

Use the primitives together with the node’s own protections:

  1. Edge detection stops a steady condition from repeating.

  2. Rate limits and cooldowns stop a flapping sensor from repeating.

  3. Idempotency keys tied to the event stop the same event from being paid twice, even across restarts.

  4. Spending caps in the node bound the damage if all of the above fail.