Payment
Agent-driven purchases for OpenClaw. Issue single-use virtual cards via Stripe Link, fill checkout forms in a browser without the agent ever seeing real card data, and pay HTTP 402 endpoints over the Machine Payments Protocol — all behind explicit, severity-graded approvals.
Plugin type
This is a code plugin for OpenClaw — it ships a bundled runtime (dist/index.js) that registers a pay tool, a before_tool_call hook for sentinel substitution, and a before_message_write hook for outbound PAN/CVV/MPP-token redaction. It is not an instruction-only skill. The plugin's runtime is what enforces the security model; the bundled SKILL.md is a behavior guide for the agent that consumes the tool, not a substitute for the runtime.
The plugin's only third-party runtime dependency at execution time is the Stripe Link CLI — Stripe's official, open-source CLI for issuing Link-backed virtual cards. The plugin invokes it via execFile("link-cli", [...args]) with array-only arguments, no shell, and a hardened runner that escalates SIGTERM → SIGKILL on timeout. There is no path from agent input to arbitrary command execution.
Overview
The pay plugin gives an OpenClaw agent two safe paths to spend money on a user's behalf:
- Virtual card checkout — the plugin asks Stripe Link to mint a single-use card, the user approves on their phone, and the agent fills the merchant's checkout form using opaque
sentinelplaceholders. A runtime hook substitutes the real card values into the browser at fill time. The agent never sees the PAN, CVV, or full billing details — they're not in its transcript, parameters, or state. - Machine Payment — the plugin executes a payment directly against an HTTP 402 endpoint that advertises the Machine Payments Protocol. No browser. No card form. Useful for paid APIs, crawl quotas, or any per-call billing surface.
Every spend is gated by an OpenClaw approval (warning for issuing a card, critical for fill-time substitution and machine payments). Outgoing transcripts are scrubbed by a before_message_write hook that redacts Luhn-valid PANs, CVV-context strings, and MPP authorization tokens regardless of which key they appear under.
Install
clawhub package install @pejmanjohn/payment
Then enable in your OpenClaw config:
{
"plugins": {
"entries": {
"pay": {
"enabled": true,
"provider": "stripe-link"
}
}
}
}
Prerequisites
To use the stripe-link provider, you need Stripe's official Link CLI installed. This is a deliberate architectural choice — Link CLI is published, signed, and maintained by Stripe at github.com/stripe/link-cli. The payment plugin does not bundle, fork, or wrap card-issuance logic itself; it delegates to Stripe's CLI.
npm install -g @stripe/link-cli
- Source of truth: github.com/stripe/link-cli (published as
@stripe/link-clion npm, owned by Stripe Inc.) - Minimum version:
0.4.0 link-climust be resolvable onPATH— the plugin invokes it as a subprocess viaexecFile("link-cli", […])- A Link account with at least one saved payment method
- The Stripe Link mobile app for biometric approval
The mock provider has no external dependencies and is suitable for tests, CI, and demos.
Tools
The plugin contributes a single tool, pay, with five actions:
| Action | Severity | What it does |
|---|---|---|
setup_status | none | Reports whether the configured provider is ready to spend (CLI installed, account linked, etc.). Always call this first. |
list_funding_sources | none | Lists the user's available funding sources and their supported rails (virtual_card, machine_payment). |
issue_virtual_card | warning | Mints a single-use card with a per-card maxAmountCents cap. Returns a handle and a fillSentinels map the agent uses with browser.act fill. Requires biometric approval on the user's phone. |
redeem_handle | none | Reads non-secret status (active, expired, denied) for a handle. Use this after fill to confirm the card was used or to detect 3DS / approval timeouts. |
execute_machine_payment | critical | Calls a remote HTTP 402 endpoint with an MPP authorization. No browser. The agent must include a clear purchaseIntent describing what is being purchased and why. |
The runtime sentinel-fill substitution (which fires when the agent calls browser.act fill with a sentinel value) is itself a critical approval gate — the user sees the merchant URL, the field set, and the card display info before the real values are typed.
Action Groups
| Group | Actions | Notes |
|---|---|---|
| Discovery | setup_status, list_funding_sources | Read-only; no approval required. |
| Virtual card | issue_virtual_card, redeem_handle | Card issuance gates on user biometric approval; fill substitution gates on critical approval. |
| Machine payment | execute_machine_payment | One-shot HTTP 402 spend with critical approval. |
Quick start (virtual card checkout)
// 1. Verify the provider is ready
{ "action": "setup_status" }
// 2. Pick a funding source
{ "action": "list_funding_sources" }
// 3. Issue a single-use card (warning approval + biometric)
{
"action": "issue_virtual_card",
"providerId": "stripe-link",
"fundingSourceId": "fs_...",
"amount": { "amountCents": 2999, "currency": "usd" },
"merchant": { "name": "Example Store", "url": "https://example.com" },
"purchaseIntent": "Blue widget (SKU W-123) for $29.99 — home office order placed 2026-04-30"
}
// → returns { handle, fillSentinels: { pan, cvv, exp_month, exp_year, exp_mm_yy, holder_name, billing_line1, ... } }
// 4. Fill the merchant's checkout form using sentinel objects (critical approval at fill time)
{
"action": "act",
"request": {
"kind": "fill",
"fields": [
{ "ref": "input[name=cardnumber]", "type": "text", "value": { "$paymentHandle": "<handle-id>", "field": "pan" } },
{ "ref": "input[name=exp-date]", "type": "text", "value": { "$paymentHandle": "<handle-id>", "field": "exp_mm_yy" } },
{ "ref": "input[name=cvc]", "type": "text", "value": { "$paymentHandle": "<handle-id>", "field": "cvv" } }
]
}
}
The agent never sees real card values — substitution happens at the OpenClaw runtime boundary, after the user approves the fill prompt.
Quick start (machine payment)
{
"action": "execute_machine_payment",
"providerId": "stripe-link",
"fundingSourceId": "fs_...",
"endpoint": "https://api.example.com/v1/premium/generate",
"amount": { "amountCents": 500, "currency": "usd" },
"purchaseIntent": "Generate a high-resolution variant of the user's uploaded image.",
}
Sentinel reference
fillSentinels exposes:
- Card fields —
pan,cvv,exp_month,exp_year,exp_mm_yy,exp_mm_yyyy,holder_name - Billing address —
billing_line1,billing_line2,billing_city,billing_state,billing_postal_code,billing_country - Forward-compat extras — string-typed top-level fields surfaced by the provider (e.g.
email,phone) become available automatically as new fields ship inlink-cli. Object-typed fields are never passed through.
Use exp_mm_yy / exp_mm_yyyy for forms with a single combined expiry input (Stripe Elements, modern checkouts). Use exp_month + exp_year for split forms.
Security model
- Sentinels never carry secrets. A sentinel is
{ "$paymentHandle": "<id>", "field": "<name>" }. Real card data is fetched fromlink-cliimmediately before fill and zeroized from process memory immediately after. - Single retrieval site. Only one place in the plugin calls
link-cli retrieveCardSecrets— easy to audit, never reachable from agent input. - Outbound redaction. A
before_message_writehook scans every assistant message and tool argument for Luhn-valid PANs, CVV-context strings, and MPP authorization tokens. Matches block the message rather than redacting silently. - Approval gates. Issuing a card surfaces a
warning-level OpenClaw approval; sentinel substitution surfaces acritical-level approval; machine payments surface acritical-level approval. None of these can be bypassed by agent input. - No shell. The runner uses
execFile("link-cli", […])with array args and no shell. There is no path from agent state to arbitrary command execution.
Configuration
{
"enabled": true,
"provider": "stripe-link",
"defaultCurrency": "usd",
"store": "~/.openclaw/payments",
"providers": {
"stripe-link": {
"clientName": "OpenClaw",
"testMode": false,
"maxAmountCents": 50000
}
}
}
| Field | Default | Notes |
|---|---|---|
provider | — | "stripe-link" or "mock". Required. |
defaultCurrency | "usd" | Used when an action omits currency. |
store | ~/.openclaw/payments | Where issued-handle metadata is persisted. |
providers["stripe-link"].clientName | "OpenClaw" | Shown in the Link approval prompt. |
providers["stripe-link"].testMode | false | Use Link's --test flag. Cards always show holder "Jane Doe" in test mode. |
providers["stripe-link"].maxAmountCents | 50000 | Hard upper bound per card; Stripe Link's own ceiling is also 50000 (= $500 USD). |
Ideas to try
- "Buy this domain on Namecheap if it's under $30" — checkout flow with a virtual card
- "Pay $0.05 to call this paid API and summarize the result" —
execute_machine_paymentagainst an HTTP 402 endpoint - "Subscribe to my Substack of choice if the annual price is under $80" — checkout flow with billing-address fill
License
MIT