A $150 gift-card purchase, a $450 hardware order, and a $2,800 laptop order hit the same AI agent's payment endpoint minutes apart today. One came back blocked with no human path at all, one cleared on its own in under a second, and one sat waiting on a named CFO. Same agent, same API key, three different outcomes, decided entirely by rules the agent itself never sees or controls.
Disclosure: I work on Pink Agentic AI Payments at PinkWallet.
Short answer: yes, a policy engine for AI agent payments is a server-side rule table that every payment request is checked against before money moves, not a prompt the agent is asked to follow. Pink Agentic AI Payments enforces that rule evaluation on the server, before a payment executes, independent of anything the agent believes about its own limits. Below is a real rule table and real calls against a sandbox, including what happens when you edit a rule the wrong way.
What a rules layer between an agent and money actually has to do
A system prompt that says "stay under budget" is advice a model reads, not a constraint it obeys. A rules layer that's actually worth calling a policy engine has to do five things a prompt can't:
- Evaluate before execution. The check happens before a credential is issued, not as a cleanup pass after money already moved.
- Be deterministic. The same request, run twice, gets the same decision. No sampling, no "it depends on the model's mood."
- Resolve conflicts with a fixed order. When two rules could both apply, the first match in the ordered list wins, and the rest never run.
- Default to deny. If nothing in the table matches, the payment is blocked, not allowed by omission.
- Say why. A human (or the agent) can see which named rule fired, not just "no."
That's the shape of the engine behind Pink Agentic AI Payments. Here's what it looks like with real traffic against it.
Step 1: a workspace with a real rule table already in it
No signup required:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/sandbox/workspaces?utm_source=devto&utm_medium=article&utm_campaign=policy-engine-rules" \
-H 'content-type: application/json' \
-d '{"company":"Harbor Metrics","email":"dev@harbor-metrics.example","template":"startup"}'
Trimmed real response:
{
"workspace_id": "8qc0ijqupz",
"name": "Harbor Metrics",
"template": "startup",
"policy_version": 1,
"agents": [
{ "id": "a_eng", "name": "Eng Infra AI", "monthly_budget": 25000, "per_payment_cap": 1000 }
],
"rules": 17
}
This seeds 6 agents and 17 rules. An agent reads its own rule table with its own key:
curl -s "https://agentic-sandbox.pinkwallet.com/v1/rules" -H "authorization: Bearer $ENG"
That returns 11 rules scoped to the Eng Infra AI agent (some of the 17 only apply to other agents, like design or recruiting). A few of the ones that matter here:
| Rule | Payee | Amount | Window | Action |
|---|---|---|---|---|
| Never: gift cards, crypto, personal shopping | blocked category | any | tx | block |
| Known vendors under $1,000 | approved | $0–$1,000 | tx | allow |
| $1,000–$5,000: CFO approves | approved | $1,000–$5,000 | tx |
ask → Sarah Kim (CFO) |
| Over $5,000: 2 of 3 leadership | any | $5,000+ | tx |
ask → 2 of 3 named leaders |
| Duplicate invoice number within 90 days: blocked | any | any | tx | block |
Rules are checked in order, first match wins. The gift-card block sits ahead of everything else in this table, so a gift-card purchase never gets a chance to match a friendlier rule further down.
Step 2: three requests, three outcomes, same agent
Allowed ($450 to an approved vendor, under the $1,000 line):
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $ENG" \
-H 'content-type: application/json' \
-d '{"payee_id":"p_dell","amount":450,"purpose":"replacement laptop dock"}'
{
"decision": "allowed",
"rule": "Known vendors under $1,000",
"policy_version": 1,
"credential": {
"type": "bank_transfer", "max_amount": 450, "locked_to": "Dell", "single_use": true,
"transfer": { "rail": "ACH", "reference": "PWS-1C5B8C68", "status": "submitted" }
}
}
pending_human ($2,800, inside the $1,000–$5,000 band):
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $ENG" \
-H 'content-type: application/json' \
-d '{"payee_id":"p_dell","amount":2800,"purpose":"3 replacement laptops for new hires"}'
{
"decision": "pending_human",
"rule": "$1,000–$5,000: CFO approves",
"hold_id": "pay_f858f5e00691",
"who": "Sarah Kim (CFO)",
"approvers_needed": 1,
"expires_at": "2026-10-20T12:57:21.518Z"
}
blocked, with a reason (gift cards, no human path regardless of amount):
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $ENG" \
-H 'content-type: application/json' \
-d '{"payee_name":"giftcards.com","amount":150,"purpose":"team swag gift cards"}'
{
"decision": "blocked",
"rule": "Never: gift cards, crypto, personal shopping",
"credential": null,
"why": "matched · block"
}
That why field is the audit trail. The agent (or a human reading logs later) gets a named rule, not a bare rejection. Every decision also carries a policy_version, so a payment made under last week's rules stays attributable to last week's rules even after the table changes.
Step 3: editing a rule the right way
PUT /v1/admin/rules replaces the entire ordered list, not just the rule you're touching, so the source of truth for any edit has to be the full table, not one agent's view of it. This step uses a fresh sandbox workspace (company "Fernbank Logistics," workspace_id y3edzx3as7, same startup template as Step 1, so it also seeds 17 rules) to show the correct workflow end to end.
First, pull the full company-wide rule table with the admin key, not the agent key:
curl -s "https://agentic-sandbox.pinkwallet.com/v1/admin/state" -H "authorization: Bearer $ADMIN"
The rules array in that response is the full ordered table, all 17 rules, including the ones scoped to other agents. That's different from GET /v1/rules with an agent key, which only returns the subset that applies to the calling agent (11 of the 17, for Eng Infra AI), because it's a per-agent view, not the company's live policy. Starting a PUT from that narrower view would silently drop every rule that applies only to other agents.
With the full table in hand, I raised the ceiling on one rule, Known vendors under $1,000, from max: 1000 to max: 1200, kept the other 16 rules and the order unchanged, and PUT the whole list back:
curl -s -X PUT "https://agentic-sandbox.pinkwallet.com/v1/admin/rules" \
-H "authorization: Bearer $ADMIN" -H 'content-type: application/json' \
-d '{"rules": [...all 17 rules, r12 with max 1200...], "by": "dev@fernbank-logistics.example"}'
One schema detail that will save you a failed request: GET /v1/admin/state returns each rule's limits nested under amount ({"min", "max", "window"}), but PUT /v1/admin/rules expects min, max and window as top-level fields on each rule. Sending the GET shape unchanged returns {"error": "each rule needs id, name, action, window"}, so flatten amount before the PUT.
{ "policy_version": 2, "rules": 17 }
Policy version ticked up to 2, and the rule count held at 17, because the edit started from the full table instead of a filtered one.
The edit itself took effect immediately:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $ENG" \
-H 'content-type: application/json' \
-d '{"payee_id":"p_dell","amount":1100,"purpose":"replacement laptops for new hires"}'
{
"decision": "allowed",
"rule": "Known vendors under $1,000",
"policy_version": 2,
"credential": { "max_amount": 1100, "locked_to": "Dell", "single_use": true }
}
A $1,100 payment to the same vendor, which would have landed as pending_human under the old $1,000 ceiling, now auto-allows under the raised one, and the response's policy_version: 2 ties that decision to the edited table. No redeploy, no change to the agent's prompt or code. The one rule that matters here: always build a PUT from the full table GET /v1/admin/state returns, never from a single agent's filtered GET /v1/rules.
What each decision actually means
| Decision | Who acts | What happens next |
|---|---|---|
allowed |
Nobody | A single-use credential locked to that payee and amount is issued immediately |
pending_human |
A named approver (or N of a named group) | The payment sits as a hold with an expires_at; nothing moves until that person approves |
blocked |
Nobody; there's no approve path | The agent gets why naming the rule that matched, and the payment never exists |
Honest limits
This is test money against a public sandbox, not production. Production with real money is early access, not something generally available yet. The rule engine checks amount, payee, category, and time window; it does not independently verify a self-asserted claim like a stated PO number, only whether one was attached.
FAQ
Can the agent talk its way past a rule? No. The check runs server-side before a credential is issued; nothing the agent says in its own context changes what the server evaluates against.
What happens when two rules could both match? The first match in the ordered list wins and nothing below it is evaluated for that request, which is why the blocked-categories rule sits at the top of this table.
Does the engine support a dry run before actually submitting a payment? The API documents a POST /v1/payments/check route for exactly that; this article only exercised live /v1/payments calls, so no response from that route is shown here.
Try it
Sandbox is free, no account required: agentic-sandbox.pinkwallet.com. Full docs: pinkwallet.com/agentic/developers. Production with real money is early access only, not generally available.
Top comments (0)