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.