Stripe Link Wallet

Commerce

Use the Stripe Link CLI as an agent wallet to create spend requests, generate one-time-use payment credentials, and complete 402 / Machine Payment Protocol (MPP) payment flows on the user's behalf.

Install
assistant skills install stripe-link-wallet

compatibility:Designed for Vellum personal assistants

Stripe Link Wallet

Spend on the user's behalf using the Stripe Link CLI. The CLI acts as a wallet: the assistant composes a spend request, the user approves it in the Link app on their device, and the CLI then issues one-time-use credentials to complete the purchase. Use this for paying 402 / Machine Payment Protocol (MPP) endpoints and for ad-hoc agent purchases that should be capped and auditable.

Required tools

  • bash for all link-cli invocations. Use host_bash only if a specific flow genuinely requires host-level access (e.g. reading a local file the user has on their machine).

Hard constraints

  • Always pass --request-approval on spend-request create. The Link app approval is the consent surface — it is non-negotiable. No spending happens without it.
  • Default to test mode (--test) unless the user has explicitly asked to spend real money. When unsure, ask in chat before dropping --test.
  • The context field must be at least 100 characters and must accurately describe what the money is for. The user reads this when approving in Link — write it for them, not for yourself.
  • Always use --format json on every command. The default interactive Ink output is for humans, not agents. Exception: demo and onboard require a TTY and have no JSON mode.
  • Amount is in cents. $10.00 = --amount 1000. Maximum is 50,000 cents ($500).
  • Never log or repeat raw card credentials (PAN, CVC) in the conversation. Always use --output-file when retrieving card credentials.

Checkout interception — always active

Whenever any flow (MCP tool, API call, web request, or otherwise) produces a checkout URL, hosted payment page, or payment session that would otherwise be handed to the user to click through — intercept it. Route the payment through Stripe Link instead. Never surface a bare checkout URL to the user as the final step of a purchase flow.

Decision tree:

  1. MPP / HTTP 402 endpoint — Flow A (Shared Payment Token)
  2. MCP service with a complete_checkout tool — Flow B to get a virtual card, tokenize the card against the provider's Stripe account, then call complete_checkout with the resulting token
  3. Standard web checkout (no payment API) — Flow B to get a virtual card, then use a browser automation skill to fill the card into the checkout form

For MCP-based checkouts (e.g. PostalForm):

  • After retrieving the virtual card from --output-file, create a Stripe PaymentMethod via POST /v1/payment_methods using the merchant's publishable key (extract from the checkout URL or session if present), then pass the resulting pm_xxx token to the MCP's complete_checkout tool.
  • If the publishable key is not available, fall back to browser automation (option 3) using the checkout URL the MCP returned — do not surface the URL to the user, navigate to it directly via browser automation.

Never skip this step. If link-cli is not installed or the user is not authenticated, surface that blocker immediately rather than falling back to a raw checkout URL.


Step 0: Check installation and auth

if command -v link-cli >/dev/null; then
  link-cli auth status --format json
else
  bunx @stripe/link-cli auth status --format json
fi
  • link-cli missing — use bunx @stripe/link-cli for Step 0 and every command below.
  • Exit 0 but "authenticated": false — fall to Setup: login.
  • Authenticated — proceed to the requested flow.
  • "update" key present in auth status — mention the update to the user but don't block on it.

Setup

If link-cli is missing

Invoke the CLI on demand with bunx:

bunx @stripe/link-cli <subcommand>

In every example below, substitute bunx @stripe/link-cli wherever you see link-cli.

If installed but not authenticated

Use your own assistant name for --client-name — read it from IDENTITY.md. This is the label the user sees in the Link app when they approve the connection.

link-cli auth login --client-name "<your assistant name>"

Opens a browser flow. The Link app will show <your assistant name> on <hostname> when the user approves the connection. After it completes, re-run Step 0.

Introspecting the CLI

If you need the exact flags for a subcommand not covered below:

link-cli --llms-full                      # all commands, LLM-friendly
link-cli spend-request create --schema    # full schema for one command
link-cli <command> --help

Pre-flight: get a payment method ID

Every spend request needs a --payment-method-id. Retrieve the user's saved methods first:

link-cli payment-methods list --format json

If the user has multiple, ask which one to use. If they have none, direct them to app.link.com/wallet to add one first.


Common flows

Flow A: Pay a 402 / MPP-protected URL

Use this when the target endpoint returns HTTP 402 and requires a Shared Payment Token (SPT).

1. Decode the challenge (optional but useful for diagnosing)

link-cli mpp decode \
  --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", ...'

Extracts the network_id and other challenge fields. Use when the URL is unfamiliar or the challenge looks malformed.

2. Create the spend request

