API Reference
Let agents pay — on your terms. A control layer that decides and logs every payment before a cent moves.
Authorization + audit · never moves fundsOverview
ZenFix PayRun is a control layer between your AI agents and real money. Every payment an agent proposes is checked against your policy and logged before anything happens — ZenFix decides allow / needs‑review / block and keeps the full trail. It never holds or moves funds: your agent executes the payment on its own rail and reports the result back so the audit loop closes.
Base URL https://intent-swap.app · all requests and responses are JSON.
Quickstart
- Create an API key on the API Keys page — the full zfk_live_… key is shown once.
- Set your rules on the Policy page (limits, allowed merchants, daily budget).
- Submit an intent:
curl
curl -X POST https://intent-swap.app/api/v1/payruns \
-H "Authorization: Bearer zfk_live_..." \
-H "Content-Type: application/json" \
-d '{
"agentId": "agent_ops_01",
"purpose": "Buy a verified API result",
"amount": "12.50",
"merchant": { "id": "acme_api", "payee": "ACME", "category": "api" },
"artifactType": "api_result",
"idempotencyKey": "optional-retry-safe-key"
}'Python
import requests
r = requests.post(
"https://intent-swap.app/api/v1/payruns",
headers={"Authorization": "Bearer zfk_live_..."},
json={
"agentId": "agent_ops_01",
"purpose": "Buy a verified API result",
"amount": "12.50",
"merchant": {"id": "acme_api", "payee": "ACME", "category": "api"},
"artifactType": "api_result",
},
)
decision = r.json()["decision"]
# decision["outcome"] is "allowed" | "needs_review" | "blocked"JavaScript
const res = await fetch("https://intent-swap.app/api/v1/payruns", {
method: "POST",
headers: {
"Authorization": "Bearer zfk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "agent_ops_01",
purpose: "Buy a verified API result",
amount: "12.50",
merchant: { id: "acme_api", payee: "ACME", category: "api" },
artifactType: "api_result",
}),
});
const { payRunId, decision } = await res.json();
Then branch on decision.outcome:
- allowed — execute the payment on your own rail, then report it back (below).
- needs_review — a workspace owner approves or denies it on the Pay Run's page; poll GET /api/v1/payruns/{id} for the outcome, then report execution once approved.
- blocked — do not pay; reasonCodes explain why.
Recipe: zero → a verified payment
The full journey — from an empty workspace to a payment ZenFix has confirmed on-chain — in about 15 minutes.
- Key. Create an API key on API Keys.
- Policy. On Policy, add your merchant to Allowed merchants and set limits/budget. Pin the merchant’s payout address under Merchant payout addresses (merchantId = 0x…) — required to earn the Verified on-chain badge; the transfer is checked against this address.
- Test USDC. Fund a wallet with Base Sepolia test USDC from a faucet (e.g. Circle’s); token 0x036CbD53842c5426634e7929541eC2318f3dCF7e.
- Propose. POST /api/v1/payruns — if the decision is allowed, continue; if needs_review, approve it in-app (or wire a Slack webhook) and poll until approved.
- Pay + prove. Send the USDC on Base Sepolia to the pinned address, then POST /api/v1/payruns/{id}/execution with rail:"base-sepolia" and the transactionHash. ZenFix reads the chain; the run closes as Verified on-chain — or 422 if the proof doesn’t check out.
A runnable, zero-dependency reference agent (all of this end to end) is in the repo under examples/aria-agent.
Authentication
Every request carries a workspace API key as a bearer token:
Authorization: Bearer zfk_live_...
Manage keys on the API Keys page. Only a hash is stored, so a lost key can only be revoked, never recovered. A revoked or unknown key returns 401.
Your policy
Decisions are made against the policy you save on the Policy page:
- Per-transaction limit — a single payment above it is blocked.
- Review threshold — at or above it, a payment needs review first.
- Absolute hard limit — the ceiling no payment may cross.
- Daily budget — authorized spend per UTC day; over it, further payments are blocked.
- Allowed / blocked merchants + categories — a merchant not on the allowlist is blocked.
Amounts are USDC. Changes apply to every subsequent decision.
Submit an intent for a decision
POST /api/v1/payruns
Submit a payment intent; ZenFix evaluates it and returns a decision. A blocked outcome is still a successful call (HTTP 200) — the decision is in the body.
curl
curl -X POST https://intent-swap.app/api/v1/payruns \
-H "Authorization: Bearer zfk_live_..." \
-H "Content-Type: application/json" \
-d '{
"agentId": "agent_ops_01",
"purpose": "Buy a verified API result",
"amount": "12.50",
"merchant": { "id": "acme_api", "payee": "ACME", "category": "api" },
"artifactType": "api_result",
"idempotencyKey": "optional-retry-safe-key"
}'Python
import requests
r = requests.post(
"https://intent-swap.app/api/v1/payruns",
headers={"Authorization": "Bearer zfk_live_..."},
json={
"agentId": "agent_ops_01",
"purpose": "Buy a verified API result",
"amount": "12.50",
"merchant": {"id": "acme_api", "payee": "ACME", "category": "api"},
"artifactType": "api_result",
},
)
decision = r.json()["decision"]
# decision["outcome"] is "allowed" | "needs_review" | "blocked"JavaScript
const res = await fetch("https://intent-swap.app/api/v1/payruns", {
method: "POST",
headers: {
"Authorization": "Bearer zfk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "agent_ops_01",
purpose: "Buy a verified API result",
amount: "12.50",
merchant: { id: "acme_api", payee: "ACME", category: "api" },
artifactType: "api_result",
}),
});
const { payRunId, decision } = await res.json();
Response
{
"payRunId": "payrun_...",
"decision": {
"outcome": "allowed", // allowed | needs_review | blocked
"reasonCodes": [],
"riskLevel": "low", // low | medium | critical
"nextAction": "prepare_funding",
"checks": [ { "ruleClass": "...", "reasonCode": "...", "outcome": "pass|review|block", "explanation": "..." } ]
}
}
The Pay Run and its full rule-by-rule trail are saved to Pay Runs. Reuse an idempotencyKey to make retries safe.
Report execution back
POST /api/v1/payruns/{payRunId}/execution
After your agent executes an allowed (or human-approved) payment on its own rail, report the outcome and proof. ZenFix records it and closes the run at execution_reported. Only a run awaiting execution accepts a report; anything else returns 409.
Verified execution (recommended). Report rail: "base-sepolia" (testnet) or rail: "base-mainnet" (real USDC on Base) with the real transactionHash. To earn the Verified on-chain badge you must first pin the merchant’s payout address on the Policy page — that pinned address is the trust anchor. ZenFix then reads the public chain and confirms the transaction succeeded, moved that chain’s USDC, paid at least the authorized amount, and went to that pinned address; a claim it can't verify is rejected with 422. Without a pinned address for the merchant the outcome is recorded as self-reported, not proof-backed — a recipient the agent merely declares can be set to match any public transfer, so it can't anchor the proof. You may also pass the paying wallet as sender; the on-chain transfer must then also have come from it (for x402/EIP-3009 the authorizing wallet is the transfer's from even when a facilitator submits the tx). Non-Base rails are always self-reported.
Trying the verified path? Fund a wallet with Base Sepolia test USDC from a faucet (e.g. Circle’s), send the payment to your merchant’s address using the test USDC token 0x036CbD53842c5426634e7929541eC2318f3dCF7e, then report that transaction hash with rail: "base-sepolia".
curl
curl -X POST https://intent-swap.app/api/v1/payruns/PAYRUN_ID/execution \
-H "Authorization: Bearer zfk_live_..." \
-H "Content-Type: application/json" \
-d '{ "outcome": "executed", "providerReference": "agent-run-42",
"rail": "base-sepolia", "transactionHash": "0x...",
"sender": "0xYourAgentWallet" }'Python
requests.post(
f"https://intent-swap.app/api/v1/payruns/{pay_run_id}/execution",
headers={"Authorization": "Bearer zfk_live_..."},
json={"outcome": "executed", "providerReference": "agent-run-42",
"rail": "base-sepolia", "transactionHash": "0x...",
"sender": "0xYourAgentWallet"},
)JavaScript
await fetch(`https://intent-swap.app/api/v1/payruns/${payRunId}/execution`, {
method: "POST",
headers: {
"Authorization": "Bearer zfk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
outcome: "executed",
providerReference: "agent-run-42",
rail: "base-sepolia",
transactionHash: "0x...",
sender: "0xYourAgentWallet",
}),
});
Response (verified rail) — the verification block is what makes the outcome proof-backed
{ "payRunId": "payrun_...", "status": "execution_reported",
"report": { "outcome": "executed", "providerReference": "agent-run-42", "transactionHash": "0x...", "rail": "base-sepolia", "reportedAt": "..." },
"verification": { "verified": true, "chain": "base-sepolia", "amountAtomic": "20000000", "recipient": "0x...", "pinnedMerchant": true } }
On a self-reported outcome — a non-Base rail, or a verified rail without a pinned merchant address — verification is { "verified": false }.
Check a Pay Run's status
GET /api/v1/payruns/{payRunId} · GET /api/v1/payruns
Read back a run to see the human review outcome and execution state — this closes the needs_review loop, letting an agent poll for the owner's approve/deny before it pays.
curl https://intent-swap.app/api/v1/payruns/payrun_... \
-H "Authorization: Bearer zfk_live_..."
Response
{
"payRunId": "payrun_...",
"status": "approved", // policy_allowed | pending_review | approved | denied | blocked | execution_reported
"decision": { "outcome": "needs_review", "reasonCodes": [ ... ], "riskLevel": "medium", "checks": [ ... ] },
"review": { "outcome": "approved", "decidedAt": "..." }, // null until a human decides
"executionReport": null // set once you report execution
}
List runs with GET /api/v1/payruns (newest first). Optional query: ?status=, ?agentId=, ?limit= (default 50, max 100). Both are scoped to your workspace; an id in another workspace returns 404.
Prefer push over polling? Set a needs-review webhook on the Policy page — ZenFix nudges it when a run needs a decision, and you confirm with this endpoint. Paste a Slack incoming webhook and the nudge arrives as a Slack message.
Tamper-evident audit
GET /api/v1/payruns/{payRunId}/audit
Returns the run’s full audit trail as a hash chain — each event carries entryHash = sha256(prevHash + canonical(event)), linked to the one before it. Anyone can re-derive the chain from the returned JSON and detect any altered, inserted, reordered, or dropped event — without trusting ZenFix.
{
"payRunId": "payrun_...",
"genesis": "0000…",
"headHash": "9f2c…",
"events": [
{ "sequence": 1, "actionCode": "payrun.created", "occurredAt": "...", "details": { ... },
"prevHash": "0000…", "entryHash": "1a7b…" },
{ "sequence": 2, "actionCode": "payrun.transition", "prevHash": "1a7b…", "entryHash": "9f2c…" }
]
}
A zero-dependency verifier is in the repo at examples/verify-audit. The table is already append-only; the chain is what lets you check it independently. (Absolute non-repudiation against a full database rewrite needs the head hash anchored externally — on the roadmap.)
Status codes
- 200 — decision or report recorded (including a blocked decision).
- 400 — missing or invalid fields.
- 401 — missing, malformed, unknown, or revoked API key.
- 404 — no such Pay Run in your workspace.
- 409 — the Pay Run is not awaiting execution (already reported, blocked, or under review).
- 422 — a base-sepolia execution whose on-chain transfer could not be verified (not found, reverted, wrong token, or under the authorized amount).
- 503 — temporarily unavailable; retry with the same idempotencyKey.