diff --git a/.changeset/lpt-merchant-binding.md b/.changeset/lpt-merchant-binding.md new file mode 100644 index 0000000..0ef1dcb --- /dev/null +++ b/.changeset/lpt-merchant-binding.md @@ -0,0 +1,5 @@ +--- +'@stripe/link-cli': minor +--- + +Add merchant-bound Link Pay Token options to spend-request creation. diff --git a/CLAUDE.md b/CLAUDE.md index 2a23592..60563f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ Commands: `auth login|logout|status`, `spend-request create|update|retrieve|requ The CLI also runs as an MCP server (`--mcp`) and serves skill files via `skills` subcommand, both provided by incur. -**When changing commands, flags, or schema descriptions, always update all three together:** `README.md`, `skills/create-payment-credential/SKILL.md`, the schema description strings in the relevant `schema.ts` file, and `CLAUDE.md`. These can easily drift apart. +**When changing commands, flags, or schema descriptions, always update all four together:** `README.md`, `skills/create-payment-credential/SKILL.md`, the schema description strings in the relevant `schema.ts` file, and `CLAUDE.md`. These can easily drift apart. Input is passed via flags. Define options in the command's zod schema — incur registers CLI flags automatically from the schema. @@ -76,6 +76,7 @@ CLI command is `spend-request` (user-facing). Implemented in `packages/cli/src/c Key input field notes: - CLI input uses `payment_method_id`; mapped to `payment_details` when calling the SDK +- `--execution-method link_pay_token` and `--merchant-account-id acct_...` are a create-only pair for Link Pay Token checkout. The agent reads the account ID from `data-stripe-merchant-account` in the AI-agent steering DOM before creating the request; Link resolves the canonical merchant identity. LPT uses `credential_type: card`, cannot use `--test` or `--network-id`, and must not accept agent-provided merchant name or URL. Never add the target fields to the update path. - `context` requires min 100 characters; `amount` is in cents with max 500000 - `--metadata` (create only) is a repeatable `key:value` flag (CLI) or a `{ key: value }` object (MCP/agent), merged into a single `metadata` string→string map. Max 50 keys, key ≤ 40 chars, value ≤ 500 chars. Reuses `parseKvString` from `line-item-parser.ts`. - `--test` flag creates testmode credentials (real testmode SPT from test card data) instead of livemode ones diff --git a/README.md b/README.md index 69a53e3..095f53a 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,36 @@ In MCP/agent mode, pass `metadata` as a structured `{ key: value }` object. By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, instead pass `--credential-type "shared_payment_token"`. +#### Link Pay Token + +Some Stripe checkout pages expose an AI-agent steering block that supports a +Link Pay Token (LPT). Inspect the checkout in a browser before creating the +SpendRequest: enable the agent checkbox, then verify that both +`input[name="link_pay_token"]` and +`data-stripe-merchant-account="acct_..."` are present in the same Stripe +frame. + +Create an LPT-bound request with the DOM-derived account ID. Do not pass +`--merchant-name` or `--merchant-url`; Link resolves the canonical merchant +identity from the account ID for the approval screen. + +```bash +link-cli spend-request create \ + --payment-method-id csmrpd_xxx \ + --execution-method link_pay_token \ + --merchant-account-id acct_... \ + --context "Purchasing an item from the checkout the agent inspected. The user initiated this purchase through the shopping assistant." \ + --amount 3500 \ + --request-approval +``` + +LPT requests use the default `card` credential type and do not support +`--test`, `--network-id`, or `shared_payment_token`. After approval, retrieve +`--include link_pay_token` immediately before using it on the same checkout +surface. Each returned LPT is valid for up to 30 minutes, or until the +SpendRequest expires. If either DOM marker is absent, create a regular virtual +card SpendRequest instead; do not create an LPT request. + ### Execute payment The approved spend request includes a `card` object with `number`, `cvc`, `exp_month`, `exp_year`, `billing_address`, and `valid_until`. Enter these into the merchant's checkout form. @@ -247,7 +277,13 @@ All commands accept `--auth ` to store auth credentials in a specific file A spend request moves through: **create** → **request approval** → **approved** (with credentials). -**Required fields for create:** `merchant_name`, `merchant_url`, `context`, `amount`. `payment_method_id` is optional — if omitted, your default payment method will be used, or the first eligible one if no default is set. +**Required fields for a regular card create:** `merchant_name`, `merchant_url`, +`context`, and `amount`. `payment_method_id` is optional — if omitted, +your default payment method will be used, or the first eligible one if no +default is set. Shared Payment Token requests instead require `network_id`; +Link Pay Token requests require `execution_method=link_pay_token` and the +DOM-derived `merchant_account_id`, and Link supplies their canonical merchant +identity. **Constraints:** `context` must be at least 100 characters; `amount` must not exceed 500000 (cents); `currency` must be a 3-letter ISO code. The user has 10 minutes from when approval is requested to approve. Approved credentials (card or SPT) are valid for 12 hours from spend request creation. **Test mode:** Pass `--test` to create a testmode SpendRequest. A testmode SpendRequest will return test payment credentials (e.g test card `4000009990001984`) rather than a real payment credential. Testmode SpendRequests will not charge the underlying payment method of the SpendRequest. This is useful for development and integration testing without real payment methods. diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts index 0f8fbd6..febd5f6 100644 --- a/packages/cli/src/__tests__/cli.test.ts +++ b/packages/cli/src/__tests__/cli.test.ts @@ -323,6 +323,152 @@ describe('production mode', () => { expect(request.network_id).toBe('net_prod_abc'); }); + it('sends Link Pay Token execution fields in HTTP POST body', async () => { + setNextResponse(200, { + ...BASE_REQUEST, + merchant_name: 'Canonical Merchant', + merchant_url: 'https://canonical.example', + }); + + const result = await runProdCli( + 'spend-request', + 'create', + '--payment-method-id', + 'pd_prod_test', + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--context', + VALID_CONTEXT, + '--amount', + '5000', + '--no-request-approval', + '--json', + ); + + expect(result.exitCode).toBe(0); + const sentBody = JSON.parse(lastRequest.body); + expect(sentBody).toMatchObject({ + payment_details: 'pd_prod_test', + credential_type: 'card', + execution_method: 'link_pay_token', + merchant_account_id: 'acct_lpt_target', + }); + expect(sentBody.merchant_name).toBeUndefined(); + expect(sentBody.merchant_url).toBeUndefined(); + }); + + const invalidLptCreateCases = [ + { + name: 'merchant-account-id without execution-method', + args: ['--merchant-account-id', 'acct_lpt_target'], + message: + 'execution-method link_pay_token is required when merchant-account-id is provided', + }, + { + name: 'execution-method without merchant-account-id', + args: ['--execution-method', 'link_pay_token'], + message: + 'merchant-account-id is required when execution-method is link_pay_token', + }, + { + name: 'blank merchant-account-id', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + ' ', + ], + message: + 'merchant-account-id is required when execution-method is link_pay_token', + }, + { + name: 'shared_payment_token credential type', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--credential-type', + 'shared_payment_token', + ], + message: + 'credential-type must be card when execution-method is link_pay_token', + }, + { + name: 'network-id', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--network-id', + 'net_lpt_target', + ], + message: + 'network-id cannot be used when execution-method is link_pay_token', + }, + { + name: 'test mode', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--test', + ], + message: 'test cannot be used when execution-method is link_pay_token', + }, + { + name: 'delegated approval', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--approve', + ], + message: + 'approve cannot be used when execution-method is link_pay_token; use request-approval instead', + }, + { + name: 'agent-provided merchant identity', + args: [ + '--execution-method', + 'link_pay_token', + '--merchant-account-id', + 'acct_lpt_target', + '--merchant-name', + 'Agent-provided Merchant', + ], + message: + 'merchant-name and merchant-url cannot be used when execution-method is link_pay_token', + }, + ]; + + for (const invalidCase of invalidLptCreateCases) { + it(`rejects Link Pay Token requests with ${invalidCase.name}`, async () => { + const result = await runProdCli( + 'spend-request', + 'create', + '--payment-method-id', + 'pd_prod_test', + ...invalidCase.args, + '--context', + VALID_CONTEXT, + '--amount', + '5000', + '--no-request-approval', + '--json', + ); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toContain(invalidCase.message); + expect(requests).toHaveLength(0); + }); + } + it('merges repeatable --metadata flags into a metadata object in POST body', async () => { setNextResponse(200, BASE_REQUEST); diff --git a/packages/cli/src/commands/spend-request/index.tsx b/packages/cli/src/commands/spend-request/index.tsx index 8224f47..c1a3d7e 100644 --- a/packages/cli/src/commands/spend-request/index.tsx +++ b/packages/cli/src/commands/spend-request/index.tsx @@ -99,6 +99,62 @@ export function createSpendRequestCli( const requestApproval = !!opts.requestApproval; const credentialType = opts.credentialType as CredentialType | undefined; const networkId = opts.networkId; + const executionMethod = opts.executionMethod; + const merchantAccountId = opts.merchantAccountId?.trim(); + const lptExecutionRequested = + executionMethod !== undefined || merchantAccountId !== undefined; + + if (lptExecutionRequested) { + if (executionMethod !== 'link_pay_token') { + return c.error({ + code: 'INVALID_INPUT', + message: + 'execution-method link_pay_token is required when merchant-account-id is provided', + }); + } + if (!merchantAccountId) { + return c.error({ + code: 'INVALID_INPUT', + message: + 'merchant-account-id is required when execution-method is link_pay_token', + }); + } + if (credentialType !== 'card') { + return c.error({ + code: 'INVALID_INPUT', + message: + 'credential-type must be card when execution-method is link_pay_token', + }); + } + if (networkId) { + return c.error({ + code: 'INVALID_INPUT', + message: + 'network-id cannot be used when execution-method is link_pay_token', + }); + } + if (opts.test) { + return c.error({ + code: 'INVALID_INPUT', + message: + 'test cannot be used when execution-method is link_pay_token', + }); + } + if (opts.approve) { + return c.error({ + code: 'INVALID_INPUT', + message: + 'approve cannot be used when execution-method is link_pay_token; use request-approval instead', + }); + } + if (opts.merchantName || opts.merchantUrl) { + return c.error({ + code: 'INVALID_INPUT', + message: + 'merchant-name and merchant-url cannot be used when execution-method is link_pay_token; Link resolves the merchant identity from merchant-account-id', + }); + } + } if (credentialType === 'shared_payment_token' && !networkId) { return c.error({ @@ -123,13 +179,21 @@ export function createSpendRequestCli( 'network-id can only be used when credential-type is shared_payment_token', }); } - if (credentialType !== 'shared_payment_token' && !opts.merchantName) { + if ( + !lptExecutionRequested && + credentialType !== 'shared_payment_token' && + !opts.merchantName + ) { return c.error({ code: 'INVALID_INPUT', message: 'merchant-name is required when credential-type is card', }); } - if (credentialType !== 'shared_payment_token' && !opts.merchantUrl) { + if ( + !lptExecutionRequested && + credentialType !== 'shared_payment_token' && + !opts.merchantUrl + ) { return c.error({ code: 'INVALID_INPUT', message: 'merchant-url is required when credential-type is card', @@ -174,6 +238,8 @@ export function createSpendRequestCli( payment_details: opts.paymentMethodId, credential_type: credentialType, network_id: networkId, + execution_method: executionMethod, + merchant_account_id: merchantAccountId, amount: opts.amount, currency: opts.currency, merchant_name: opts.merchantName, diff --git a/packages/cli/src/commands/spend-request/schema.ts b/packages/cli/src/commands/spend-request/schema.ts index 3459042..69f9dd2 100644 --- a/packages/cli/src/commands/spend-request/schema.ts +++ b/packages/cli/src/commands/spend-request/schema.ts @@ -6,7 +6,7 @@ export const createOptions = z.object({ .enum(['shared_payment_token', 'card']) .default('card') .describe( - '"card" for checkout forms/Stripe Elements; "shared_payment_token" for HTTP 402/machine payment flows', + '"card" for checkout forms and Link Pay Token; "shared_payment_token" for HTTP 402/machine payment flows', ), networkId: z .string() @@ -14,6 +14,18 @@ export const createOptions = z.object({ .describe( 'Network ID (required for shared_payment_token) — use `link-cli mpp decode` to extract', ), + executionMethod: z + .enum(['link_pay_token']) + .optional() + .describe( + 'Use link_pay_token only with merchant_account_id read from the checkout AI-agent steering DOM', + ), + merchantAccountId: z + .string() + .optional() + .describe( + 'Stripe account ID from data-stripe-merchant-account; required with execution_method link_pay_token', + ), amount: z.coerce .number() .int() @@ -25,13 +37,13 @@ export const createOptions = z.object({ .string() .optional() .describe( - 'Merchant name (required for card; forbidden for shared_payment_token)', + 'Merchant name (required for regular card requests; omit for link_pay_token and shared_payment_token)', ), merchantUrl: z .string() .optional() .describe( - 'Merchant URL (required for card; forbidden for shared_payment_token)', + 'Merchant URL (required for regular card requests; omit for link_pay_token and shared_payment_token)', ), context: z .string() diff --git a/packages/sdk/src/resources/__tests__/spend-request.test.ts b/packages/sdk/src/resources/__tests__/spend-request.test.ts index 2c9bbd6..4ca9918 100644 --- a/packages/sdk/src/resources/__tests__/spend-request.test.ts +++ b/packages/sdk/src/resources/__tests__/spend-request.test.ts @@ -117,6 +117,22 @@ describe('SpendRequestResource', () => { expect(result.network_id).toBe('net_abc'); }); + it('serializes Link Pay Token execution fields in POST body', async () => { + const paramsWithLptExecution: CreateSpendRequestParams = { + ...validParams, + execution_method: 'link_pay_token', + merchant_account_id: 'acct_lpt_target', + }; + mockFetchResponse(200, spendRequestResponse); + + await repo.createSpendRequest(paramsWithLptExecution); + + const [, opts] = mockFetch.mock.calls[0]; + const sentBody = JSON.parse(opts.body); + expect(sentBody.execution_method).toBe('link_pay_token'); + expect(sentBody.merchant_account_id).toBe('acct_lpt_target'); + }); + it('serializes metadata in POST body', async () => { const paramsWithMetadata: CreateSpendRequestParams = { ...validParams, @@ -168,10 +184,13 @@ describe('SpendRequestResource', () => { expect(sentBody.test).toBeUndefined(); }); - it('sends to /spend_requests/create_delegated when approve is true', async () => { + it('sends delegated requests to /spend_requests/create_delegated when approve is true', async () => { mockFetchResponse(200, spendRequestResponse); - await repo.createSpendRequest({ ...validParams, approve: true }); + await repo.createSpendRequest({ + ...validParams, + approve: true, + }); const [url, opts] = mockFetch.mock.calls[0]; expect(url).toBe('https://api.link.com/spend_requests/create_delegated'); diff --git a/packages/sdk/src/resources/interfaces.ts b/packages/sdk/src/resources/interfaces.ts index e70bba3..cd69673 100644 --- a/packages/sdk/src/resources/interfaces.ts +++ b/packages/sdk/src/resources/interfaces.ts @@ -55,6 +55,8 @@ export interface CreateSpendRequestParams { payment_details?: string; credential_type?: CredentialType; network_id?: string; + execution_method?: 'link_pay_token'; + merchant_account_id?: string; amount?: number; currency?: string; merchant_name?: string; diff --git a/skills/create-payment-credential/SKILL.md b/skills/create-payment-credential/SKILL.md index 720aefc..bf150a6 100644 --- a/skills/create-payment-credential/SKILL.md +++ b/skills/create-payment-credential/SKILL.md @@ -33,6 +33,10 @@ The CLI can produce one of two credential types: - A virtual card (PAN) for use with a standard web checkout form. The issued card works anywhere. - A Shared Payment Token (SPT) when the seller is in the Stripe Network and accepts payments programmatically (for example with Machine Payment Protocols). +It can also create a Link Pay Token (LPT)-bound SpendRequest for a supported +Stripe checkout surface. LPT is an execution mode for the card flow, not a +third credential type. + ## Installing Install with `npm install -g @stripe/link-cli`. Or run directly with `npx @stripe/link-cli`. @@ -116,7 +120,7 @@ If the user is already authenticated but you need broader access (an additional **Determine how the merchant accepts payment:** 1. **Navigate to the merchant page** — browse it, read the page content, and understand how the site accepts payment. -2. **If the checkout page includes the AI-agent steering block** (find the "I am an AI agent" checkbox, or the `.AiAgentPaymentSteering` container — visually hidden but present in the DOM, typically inside a Stripe iframe) — it may support the **Link Pay Token flow** (Step 5, "Link Pay Token" section). **Requires browser automation.** Confirm before committing to it: check the checkbox and see whether `input[name="link_pay_token"]` then appears. If it does, use the token flow. If it does **not** (some surfaces render the steering block but keep the input disabled), follow the block's on-page instructions and use `card` instead. Without browser automation, use `card`. +2. **If the checkout page includes the AI-agent steering block** (find the "I am an AI agent" checkbox, or the `.AiAgentPaymentSteering` container — visually hidden but present in the DOM, typically inside a Stripe iframe) — it may support the **Link Pay Token flow** (Step 5, "Link Pay Token" section). **Requires browser automation.** Before creating an LPT request, check the checkbox and verify that both `input[name="link_pay_token"]` and `data-stripe-merchant-account` appear in the same frame. Read the account ID from that attribute. If either marker does **not** appear, follow the block's on-page instructions and use `card` instead. Without browser automation, use `card`. 3. **If the page has a credit-card form and no AI-agent steering block** (no "I am an AI agent" checkbox / `.AiAgentPaymentSteering`) — use `card`. 4. **If the page describes an API or programmatic payment flow** — make a request to the relevant endpoint. If it returns **HTTP 402** with a `www-authenticate` header, use `shared_payment_token`. @@ -124,7 +128,7 @@ What you find determines which credential type to use: | What you see | Credential type | What to request | |---|---|---| -| `.AiAgentPaymentSteering` block / "I am an AI agent" checkbox, and ticking it reveals `input[name="link_pay_token"]` | (none needed) | Link Pay Token flow (else `card`) | +| `.AiAgentPaymentSteering` block / "I am an AI agent" checkbox, and ticking it reveals both `input[name="link_pay_token"]` and `data-stripe-merchant-account` | (none needed) | Link Pay Token flow (else `card`) | | Credit-card form, no AI-agent steering block | `card` (default) | Card | | HTTP 402 with `method="stripe"` in `www-authenticate` | `shared_payment_token` | Shared payment token (SPT) | | HTTP 402 without `method="stripe"` in `www-authenticate` | not supported | Do not continue | @@ -147,6 +151,10 @@ link-cli shipping-address list ### Step 4: Create the spend request with the right credential type +For card and Shared Payment Token flows, use the command below. For Link Pay +Token, do **not** create this generic request: follow the LPT instructions in +Step 5 after you have read the merchant account ID from the checkout DOM. + ```bash link-cli spend-request create \ --payment-method-id \ @@ -171,7 +179,7 @@ link-cli spend-request cancel Recommend the user approves with the [Link app](https://link.com/download). Show the download URL. -**Test mode:** Add `--test` to create testmode credentials instead of real ones. Useful for development and integration testing. +**Test mode:** Add `--test` to create testmode credentials instead of real ones. Useful for development and integration testing. Link Pay Token does not support test mode. **Approval details:** For delegated/pre-approved flows, pass `--approval-detail` as a JSON object (MCP/agent) or JSON string (CLI). Required fields: `approved_at` (unix timestamp), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`). @@ -203,33 +211,69 @@ The SPT is **one-time use** — if the payment fails, run `mpp pay` again (it wi link-cli mpp pay --spend-request-id [-X POST] [-d ''] [-H 'Name: Value'] ``` -**Link Pay Token:** Some checkout pages embed an AI-agent steering block (the `AiAgentPaymentSteering` component) that lets an agent pay with a Link Pay Token, using the consumer's saved card without handling card numbers. This flow requires browser automation. +**Link Pay Token:** Some checkout pages embed an AI-agent steering block (the +`AiAgentPaymentSteering` component) that lets an agent pay with a Link Pay +Token, using the consumer's saved card without handling card numbers. This flow +requires browser automation. -The block is visually hidden but present in the DOM, and may be inside a Stripe frame. Do not assume a fixed location -- search the top document and any Stripe frames for the `.AiAgentPaymentSteering` block or the "I am an AI agent" checkbox, and run the snippets below in whichever frame contains it. Checking the checkbox reveals the block's own instructions and, where the inline token is supported, the `link_pay_token` input. **The block is the source of truth -- follow the steps it renders.** +The block is visually hidden and may be inside a Stripe frame. Do not assume a +fixed location: search the top document and Stripe frames for +`.AiAgentPaymentSteering` or the "I am an AI agent" checkbox, and run the +following steps in the frame that contains it. -1. Create a spend request (same as Step 4 -- no `--credential-type` flag needed) and get approval. +1. Open the merchant checkout page and locate the steering block. -2. Open the merchant checkout page. - -3. **Check the "I am an AI agent" checkbox** to reveal the block, then read and follow the instructions it renders. Use a DOM-level `click()` -- the control is keyboard-hidden, so a normal automated click may be refused as not actionable: +2. **Check the "I am an AI agent" checkbox** to reveal the block. Use a + DOM-level `click()` because the control is keyboard-hidden: ```javascript document.querySelector('.AiAgentPaymentSteering input[type="checkbox"]').click(); ``` -4. **Confirm the token path is available.** Within a few seconds, `input[name="link_pay_token"]` should appear in the same frame. - - If it appears, continue. - - If it does **not** appear, this surface renders the steering block but does not enable the inline token input. Do **not** loop waiting for it. The spend request you created uses the default (`card`) credential type, so you can retrieve `--include card` on the **same** spend request (no new request, no re-approval) and use the card flow, follow the block's instructions to pay without Link, or report `blocked`. +3. **Confirm the bound token path is available before creating a + SpendRequest.** Within a few seconds, the same frame must contain both + `input[name="link_pay_token"]` and a + `data-stripe-merchant-account="acct_..."` attribute on the steering block. + + ```javascript + const merchantAccountId = document + .querySelector( + '.AiAgentPaymentSteering [data-stripe-merchant-account]', + ) + ?.getAttribute('data-stripe-merchant-account'); + ``` + + If either marker is absent or `merchantAccountId` is empty, do **not** + create an LPT request. Use the normal `card` flow instead. -5. **Retrieve the token now** -- it is short-lived (~5 minutes), so fetch it right before injecting, not earlier: +4. **Create the merchant-bound SpendRequest.** Use the DOM-derived account ID; + do not send `--merchant-name` or `--merchant-url`. Link resolves the + canonical merchant identity before the consumer approves. ```bash - link-cli spend-request retrieve --include link_pay_token --format json + link-cli spend-request create \ + --execution-method link_pay_token \ + --merchant-account-id \ + --payment-method-id \ + --amount \ + --context "" \ + --line-item "name:,unit_amount:,quantity:" \ + --total "type:total,display_text:Total,amount:" ``` - The response includes `link_pay_token: "eyJ..."`. + LPT uses the default `card` credential type. Do not set + `--credential-type shared_payment_token`, `--network-id`, or `--test`. + Present the approval URL and wait for approval before retrieving a token. -6. **Inject the token** into `input[name="link_pay_token"]` with the native value setter. Do NOT type it in -- it is a long JWT and character-by-character typing will time out: +5. **Retrieve the token immediately before injecting it.** Each returned LPT + is valid for up to 30 minutes, or until the SpendRequest expires: + + ```bash + link-cli spend-request retrieve --include link_pay_token --format json + ``` + +6. **Inject the token** into `input[name="link_pay_token"]` with the native + value setter. Do not type it character by character: ```javascript const input = document.querySelector('input[name="link_pay_token"]'); @@ -238,20 +282,28 @@ The block is visually hidden but present in the DOM, and may be inside a Stripe input.dispatchEvent(new Event('input', { bubbles: true })); ``` -7. **Wait for the exchange and login to complete.** The card form is replaced by a single saved card showing the consumer's email in the header -- that is your go signal. (In the network panel you will see `POST /v1/link/auth_token/exchange` succeed, then `/v1/consumers/sessions/lookup` return the authorized card.) - -8. Click the Pay/Submit button. Payment confirms without CVC or CAPTCHA. +7. **Wait for the exchange and login to complete.** The card form is replaced + by a single saved card showing the consumer's email in the header. Then + click the Pay/Submit button. -**If it does not transition, stop -- do not loop.** If the checkbox is absent, the input never appears after you check it, or the saved card does not replace the form within ~10s, then the token path is not available here or the token expired. Retry at most once with a freshly retrieved token. Otherwise fall back to the `card` flow: the spend request uses the default (`card`) credential type, so retrieve `--include card` on the **same** spend request (no new request, no re-approval) and use the card details, or report `blocked` (see "Reporting outcomes"). Re-injecting or re-scanning will not enable a surface that has the input turned off. +**If it does not transition, stop -- do not loop.** If injection is delayed, +retrieve one fresh token and retry once. If the saved card does not replace the +form, cancel the bound SpendRequest and create a new normal card request, or +report `blocked`. Do not reuse the LPT at a different checkout surface. **Important notes for the Link Pay Token flow:** -- The block is the source of truth -- follow the steps it renders after you check the box. -- The token is short-lived (~5 minutes) -- retrieve it right before injecting (step 5); if injection is delayed, retrieve a fresh one. -- The controls are invisible to a human and may live in a Stripe frame -- operate them programmatically in whichever frame contains the block, not by visible-element clicks. -- Card numbers are not needed -- the token authorizes payment directly using the consumer's saved card on file. -- The agent pays with the token, not an interactive Link login. If the checkbox is missing, a signed-in Link session may be showing the Link wallet instead of the card form -- retry in a context not signed in to Link. -- The token flow and the card flow share one spend request -- it uses the default (`card`) credential type, so if the token path is unavailable you can retrieve `--include card` on the same request; no new request or approval is needed. -- The consumer only sees the card they authorized in the spend request. +- The account ID is browser-provided input, not proof of merchant identity. + Link resolves it server-side and shows canonical merchant identity to the + consumer before approval. +- The controls are invisible to a human and may live in a Stripe frame -- + operate them programmatically in the frame that contains the block. +- Card numbers are not needed -- the token authorizes payment directly using + the consumer's saved card on file. +- The agent pays with the token, not an interactive Link login. If the checkbox + is missing, a signed-in Link session may be showing the Link wallet instead + of the card form; retry in a context not signed in to Link. +- A bound LPT request is not the fallback virtual-card request. If the marker + is missing before creation, create a normal card SpendRequest instead. ## Important @@ -353,4 +405,4 @@ Report output is agent-only (not shown to the user). Reporting is encouraged but - MPP/x402 protocol: https://mpp.dev/protocol.md, https://mpp.dev/protocol/http-402.md, https://mpp.dev/protocol/challenges.md - Link: https://link.com/agents - Link App (for account management): https://app.link.com -- Link support (if the user needs help with Link): https://support.link.com/topics/about-link \ No newline at end of file +- Link support (if the user needs help with Link): https://support.link.com/topics/about-link