ZenFix PayRunOpen app →

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 funds

Overview

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

  1. Create an API key on the API Keys page — the full zfk_live_… key is shown once.
  2. Set your rules on the Policy page (limits, allowed merchants, daily budget).
  3. 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:

Recipe: zero → a verified payment

The full journey — from an empty workspace to a payment ZenFix has confirmed on-chain — in about 15 minutes.

  1. Key. Create an API key on API Keys.
  2. 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.
  3. Test USDC. Fund a wallet with Base Sepolia test USDC from a faucet (e.g. Circle’s); token 0x036CbD53842c5426634e7929541eC2318f3dCF7e.
  4. 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.
  5. 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:

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