A payment settled; the request failed
A new paid short URL crosses two systems with different transaction boundaries: facilitator settlement and PostgreSQL persistence. The ordinary response is easy to recognize—201 Created with a short URL—but understanding failure requires asking which side effect happened first. A request can return 503 after the payment settled and before the mapping was saved.
This walkthrough follows the implemented order. It distinguishes the bounded testnet HTTP observation from payment guarantees that the repository does not establish.
Enlarge diagram: Paid request from challenge and client signing through verification, settlement, durable insertion, and redirect · Version-controlled diagram source
Start with a validated destination and an authoritative lookup
Section titled “Start with a validated destination and an authoritative lookup”The client submits POST /shorten with {"url":"https://example.invalid/article"}. This is an illustrative destination, not a recorded request. Validation strips surrounding whitespace and requires an absolute HTTP or HTTPS URL without embedded credentials. Invalid request data produces FastAPI validation errors before route execution.
The route queries PostgreSQL for the destination before it examines payment. If the mapping exists, it returns 200 with code, short_url, and original_url. It does not verify or settle an attached payment header. Reusing a destination is an application idempotency behavior distinct from replaying a settlement for a different destination.
If the database lookup fails, the route returns 503 with database unavailable before attempting payment. This ordering reduces needless payment work when storage is already known to be unavailable. It cannot guarantee storage will remain available after the lookup.
Payment mode is enabled when facilitator URL, service wallet, and asset contract settings are all nonempty. The default local Compose stack omits them and performs unpaid creation. Configured amount and network alone do not enable payment.
The server offers terms; the client obtains authorization
Section titled “The server offers terms; the client obtains authorization”For a new destination in payment mode, a missing PAYMENT-SIGNATURE produces 402 Payment Required. The PAYMENT-REQUIRED response header contains standard-base64 JSON describing x402 version 2 and server-owned exact/Permit2 requirements. The current response body is {}; clients need the header to read the challenge.
The requirements include network, atomic-unit amount, asset, recipient, and a 300 second maximum timeout. These values come from server configuration rather than the submitted envelope. The amount is a decimal string in the wire contract, avoiding floating-point payment arithmetic.
The client asks the separate signer for an authorization. The signer owns payer keys and validated settings. It signs an EIP-712 Permit2 message that binds token, configured amount, proxy spender, random uint256 nonce, expiry, and recipient witness. The domain omits a version field, and signature recovery uses Ethereum-compatible v values. The proxy is the spender; the facilitator is not substituted for it.
Signing happens locally after bootstrap. At startup, the signer verifies its chain and checks each configured wallet’s Permit2 allowance, optionally sending an approval transaction. Request-time signing performs neither RPC nonce lookup nor settlement. The signer returns authorization and signature material to the client without returning a private key. Such request material is intentionally excluded from public evidence here.
Verification precedes settlement
Section titled “Verification precedes settlement”The client submits its envelope in PAYMENT-SIGNATURE. The application strictly decodes standard-base64 JSON and requires a JSON object. It then calls facilitator /verify with both that object and the server’s requirements. The application does not itself cryptographically verify the authorization or fully compare the client’s accepted terms; the configured facilitator is the authority evaluating them against the separate server-owned requirements.
A successful verification needs boolean isValid: true. A false verdict returns a coarse rejection outcome, while a missing or wrongly typed verdict cannot be treated as valid. Only a valid verification reaches /settle, using the same envelope and server-owned requirements.
Successful settlement needs boolean success: true, a canonical 32-byte hexadecimal transaction identifier, and a correctly shaped payer address if one is provided. Validation happens before the identifier can be persisted. HTTP 200 by itself is insufficient, and an arbitrary transaction-like string cannot become a successful creation.
The exact HTTP mapping has an implemented discrepancy from the payment contract. Transport errors, HTTP errors, malformed JSON, and non-object responses are classified as facilitator unavailable and become 503. An object with structurally invalid verification or settlement fields becomes invalid_response and the current route maps it to 402, together with rejected payment outcomes. The design contract describes malformed facilitator responses generally as 5xx. The API reference records the actual narrower mapping rather than silently changing application behavior.
Persistence is a second transaction boundary
Section titled “Persistence is a second transaction boundary”After validated settlement, the route generates a code and attempts insertion. Paid rows include the destination, code, settlement identifier, optional payer, and settlement time. Database constraints enforce unique codes, unique destinations, and unique non-null settlement identifiers. Conflict-safe insertion uses ON CONFLICT DO NOTHING and makes up to three code attempts, checking destination and settlement conflicts after an unsuccessful insert.
If insertion succeeds, url_shortener_urls_created_total increments and the route returns 201. An existing destination found after a concurrent insert returns 200. A previously used settlement associated with another destination increments url_shortener_payment_replays_total and returns 409 with payment settlement has already been used.
The unique settlement index stops a duplicate application association even when the facilitator returns an idempotent prior settlement result. It does not itself stop a facilitator call: settlement occurs before the database checks that identifier. The intended complementary layers are Permit2 nonce consumption, possible facilitator idempotency, and the application constraint. Only the last layer is implemented inside this repository’s database path.
Concurrent submissions for the same previously absent destination can both pass the initial lookup and reach payment before one wins the unique destination insert. The destination constraint protects the resulting mapping, but this application does not implement a distributed reservation that guarantees no additional settlement under that race. The ordinary existing-destination shortcut should not be read as a complete concurrency payment guarantee.
The gap after settlement is an operational limit
Section titled “The gap after settlement is an operational limit”Suppose /settle returns a validated success and the subsequent database command fails. The route returns 503; no success response is invented. There is no shared transaction spanning facilitator and PostgreSQL, no compensating refund, and no built-in reconciliation endpoint or durable pre-settlement journal in this application.
Reconciliation by the validated settlement identifier is the required operational principle in the design contract. It is not an implemented recovery workflow that this site can claim to demonstrate. Blindly obtaining a fresh authorization and paying again can worsen the incident. The repository contains tests for valid creation, payment rejection, and application replay, but it does not retain a public live post-settlement database-failure case proving recovery.
The metrics also reflect the split. The payment settled outcome increments when processing returns settlement success, before insertion. It therefore can increase without urls_created_total increasing. Facilitator call duration measures its individual /verify and /settle requests, not database persistence. The load client’s payment_settled_rate uses paid 201 as its success proxy, and its end-to-end success additionally needs a redirect. These observations describe different points in the path.
Follow the returned code
Section titled “Follow the returned code”GET /{code} first reads Redis. A miss queries PostgreSQL, attempts a best-effort cache fill, and returns 302 with the destination in Location. The bounded load client disables redirect following, so it tests the application’s resolution without contacting the destination website. An unavailable cache may add delay and database work; an unavailable database blocks an uncached resolution.
The sanitized owned-testnet smoke records 402 → signed 201 → redirect 302 → replay 409. That is a scoped HTTP/application replay observation, not audited payment finality, custody, or universal settlement availability. Its public provenance is in the verification report; historical screenshots are interpreted separately in the evidence guide.
Inspect routes and schema, payment normalization, Permit2 signing, paid route tests, facilitator tests, and replay tests. Continue with API behavior or signer failure.
Maintained by Satyam Agnihotri · DevOps & Cloud Engineer