Skip to content

Policy

The policy engine lets you control which rails are allowed, set per-call spend caps, and deny payments to specific URLs — all before Routeweiler signs a single transaction.

Pass a Policy instance to Routeweiler(policy=...). If omitted, the built-in default is used (prefer x402, no rules, no currency).

Basic structure

from routeweiler import Policy, PolicyRule, RuleMatch

policy = Policy(
    default_rail="x402",    # tiebreaker when multiple rails score equally
    currency="usd",         # reference currency for max_per_call_minor_units
    rules=[
        PolicyRule(
            name="cap-all-calls",
            when=RuleMatch(url_matches="*"),
            max_per_call_minor_units=500,  # $5.00 per call maximum
        ),
    ],
)

Rules are evaluated first-match wins, top to bottom. Once a rule matches, later rules are skipped.

RuleMatch — when does a rule fire?

RuleMatch is the condition predicate. All non-None fields are combined with AND.

from routeweiler import RuleMatch

# Match any request to URLs containing "example.com":
RuleMatch(url_matches="*.example.com")

# Match x402 challenges on the "base" network specifically:
RuleMatch(network="base")

# AND: base network AND exact scheme:
RuleMatch(network="base", scheme="exact")

# OR: either of the above conditions:
RuleMatch(any=[
    RuleMatch(network="base"),
    RuleMatch(url_matches="*.evil.com"),
])
Field Type Matches against
url_matches glob string challenge.resource.url (fnmatch)
scheme "exact" challenge.scheme
network string x402 accepts[i].network (e.g. "base", "base-sepolia")
any list of RuleMatch Short-circuit OR

PolicyRule — what to do when it matches

from routeweiler import PolicyRule, RuleMatch

PolicyRule(
    name="my-rule",            # appears in trace events and error messages
    when=RuleMatch(...),       # condition
    deny=False,                # True → raise PolicyDeniedError, never pay
    prefer=["l402", "x402"],   # boost these rails in the scoring step
    max_per_call_minor_units=1000,  # in the policy's reference currency minor units
    reason="optional reason shown in the error",
)

Worked examples

Deny a domain

from routeweiler import Policy, PolicyRule, RuleMatch

policy = Policy(
    rules=[
        PolicyRule(
            name="deny sketchy APIs",
            when=RuleMatch(url_matches="*.sketchy.io"),
            deny=True,
            reason="This domain is not approved.",
        ),
    ]
)

Cap per-call spend to $1.00

policy = Policy(
    currency="usd",
    rules=[
        PolicyRule(
            name="cap per call",
            when=RuleMatch(url_matches="*"),
            max_per_call_minor_units=100,  # $1.00 in cents
        ),
    ],
)

Note

max_per_call_minor_units is measured in the minor units of Policy.currency (or BudgetEnvelope.cap_currency when an envelope is configured). You must set at least one of these — Routeweiler raises ValueError at construction if neither is set and a max_per_call_minor_units rule exists.

Prefer Lightning for a specific host

policy = Policy(
    default_rail="x402",
    rules=[
        PolicyRule(
            name="prefer lightning for inference.ai",
            when=RuleMatch(url_matches="*.inference.ai"),
            prefer=["l402"],
        ),
    ],
)

prefer boosts the listed rails in the scoring step but does not deny others. If L402 funding is unavailable, Routeweiler falls through to the next best rail.

Combine multiple conditions

policy = Policy(
    currency="usd",
    rules=[
        PolicyRule(
            name="deny unsupported networks",
            when=RuleMatch(url_matches="*"),
            deny=True,
            reason="Only Base network is approved.",
        ),
        PolicyRule(
            name="allow Base",
            when=RuleMatch(network="base"),
            deny=False,  # this fires first for Base — allows it
            max_per_call_minor_units=200,
        ),
    ],
)

Order matters

Rules are first-match wins. Put the most specific rules first.

Passing policy to the client

from routeweiler import Routeweiler, Funding, Policy, PolicyRule, RuleMatch

async with Routeweiler(
    funding=[Funding.base_usdc(wallet=signer)],
    policy=policy,
) as client:
    response = await client.get("https://api.example.com/data")

The policy hash (stable SHA-256 fingerprint) is written into every TraceEvent so you can correlate trace rows with the policy that governed them.