Repository navigation
feat(x402): Add the x402 credit purchases endpoint - #6648
Conversation
3af148e to
2667e81
Compare
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — the new purchase path can settle and grant credits against targets that the gate path would reject.
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — pending purchases have no completion path. A facilitator timeout or |
78ab7ba to
40b2dbb
Compare
|
Regarding the automated pre-review:
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — ambiguous settlements can remain pending forever, potentially taking funds without issuing credits.
|
This is handled on #6651 |
## Context E8's refusals, the settle pipeline and the grant exist, but nothing runs them in order for a credit purchase. ## Description `X402::CreditPurchases::PurchaseService` computes the payment digest, then runs the refusals that need no payer, `/verify`, the refusals that need the verified payer, the `pending` row and `/settle`. A settled answer is flipped alone and then granted; any other answer leaves the row `pending` for reconciliation. A grant that fails after the settle answers `credit_grant_failed` on `base`, whatever stopped it, so the caller knows the money moved; the settled row is what a replay grants from.
## Context The middleware resends a payment when E8's answer is lost or late, so a resent payment must never reach the facilitator again, and must not be refused by settings that changed since. ## Description The payment digest is looked up first, among the organization's pending and settled rows, before the connection and every refusal. A granted row answers as it is, a settled row without a grant is granted from its stored settings, a pending row answers pending, and an invoice payment is refused as `recorded_as_invoice_payment`. An insert that loses a race on the digest or the payer's pending purchase looks the digest up again, so a payment sent twice at once answers from the row that won. Replays are logged with whether their settings differ from the row's.
## Context The lago-x402 middleware needs an endpoint to hand over the payment an agent signed, and to learn whether credits were granted or the payment is still settling. ## Description `POST /api/v1/x402/credit_purchases` runs under the `x402` API-key scope, with the body under a `credit_purchase` root key. `payment` and `payment_requirements` are read from the raw JSON body, because parameter parsing drops `null` elements from arrays and they must reach the facilitator as the agent signed them. `V1::X402::SettlementSerializer` renders the settlement under `x402_settlement`: the grant once settled, `reconcile_after` while pending, and whether the answer is a replay.
## Context A credit purchase answers `credit_grant_failed` whenever the grant fails after the money moved, so the middleware knows to serve the agent and resend the same payment. An exception raised inside the grant, rather than a failed result, escaped as a 500, which looks like nothing happened. ## Description An exception raised by the grant after the settle is reported to Sentry and answered as `credit_grant_failed`, like a failed grant; the settled row is still what a replay grants from.
## Context The credit purchases endpoint reads the signed payment from the raw JSON body. A body that isn't JSON, or one without the `credit_purchase` root, raised a 500. ## Description When the raw body can't supply the payment, the request carries none, and the purchase is refused as `invalid_payment` like any other request without one.
## Context A `credit_purchase` sent as a string, a number or an array made strong parameters raise, so the endpoint answered a 500 instead of refusing the request. ## Description The root is read with `params.expect`, which refuses a missing or non-object root with a 400. A form-encoded body, whose raw body isn't JSON, is covered too: it carries no payment, so it is refused as `invalid_payment`.
## Context The purchase specs did not cover a grant failing on a replay, a subscription id held by another customer, or a payer at the wallet cap. ## Description Add those cases: the last two are refused before `/settle`, and a replay whose grant fails answers `credit_grant_failed`, as a live purchase does.
40b2dbb to
ceeec5e
Compare
Context
E8 is how the lago-x402 middleware turns an agent's signed payment into credits: Lago settles the payment itself, then grants. This completes the endpoint on top of its refusals.
Description
X402::CreditPurchases::PurchaseServicelooks the payment digest up first and answers a replay from its row; otherwise it runs the refusals,/verify, thependingrow and/settle, flips a settled row alone, then grants.pendingwithreconcile_after. Any grant failure after the settle answerscredit_grant_failed, so the middleware knows the money moved, and the settled row is what a replay grants from.POST /api/v1/x402/credit_purchasesrenders the settlement underx402_settlement. It reads the payment from the raw body, because parameter parsing dropsnullelements from arrays and the payment must reach the facilitator as the agent signed it.