link-cli spend-request create \
  --payment-method-id <id> \
  --merchant-name "<merchant>" \
  --merchant-url "<url>" \
  --context "<min-100-char description of what is being purchased and why>" \
  --amount <cents> \
  --credential-type "shared_payment_token" \
  --line-item "name:<item>,unit_amount:<cents>,quantity:<n>" \
  --total "type:total,display_text:Total,amount:<cents>" \
  --request-approval \
  --test \
  --format json

Drop --test only if the user has explicitly asked to spend real money — and say so in chat before running.

Important — JSON mode does not block. With --format json, create --request-approval returns immediately with an _next.command value pointing to spend-request retrieve. You must then poll for approval.

3. Poll for approval

link-cli spend-request retrieve <id> \
  --interval 3 --max-attempts 60 \
  --format json

Polls every 3 seconds, up to 3 minutes. Terminal statuses: approved, denied, expired, canceled. If polling exhausts --max-attempts while still non-terminal, the command exits non-zero with code: "POLLING_TIMEOUT" — report this to the user and offer to cancel or retry.

4. Pay the URL

Once status is approved:

link-cli mpp pay <url> \
  --spend-request-id <id> \
  --method POST \
  --data '<json body>' \
  --header "X-Custom: value" \
  --format json
  • --header is repeatable: --header "Name: Value".
  • Content-Type: application/json is auto-applied when --data is provided; user-provided headers take precedence.
  • The SPT is one-time-use. If payment fails, you must create a new spend request.

Before running, read the URL and amount back to the user in plain language to catch typos.

5. Report the result — status code, what the endpoint returned.


Flow B: Virtual card for a standard checkout

Use this when the merchant does not support MPP (no HTTP 402). Credentials are a one-time virtual Visa/Mastercard.

1. Create the spend request

link-cli spend-request create \
  --payment-method-id <id> \
  --merchant-name "<merchant>" \
  --merchant-url "<url>" \
  --context "<min-100-char description>" \
  --amount <cents> \
  --line-item "name:<item>,unit_amount:<cents>,quantity:<n>" \
  --total "type:total,display_text:Total,amount:<cents>" \
  --request-approval \
  --test \
  --format json

Omit --credential-type (or use the default). With --format json, returns immediately — proceed to polling.

2. Poll for approval (same as Flow A step 3)

3. Retrieve card credentials securely

link-cli spend-request retrieve <id> \
  --include card \
  --output-file /tmp/link-card.json \
  --force \
  --format json

--output-file writes the full card (PAN, CVC, billing address) to a local file with 0600 permissions and redacts card data in stdout. The JSON output replaces the card object with redacted fields and adds a card_output_file path. Never omit --output-file when requesting card credentials — raw PANs must not appear in the conversation or logs.

4. Use the card

The file at /tmp/link-card.json contains number, cvc, exp_month, exp_year, billing_address, and valid_until. Hand the path to a browser automation skill or tell the user where to find it. Do not read the file back into the conversation.


Inspect, update, cancel

Read-only and mutation operations need no extra gating:

# List saved payment methods
link-cli payment-methods list --format json

# List saved shipping addresses
link-cli shipping-address list --format json

# Retrieve a spend request (no card data by default)
link-cli spend-request retrieve <id> --format json

# Update before approval (e.g. fix merchant URL)
link-cli spend-request update <id> --merchant-url <url> --format json

# Request approval separately (if created without --request-approval)
link-cli spend-request request-approval <id> --format json

# Cancel (valid from created, pending_approval, or approved)
link-cli spend-request cancel <id> --format json

Line items and totals reference

--line-item and --total use repeatable key:value format.

--line-item keys: name (required), quantity, unit_amount, description, sku, url, image_url, product_url

--line-item "name:Running Shoes,unit_amount:12000,quantity:1,description:Trail runners"

--total keys: type (required; one of subtotal, tax, total), display_text (required), amount (required)

--total "type:subtotal,display_text:Subtotal,amount:11000"
--total "type:tax,display_text:Tax,amount:1000"
--total "type:total,display_text:Total,amount:12000"

Error handling

Error / conditionAction
link-cli not foundInvoke it with bunx @stripe/link-cli and substitute that prefix wherever examples use link-cli.
Not authenticatedRun auth login --client-name "<your assistant name>" (see Setup)
POLLING_TIMEOUT on retrieveReport to user; offer cancel or fresh spend request
SPT payment fails (402 again after pay)SPT is consumed — create a new spend request
amount > 50000Tell user the cap is $500 per transaction
context < 100 charsExpand it before retrying
Card file already existsUse --force to overwrite, or pick a different path

References

CreatorVellum
LicenseMIT
Updated18 days ago
SecurityVerified
View on GitHub

The Personal AI you were promised

GET STARTED