Skip to Content
Payment RailsOverview

Payment Rails

Keel’s managed payment boundary is payment.execute for Stripe MPP and x402. It evaluates the bound spend request before dispatch and records the outcome it observes. An authorization decision alone does not prove receipt, settlement, or an external state change.

Current boundaries

RailCurrent Keel contract
Stripe MPPManaged payment decisions and dispatch through the documented payment path.
x402Managed payment decisions and dispatch through the documented payment path.
AP2, PayPal, Visa, Mastercard, OpenAI ACP, UCPEvidence binding only. Keel does not execute or positively authorize these rails; current payment decisions deny.
L402Evidence binding only. Keel does not execute Lightning payments; current L402 payment decisions deny.
Amex ACENot released; no supported settlement path.

Evidence binding means included rail artifacts can be covered by a signed request binding. It does not mean Keel verified the external effect, granted spend authority, or intercepted a payment your application made directly. Do not treat a signed denied Permit as payment authorization.

Plans and setup

Stripe MPP and x402 are available on every plan. Starter uses the AI Permit-to-Pay template; custom payment authoring needs Production or Enterprise. Configure legitimate payment credentials and follow the relevant rail guide. Both rails require enabled owner-managed project MPP configuration, a trusted customer gateway with an unbound Client key, and operator-enrolled rail hosts. Configuration-state validation alone does not prove merchant or wallet eligibility. Requests beyond the plan allowance are refused; there is no overage.

Configure the trusted payment gateway

Keep ready-to-spend payment material in a trusted customer gateway, outside the agent process. Use a server-only Client key that is not bound to an AgentPrincipal. Create it with the project admin key, omitting agent_principal_id. It differs from the bound key required for managed MCP. An AgentPrincipal-bound key carrying an SPT or x402 payment payload is refused before policy or dispatch with 403 payment_credential.agent_delivery_forbidden, including expired or revoked agent bindings. Do not remove an agent binding or deliver this gateway key or payment credential to an agent. Keel governs requests through its managed boundary; it does not control direct payments made by the credential-holding customer gateway. Constrain the agent credentials and network access accordingly.

The project owner must enable the shared payment configuration for both Stripe MPP and x402. These public routes require the authenticated owner user JWT, not a project key:

  • POST /v1/projects/{project_id}/mpp-config: set stripe_profile_id and enabled: true.
  • GET /v1/projects/{project_id}/mpp-config: read configured, enabled, and the saved profile identifier.
  • POST /v1/projects/{project_id}/mpp-config/test: validate enabled configuration state.

The /test probe does not verify merchant eligibility, wallet funding, approval, or settlement. The shared field is named stripe_profile_id for both rails. It is configuration/attestation metadata: x402 does not require a Stripe Link account just to satisfy this field. Supply the actual stable identifier for your gateway payment profile. Saving an example string does not establish merchant, wallet, or credential eligibility. Missing or disabled configuration returns 409 MPP_CONFIG_NOT_ENABLED.

The Keel operator must enroll the exact merchant/resource HTTPS hostname in mpp_allowed_rail_hosts, the operator-controlled outbound allowlist. Confirm enrollment before sending payment material. Customer policies and the config probe cannot enroll arbitrary merchant hosts.

Configure KEEL_OWNER_JWT from the legitimate owner dashboard session, KEEL_DASHBOARD_ORIGIN to its allowed origin, KEEL_ADMIN_KEY to the server-held project admin key, KEEL_PROJECT_ID, and STRIPE_PROFILE_ID. Never give these management credentials to the agent. This Python 3 setup uses the standard library:

import json import os import secrets import time from urllib.request import Request, urlopen base_url = os.environ.get("KEEL_BASE_URL", "https://api.keelapi.com").rstrip("/") project_id = os.environ["KEEL_PROJECT_ID"] def setup_request(method, path, key, body=None): headers = { "Authorization": "Bearer " + key, "Content-Type": "application/json", "Origin": os.environ["KEEL_DASHBOARD_ORIGIN"], "X-Keel-Timestamp": str(int(time.time())), "X-Keel-Nonce": secrets.token_hex(16), } request = Request( base_url + path, data=None if body is None else json.dumps(body).encode("utf-8"), headers=headers, method=method, ) with urlopen(request, timeout=30) as response: return json.load(response) config_path = "/v1/projects/" + project_id + "/mpp-config" setup_request("POST", config_path, os.environ["KEEL_OWNER_JWT"], { "stripe_profile_id": os.environ["STRIPE_PROFILE_ID"], "enabled": True, }) state = setup_request("GET", config_path, os.environ["KEEL_OWNER_JWT"]) if not state["configured"] or not state["enabled"]: raise RuntimeError("Project payment configuration is not enabled") setup_request("POST", config_path + "/test", os.environ["KEEL_OWNER_JWT"], {}) credential = setup_request("POST", "/v1/api-keys", os.environ["KEEL_ADMIN_KEY"], { "scope": "client", "name": "Trusted payment gateway", }) gateway_key = credential["raw_key"] # Use only in this gateway; do not print or deliver this secret to an agent. os.environ["KEEL_API_KEY"] = gateway_key

This configures the SDK in the same gateway process. Persist gateway_key in your server-side secret store for later runs, then load it as KEEL_API_KEY. The raw key is returned only once. SDK calls add freshness headers; raw HTTP execution must supply a fresh timestamp and nonce on every attempt too. See Request Freshness.

Independent verification

Use the exact released verifier and the public trust material described in Running Keel Verify. A signed request binding proves what Keel recorded. Provider receipt, provider execution, and external state change each require their own evidence. The binding version in the artifact selects its canonicalization; historical artifacts remain unchanged.

Guides

Last updated on Edit this page on GitHub