<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Fone Engineering Notes]]></title><description><![CDATA[Fone Engineering Notes]]></description><link>https://0xfone.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 18:32:29 GMT</lastBuildDate><atom:link href="https://0xfone.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Cancel Stale Wallet Prompts with Async Generation Checks]]></title><description><![CDATA[Reproduce stale wallet prompts with a Python async lab. Test cancellation, account changes and duplicate confirms with ten deterministic tests.]]></description><link>https://0xfone.hashnode.dev/cancel-stale-wallet-prompts-with-async-generation-checks</link><guid isPermaLink="true">https://0xfone.hashnode.dev/cancel-stale-wallet-prompts-with-async-generation-checks</guid><category><![CDATA[Web3]]></category><category><![CDATA[Python]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[HaoJie Luo (Fone)]]></dc:creator><pubDate>Fri, 13 Nov 2026 02:16:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae5e34a547ddc5d64387372/8616f112-111e-4315-a1b1-3c806bf27fd5.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://fone.cc/insights/cancel-stale-wallet-prompts-async-generation-checks/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=hashnode_gu_l01_20261112"><em>Fone</em></a><em>.</em></p>
<p>A canceled authorization review should never open a wallet prompt a moment later. That can happen when a page waits for simulation or an account check, the user cancels, and the old asynchronous function resumes with values it captured before cancellation.</p>
<p>This lab builds a small asynchronous controller that reproduces that race and prevents the stale prompt. The result is a Python program with deterministic tests, including cancellation during the final wallet read. It uses fake callbacks and contains no signing keys, RPC calls or transactions. <a href="https://fone.cc/wp-content/uploads/2026/10/cancel-stale-wallet-prompts-async-generation-checks-lab-v1.zip">Download the complete runnable lab</a>.</p>
<p>The engineering question comes from Agent Wallet Guard's Owner authorization flow. A source review on 29 October 2026 found a generation check after simulation and another after the final wallet checks, immediately before the transaction path. Its historical local acceptance report also records a cancellation fix. Here we isolate that narrow behavior so you can inspect the timing without running the application stack.</p>
<h2>Run the race locally</h2>
<p>Use Python 3.13.3, the version used for this lab's verification. There are no packages to install. Extract the ZIP, open its directory and run the following commands.</p>
<pre><code class="language-bash">python3 preflight.py
python3 -m unittest -v
</code></pre>
<p>The demo pauses simulation using an event, cancels the review, then releases simulation. Its output reports <code>stale</code>, zero wallet prompts and the <code>idle</code> phase. No sleep duration determines the result. The test controls exactly when the delayed work is allowed to finish.</p>
<p>That detail matters during debugging. A test that waits ten milliseconds and hopes cancellation wins may pass on one machine and fail on another. An event gives the test a specific observation point. The simulated preflight has started, its result has not returned, and cancellation happens inside that interval.</p>
<h2>Capture what the Owner actually reviewed</h2>
<p>The controller uses a frozen context containing an Owner label, chain ID, deployment label and Session label. These are synthetic identifiers. An integration would use the actual account and contract context required by its authorization protocol.</p>
<pre><code class="language-python">@dataclass(frozen=True)
class Context:
    owner: str
    chain: int
    deployment: str
    session: str
</code></pre>
<p>A chain ID alone cannot describe the entire review. Two deployments can use the same local chain ID, while a page can switch to another Session without changing the connected account. Comparing all four fields lets the exercise detect those separate changes.</p>
<p>Each review also receives an increasing generation number. Canceling a review advances that number. Old work can still finish, but it no longer owns the right to open a prompt or reset the current interface. This distinction lets the page begin a new review while an older request is still returning.</p>
<p>The number is local control state. It does not travel to a contract and does not grant spending permission. Agent Wallet Guard's actual Session validation remains a separate part of execution. The existing <a href="https://fone.cc/insights/ai-agent-wallet-spending-limit-enforcement/">spending limit article</a> explains that other boundary.</p>
<h2>Check again after the last wait</h2>
<p>The following method is the complete prompt gate from the downloadable controller. <code>simulate</code>, <code>read_wallet</code> and <code>prompt</code> are injected callbacks, which lets the test stop each stage independently.</p>
<pre><code class="language-python">async def confirm(self, ticket, simulate, read_wallet, prompt):
    generation, reviewed = ticket
    if generation != self.generation or self.phase != "review":
        return "stale"
    self.phase = "preflight"
    try:
        await simulate(reviewed)
        if generation != self.generation:
            return "stale"
        current = await read_wallet()
        if generation != self.generation:
            return "stale"
        if current != reviewed or self.context != reviewed:
            self.phase = "idle"
            return "context_changed"
        # There is no await between the final guard and entering wallet phase.
        self.phase = "wallet"
        await prompt(reviewed)
        self.phase = "submitted"
        return "submitted"
    except Exception:
        if generation == self.generation:
            # Once invoked, a failed provider response may hide a broadcast.
            self.phase = "uncertain" if self.phase == "wallet" else "idle"
        raise
</code></pre>
<p>The first check rejects a duplicate confirm call as soon as another call enters preflight. The next check runs after simulation. A further check runs after reading the wallet, because cancellation could have happened during that read too. Passing the earlier check gives no guarantee about later suspended work.</p>
<p>Immediately before invoking the prompt, the code compares the returned context with the reviewed context and the controller's current context. It then moves into the wallet phase without another wait between the final guard and that transition. Within this single asyncio event loop, another task cannot slip a cancellation into that synchronous segment.</p>
<p>This is a control-flow guarantee inside the teaching model. A browser wallet and a chain remain separate systems. A real integration must also handle provider account changes and validate the actual transaction. The small controller helps reviewers locate one race; it cannot prove every race in those other systems is absent.</p>
<img src="https://fone.cc/wp-content/uploads/2026/10/cancel-stale-wallet-prompts-async-generation-checks-diagram.webp" alt="A canceled review invalidates delayed preflight work before it reaches the wallet prompt" style="display:block;margin:0 auto" />

<h2>Test the latest possible cancellation</h2>
<p>The most revealing test pauses the final wallet read. Simulation has already passed, which catches an implementation that checks cancellation only after simulation.</p>
<pre><code class="language-python">async def test_cancel_during_last_wallet_read_blocks_prompt(self):
    entered, release = asyncio.Event(), asyncio.Event()
    async def slow():
        entered.set()
        await release.wait()
        return self.context
    task = asyncio.create_task(self.run_ticket(self.flow.review(), wallet=slow))
    await entered.wait()
    self.flow.cancel()
    release.set()
    self.assertEqual(await task, "stale")
    self.assertEqual(self.prompts, [])
</code></pre>
<p>Run this test alone while changing the controller. If you remove the generation check after <code>read_wallet</code>, the old continuation can incorrectly open a prompt when the returned context still matches. The test expects zero prompts and therefore fails on the unsafe behavior.</p>
<p>Another test delays an old simulation until a fresh review exists, then makes the old simulation fail. The exception handler checks the generation before changing the phase. Without that condition, a late error from the abandoned attempt could reset the new review to idle. This case often gets missed when cancellation is tested only on successful requests.</p>
<p>The suite also changes each context field independently, submits the same ticket twice, and checks a preflight exception. Ten named tests passed from a fresh extraction of the archive. These are tests of the teaching controller; the product's application and contract suites were not rerun for this article.</p>
<h2>Decide where cancellation stops</h2>
<p>Once the prompt callback starts, <code>cancel</code> returns false. That is a deliberate boundary in this exercise. Dismissing a page-level review cannot undo an already invoked external operation.</p>
<p>The same distinction controls error handling. A simulation error returns the controller to idle. An error from the prompt leaves it in <code>uncertain</code>, where a new review is blocked. A provider response could have been lost after a transaction was submitted, so this model requires a separate recovery decision before another attempt. It deliberately has no button that silently retries an uncertain send.</p>
<p>Agent Wallet Guard has a broader recovery flow that distinguishes chain confirmation from API synchronization. The <a href="https://fone.cc/insights/onchain-authorization-api-registration-recovery/">authorization recovery article</a> covers those records. Keep that recovery concern separate when porting this lab. A generation counter belongs to the currently mounted interaction; it cannot replace a durable transaction record across a refresh.</p>
<h2>Turn the race into a handover demonstration</h2>
<p>For a team adding paid actions to an Agent product, this gives a concrete review script. Start an authorization, delay preflight, cancel it, and prove that the wallet stays closed after the delayed response arrives. Repeat during the last account read, then switch the account or Session and check that a new review is required.</p>
<p><a href="https://fone.cc/products/agent-wallet-guard/">Agent Wallet Guard</a> provides the Owner workspace, independent signing components and Session authorization path that motivate this exercise. Its recorded demonstration uses local chain 1337 and MockUSDC. Actual Agent services, target networks and wallet combinations require their own integration and acceptance work. Bring the cancellation test to that walkthrough and ask to see the exact point where an abandoned review loses permission to prompt.</p>
<p><em>Prepared with AI assistance. The source wording and code are preserved.</em></p>
]]></content:encoded></item><item><title><![CDATA[Is a Valid Payment Webhook Signature Enough to Fulfill an Order?]]></title><description><![CDATA[Check payment webhook signatures, order binding and current refund state before fulfillment, with a small exercise and a merchant acceptance worksheet.]]></description><link>https://0xfone.hashnode.dev/is-a-valid-payment-webhook-signature-enough-to-fulfill-an-order</link><guid isPermaLink="true">https://0xfone.hashnode.dev/is-a-valid-payment-webhook-signature-enough-to-fulfill-an-order</guid><category><![CDATA[webhooks]]></category><category><![CDATA[Python]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[HaoJie Luo (Fone)]]></dc:creator><pubDate>Thu, 12 Nov 2026 13:25:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae5e34a547ddc5d64387372/1c37dcea-12f7-4688-8242-6d65dc795bbf.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://fone.cc/insights/payment-webhook-signature-fulfillment-checks/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=hashnode_st_t01_20261112"><em>Fone</em></a><em>.</em></p>
<p>A valid payment webhook signature is one condition for accepting a notification. Before granting access, the merchant still needs to bind the event to its own order and check whether that order is eligible for delivery now. A correctly signed payment event can arrive after a full refund.</p>
<p>Reading StableIn's SDK alongside its merchant example makes that division unusually clear. The SDK checks the signed bytes. The merchant then checks the event structure, payment evidence and purchase details. Only after reading the current order does it decide whether to queue delivery. The implementation discussed here is a local demo using test assets, not a production security assessment.</p>
<h2>Start with the bytes that arrived</h2>
<p>StableIn signs the timestamp, a dot and the raw request body with HMAC-SHA256. Its verifier rejects a timestamp more than 300 seconds away from the supplied current time, compares the signature with a timing-safe comparison and checks that the event ID in the body matches the event ID header.</p>
<p>The raw body matters. Parsing JSON and serializing it again can change whitespace or key order. That can change the signed bytes while leaving the apparent meaning intact. Preserve the original bytes for verification before handing a parsed object to the rest of the application. Stripe documents the same raw-body requirement for its own webhook protocol, although its headers and event formats differ. <a href="https://docs.stripe.com/webhooks">Stripe webhook documentation</a>.</p>
<p>The timestamp window also has a narrow job. It limits how old a signed delivery can be when received. It does not make an event unique. A repeated delivery inside the accepted window still needs persistent duplicate handling. The existing <a href="https://fone.cc/insights/idempotent-payment-webhooks-digital-goods/">idempotency walkthrough</a> covers that part.</p>
<h2>A signed body can still fail the payment checks</h2>
<p>In the merchant example, <code>verifyNotice</code> first calls the SDK verifier. It then validates the result against the webhook schema. For a <code>payment.paid</code> event, it requires a paid order and finalized payment evidence. It compares the payment's identity, chain, gateway, recipient, token, amount, fee and net amount with the corresponding order fields.</p>
<p>These checks answer different questions. A matching signature establishes that the bytes were signed with the shared secret. Schema validation establishes that the receiver understands the payload. Comparing the payment evidence with the order catches an internally inconsistent event.</p>
<p>The next comparison uses information that came from the merchant's own saved purchase. StableIn's example checks the local order reference, platform order ID when already bound, chain, gateway, token and raw integer amount. It also requires the expected confirmation policy. An amount that agrees with another field in the notification can still disagree with the price saved by the store.</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Example of a rejection</th>
<th>Merchant consequence</th>
</tr>
</thead>
<tbody><tr>
<td>Raw-body signature</td>
<td>Body changed after signing</td>
<td>Stop before processing the payload</td>
</tr>
<tr>
<td>Timestamp and event header</td>
<td>Delivery outside the accepted window or mismatched event ID</td>
<td>Reject the delivery</td>
</tr>
<tr>
<td>Event schema</td>
<td>Amount has an unexpected data type</td>
<td>Do not interpret the event as a valid order update</td>
</tr>
<tr>
<td>Payment evidence</td>
<td>Payment amount differs from the event's order amount</td>
<td>Reject the inconsistent payment claim</td>
</tr>
<tr>
<td>Local order binding</td>
<td>Event refers to a different token or purchase</td>
<td>Keep the local purchase unchanged</td>
</tr>
<tr>
<td>Current eligibility</td>
<td>Current order is fully refunded</td>
<td>Do not create a new entitlement</td>
</tr>
</tbody></table>
<img src="https://fone.cc/wp-content/uploads/2026/10/payment-webhook-signature-fulfillment-checks-diagram.webp" alt="Payment webhook checks from original request bytes through event validation, local order matching and current fulfillment eligibility" style="display:block;margin:0 auto" />

<p><em>The checks follow StableIn's merchant example. The diagram separates notification acceptance from granting an entitlement.</em></p>
<h2>Read the order before scheduling its delivery</h2>
<p>A historical event describes a change that happened. The order API can provide a later view of the purchase. StableIn's receiver fetches that view, compares it with the local purchase and rejects an API snapshot older than the event it is processing. Its local projection also refuses to move backwards to an older revision.</p>
<p>The fulfillment predicate is small enough to inspect directly. This is the rule from the merchant example, reformatted for reading. It assumes the preceding schema and order checks have succeeded.</p>
<pre><code class="language-js">function canFulfill(order, localRevision) {
  return order.paymentStatus === 'paid'
    &amp;&amp; order.refundStatus !== 'full'
    &amp;&amp; BigInt(order.refundedAmount) &lt; BigInt(order.amount)
    &amp;&amp; BigInt(order.revision) &gt;= BigInt(localRevision);
}
</code></pre>
<p>The receiver records the event and eligible pending work in a database transaction. A separate worker rechecks the order before granting the local entitlement. This gives a delayed notification another opportunity to encounter the newer refund state.</p>
<p>An API outage adds a tradeoff. Fetching current state adds latency and a dependency to the receiver. In this example, failure to obtain the required state prevents that transaction from completing. The sender needs a working retry path. A different architecture could durably accept a verified event first and reconcile it asynchronously, but it would need its own recovery rules. That alternative is not what this merchant example currently implements.</p>
<h2>Put the difficult cases into the acceptance test</h2>
<p>A useful demo should include a correctly signed event that the merchant refuses to use. Ask for a mismatched local amount, valid JSON with an invalid schema, and a paid event whose current order is fully refunded. Keep duplicate delivery as a separate case so the team can show which constraint prevents a second entitlement.</p>
<p><a href="https://fone.cc/wp-content/uploads/2026/10/payment-webhook-signature-fulfillment-checks-kit-v1.zip">Download the acceptance worksheet and small signature exercise</a>. The exercise uses synthetic data and Python's standard library. It demonstrates exact-byte signing and selected order checks. It does not implement StableIn's complete schema, HTTP receiver, durable deduplication or payment finality. Its test report accompanies the download. The four existing merchant rule tests were also rerun on 28 October 2026; the full chain and database suite was not rerun for this article.</p>
<p>For an integration review, <a href="https://fone.cc/products/stablein-payments/">StableIn Payments</a> provides the checkout and merchant workflow behind these examples. Bring your current order format and the action that grants access. Those determine where the checks belong and which failure cases the delivery needs to pass.</p>
<p><a href="https://www.youtube.com/watch?v=FxCNO_7iB4A">Related product demonstration: signed webhooks and order fulfillment</a>.</p>
<p><em>This cross-post was prepared with AI assistance. The original wording and code are retained.</em></p>
]]></content:encoded></item><item><title><![CDATA[Test Bridge Indexer Evidence Before Calling a Deposit Final]]></title><description><![CDATA[Run an offline Python bridge indexer lab. Reject conflicting ancestors and mismatched receipt logs with 14 tests and a downloadable source ZIP.]]></description><link>https://0xfone.hashnode.dev/test-bridge-indexer-evidence-before-calling-a-deposit-final</link><guid isPermaLink="true">https://0xfone.hashnode.dev/test-bridge-indexer-evidence-before-calling-a-deposit-final</guid><category><![CDATA[Python]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Blockchain]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[HaoJie Luo (Fone)]]></dc:creator><pubDate>Tue, 10 Nov 2026 13:45:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae5e34a547ddc5d64387372/e4859411-9f37-44c2-99d3-e9e68507ada2.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://fone.cc/insights/python-bridge-indexer-finality-receipt-lab/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=hashnode_br_l01_20261109"><em>Fone</em></a><em>.</em></p>
<p>Two RPC responses can return the same block at height 2 while one provider's finalized head points to a different parent. A bridge indexer that compares only the height and hash of that numeric block would miss the contradiction. The downloadable Python lab reproduces this case with four synthetic blocks, then rejects it by walking back from each tagged head.</p>
<p>You will also test whether a deposit log actually appears in its transaction receipt. The result is a small, runnable evidence checker with separate outcomes for missing data and contradictory data. It uses Python's standard library and runs entirely offline. Every block, address and transaction identifier is a teaching fixture.</p>
<p>I checked Passage Bridge's scanner and local relayer source when preparing this example. The scanner follows parent identities from tagged heads, checks receipts against logs, and commits indexed facts with their projections in a database transaction. Those are useful engineering choices to examine. This independent Python program demonstrates a subset of those checks; it does not validate a live chain or authorize a payout.</p>
<p><a href="https://fone.cc/wp-content/uploads/2026/10/python-bridge-indexer-finality-receipt-lab-v1.zip">Download the complete bridge indexer lab</a>. It includes the implementation, 14 tests and instructions.</p>
<h2>Start with the disagreement you want to catch</h2>
<p>An RPC response for a finalized tag supplies an anchor for subsequent checks. If providers report different finalized heights, choose the lower height and follow each provider's own parent links back to it. The comparison now concerns blocks connected to the reported heads.</p>
<p>The lab deliberately supplies symbolic hashes such as <code>block-2</code>. Production block hashes have different encoding and verification requirements. Here the strings make the failing relationship easy to read.</p>
<pre><code class="language-python">from dataclasses import replace
from finality import fixture, agreed_anchor, Conflict

chain, log, receipt = fixture()
fork = dict(chain)
fork[3] = replace(chain[3], parent="other-block-2")

try:
    agreed_anchor(fork, fork[3], chain, chain[2])
except Conflict as error:
    print(error)  # tagged branch changed
</code></pre>
<p>Both dictionaries still contain the identical object at numeric height 2. The primary head says its parent is <code>other-block-2</code>, so the walk rejects the supplied numeric block. This is the exact distinction that an equal-height comparison would lose. The test <code>test_agreeing_numeric_blocks_cannot_hide_wrong_parent</code> makes the regression reproducible.</p>
<p>Passage's inspected scanner uses this parent-walk approach when comparing tagged RPC heads. It also bounds the walk. Its local confirmation mode follows a separate policy, so a successful local run should never be described as evidence that the production finality policy has passed.</p>
<h2>Run the archive in a clean directory</h2>
<p>Download and extract the ZIP into an empty directory. No package installation, wallet or RPC credentials are required. The code targets Python 3.10 or newer; the execution log supplied with this draft records the interpreter actually used.</p>
<pre><code class="language-sh">python3 -m unittest -v
python3 finality.py
</code></pre>
<p>The first command runs 14 tests. The second accepts a receipt-backed synthetic event at block 1 under an agreed anchor at height 2, then prints a rejection for the deliberately broken parent link. The tests were executed after extracting the final ZIP into a fresh temporary directory.</p>
<p>This separation makes the download reviewable. A buyer can run the same artifact a developer receives, and a developer can change a fixture without setting up the product stack. The complete product's historical test counts are outside this lab's result.</p>
<h2>Bound the walk and preserve the reason for waiting</h2>
<p><code>ancestor</code> starts with the supplied head hash, loads the block at that height, and checks the expected identity. It then carries the parent hash into the next iteration. The downloadable implementation uses this loop.</p>
<pre><code class="language-python">expected = head.hash
for number in range(head.number, height - 1, -1):
    block = blocks.get(number)
    if block is None:
        raise Unavailable("missing ancestor")
    if block.number != number or block.hash != expected:
        raise Conflict("tagged branch changed")
    if number == height:
        return block
    expected = block.parent
</code></pre>
<p>The caller sets <code>max_steps</code>. A distance outside that bound raises <code>Unavailable</code>, as does a missing block. A mismatched parent identity raises <code>Conflict</code>. Neither outcome accepts the event, but the distinction gives an operations workflow useful information. Missing data can justify fetching a wider batch or retrying later. Conflicting identities require investigation of the evidence already supplied.</p>
<p>The lab does not schedule retries. It exposes those reasons to the caller so that retry policy can be tested separately. A large walk bound also has a cost in a real RPC adapter. It increases the number of block requests and the time during which responses can change. The product scanner includes an additional reread of fetched block identities before committing; this offline model assumes its dictionaries remain fixed during a call.</p>
<img src="https://fone.cc/wp-content/uploads/2026/10/python-bridge-indexer-finality-receipt-lab-diagram.webp" alt="Two tagged heads are traced to a common ancestor before a receipt-backed event is accepted" style="display:block;margin:0 auto" />

<h2>Check the event against the receipt</h2>
<p>Agreement about a block does not show that the claimed deposit log occurred in it. The next check needs the successful receipt, its transaction identity and its actual log entries. In the lab, the full frozen <code>Log</code> value must appear in <code>receipt.logs</code>.</p>
<pre><code class="language-python">result = accept_log(
    chain, chain[3], chain, chain[2],
    log, receipt, expected_address="pool-demo",
)
assert result["decision"] == "accepted"
assert result["block_hash"] == "block-1"
</code></pre>
<p>The function checks the expected contract address and rejects removed logs. It compares the receipt's block number, block hash and transaction hash with the event evidence, and requires a success status. Changing only the receipt log's data, topic or index makes the test fail. A transaction receipt with the right hash but an unrelated event therefore cannot satisfy this checker.</p>
<p>In Passage's source, <code>receiptMatches</code> performs corresponding identity and log-content comparisons. The scanner also rejects logs from the wrong target or block, deduplicates them by block hash, transaction hash and log index, and orders the retained events before projection. The teaching program stops at one accepted fact. It leaves deduplication, database commits and projection recovery to the integration that follows.</p>
<h2>Turn the test into a delivery discussion</h2>
<p>For a wallet or multichain application buyer, the practical question is how an unresolved source fact reaches the user interface and operations team. An accepted fact here means that the supplied observations passed these checks. It carries no approval to spend destination inventory. Passage's inspected local relayer separately requires finalized source and approval states, indexed data, and an unclaimed execution state before continuing with its policy checks.</p>
<p>Provider agreement still depends on the providers and the chain policy. Two providers can share infrastructure or return the same incorrect information. This lab contains no light client, consensus verification, cryptographic block validation or independent deposit proof. Its exact string comparisons also assume inputs have already been normalized by an adapter.</p>
<p>The <a href="https://fone.cc/products/passage-bridge/integration/">Passage Bridge integration scope</a> is the appropriate place to agree on target networks and evidence policy. The current product uses independent operational approval and destination inventory. Its relayer signing path rejects non-local execution; production signing requires additional implementation and acceptance.</p>
<p>Ask for a demonstration where one provider is missing data and another where the two branches conflict. Then inspect the recorded reason and confirm that neither case advances to payout. The <a href="https://fone.cc/insights/cross-chain-message-execution-context/">earlier message identity article</a> covers how an approved message binds its transfer fields. This lab gives that discussion an earlier checkpoint with runnable failures and inspectable evidence.</p>
]]></content:encoded></item><item><title><![CDATA[Your Order API Needs More Than an Idempotency Key]]></title><description><![CDATA[Build a Python and SQLite order API. Reproduce lost responses and concurrent retries, then test where idempotency keys prevent duplicate orders.]]></description><link>https://0xfone.hashnode.dev/your-order-api-needs-more-than-an-idempotency-key</link><guid isPermaLink="true">https://0xfone.hashnode.dev/your-order-api-needs-more-than-an-idempotency-key</guid><category><![CDATA[Python]]></category><category><![CDATA[SQLite]]></category><category><![CDATA[api]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[HaoJie Luo (Fone)]]></dc:creator><pubDate>Sat, 31 Oct 2026 12:54:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae5e34a547ddc5d64387372/2301c9d4-0345-423a-9039-d22668a8f359.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An order API can accept an <code>Idempotency-Key</code> header on every request and still create a second order for the same intended purchase. Generate a fresh key after a timeout, and a server that deduplicates by key has no way to connect the retry to the first attempt.</p>
<p>There is a separate server-side problem too. A lookup followed by an insert leaves a race if competing requests can both observe that the key is absent. The key needs an atomic relationship with the saved order.</p>
<p>That is the argument worth testing. How much does your retry guarantee actually cover?</p>
<p>This Python and SQLite lab commits an order, deliberately drops the response, and lets you retry it. You can inspect the database instead of guessing whether the request succeeded. It creates local order records only and never charges a wallet.</p>
<h2>Download and start the API</h2>
<p>Use Python 3.10 or later with <code>sqlite3</code>, and curl. The package has no third-party dependencies. The commands below use a macOS or Linux shell; on Windows, run the curl commands in WSL. The recorded checks used Python 3.13.3 on macOS.</p>
<p><a href="https://fone.cc/wp-content/uploads/2026/10/python-sqlite-idempotent-order-api-lab-v1.zip">Download the complete checkout retry lab</a>, extract it and open a terminal inside the extracted directory. You should see <code>checkout.py</code>, <code>test_checkout.py</code> and <code>README.txt</code>.</p>
<p>The same files are available in the <a href="https://github.com/imminer668/idempotent-order-api-lab">GitHub repository</a>. Clone it if you prefer working from Git, then run the commands below from the repository directory.</p>
<pre><code class="language-sh">python3 -B -m unittest -v
python3 checkout.py --db checkout-demo.sqlite --port 8765
</code></pre>
<p>The test suite creates its own temporary database and chooses a free local port. All seven included tests passed in the October 31, 2026 verification on Python 3.13.3. The second command leaves a server running on <code>127.0.0.1:8765</code>. Keep that terminal open and use another terminal for the requests below. If port 8765 is occupied, choose another port and change the request URLs to match.</p>
<p>This is a local learning server. Python's documentation does not recommend <code>http.server</code> for production. The example also has no customer authentication or merchant isolation. <a href="https://docs.python.org/3/library/http.server.html">Python HTTP server documentation</a>.</p>
<h2>Create one order, then repeat the request</h2>
<p>The browser is allowed to choose a known SKU. The server's catalog owns the amount. In this example, <code>field-kit</code> costs the synthetic raw amount <code>100000000</code>.</p>
<pre><code class="language-sh">curl -i http://127.0.0.1:8765/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: reader-attempt-0001' \
  -d '{"sku":"field-kit"}'
</code></pre>
<p>The first response has status <code>201</code>. Its JSON contains a generated order <code>id</code>, the SKU, the saved amount and <code>status</code> set to <code>open</code>. Run the same command again. You should receive <code>200</code> with the same JSON body and the same order ID.</p>
<p>The different status codes are deliberate in this teaching API. <code>201</code> means this request created the record; <code>200</code> means it recovered an existing one. Other APIs have different replay contracts. Stripe, for example, documents response retention and parameter comparison in its own idempotency interface. Match the contract of the system you integrate. <a href="https://docs.stripe.com/api/idempotent_requests">Stripe idempotent requests</a>.</p>
<h2>Put the key and the order in the same transaction</h2>
<p>This is the table created by the downloaded server.</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS orders (
    id TEXT PRIMARY KEY,
    request_key TEXT NOT NULL UNIQUE,
    sku TEXT NOT NULL,
    amount TEXT NOT NULL
);
</code></pre>
<p>The unique constraint makes the key durable alongside the order. The function below, extracted from <code>checkout.py</code>, keeps one transaction around the lookup and insert. Run it through the supplied server, which provides the imports and initializes this table.</p>
<pre><code class="language-python">def create_order(db_path, key, body):
    if not re.fullmatch(r'[A-Za-z0-9_-]{8,128}', key or ''):
        return 400, {'error':'invalid_idempotency_key'}
    if not isinstance(body, dict) or set(body) != {'sku'} or body['sku'] not in CATALOG:
        return 400, {'error':'send_only_a_known_sku'}
    with closing(sqlite3.connect(db_path, timeout=10, isolation_level=None)) as db:
        db.execute('BEGIN IMMEDIATE')
        try:
            row = db.execute('SELECT id, sku, amount FROM orders WHERE request_key=?', (key,)).fetchone()
            if row:
                db.commit()
                if row[1] != body['sku']: return 409, {'error':'idempotency_conflict'}
                return 200, {'id':row[0], 'sku':row[1], 'amount':row[2], 'status':'open'}
            order_id, amount = str(uuid.uuid4()), CATALOG[body['sku']]
            db.execute('INSERT INTO orders VALUES (?,?,?,?)', (order_id,key,body['sku'],amount))
            db.commit()
            return 201, {'id':order_id, 'sku':body['sku'], 'amount':amount, 'status':'open'}
        except BaseException:
            db.rollback(); raise
</code></pre>
<p><code>BEGIN IMMEDIATE</code> acquires the SQLite write transaction before the lookup. A competing writer has to wait or fail, rather than independently reading an absent key and proceeding with its own insert. The unique constraint is an additional guard; the server returns success only after commit. SQLite allows one writer at a time, so this approach trades write concurrency for a small, inspectable implementation. See <a href="https://www.sqlite.org/lang_transaction.html">SQLite transaction behavior</a>.</p>
<p>If SQLite cannot obtain the lock within the configured timeout, the HTTP handler returns <code>503</code> with <code>database_busy_retry_same_key</code>. A caller should retry with backoff and keep the same purchase key. This example does not establish a production throughput limit.</p>
<p>There is a small but consequential choice in the replay branch. It returns the amount saved with the order. It does not calculate a new price from the catalog. A price change between attempts should not silently change the purchase already accepted under that key.</p>
<img src="https://fone.cc/wp-content/uploads/2026/10/python-sqlite-idempotent-order-api-diagram.webp" alt="A create-order request committing one database record, losing its response and returning the same order when the purchase key is retried" style="display:block;margin:0 auto" />

<p><em>The saved purchase key connects the retry to the order that already exists.</em></p>
<h2>Reproduce the uncertain result</h2>
<p>Now use a fresh key and the lab's fault-injection header.</p>
<pre><code class="language-sh">curl -i http://127.0.0.1:8765/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: reader-attempt-0002' \
  -H 'X-Demo-Drop-Response: yes' \
  -d '{"sku":"field-kit"}'
</code></pre>
<p>Curl should report an empty response, typically exit code 52. The handler committed the order and then shut down the connection before sending response bytes. This is a controlled failure in the lab, not evidence of a real network incident.</p>
<p>Retry without the fault header, keeping the same key.</p>
<pre><code class="language-sh">curl -i http://127.0.0.1:8765/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: reader-attempt-0002' \
  -d '{"sku":"field-kit"}'
</code></pre>
<p>The response should be <code>200</code>. The test suite checks that the disconnected call left one database row and the retry did not add another. If you repeat the fault-injected call after running the example before, the saved order will already exist; the database filename is intentionally persistent.</p>
<h2>Now change only the key</h2>
<p>After recovering <code>reader-attempt-0002</code>, send the same purchase under a new key.</p>
<pre><code class="language-sh">curl -i http://127.0.0.1:8765/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: reader-attempt-0003' \
  -d '{"sku":"field-kit"}'
</code></pre>
<p>On a fresh run, this returns <code>201</code> and a different order ID. Both requests have an idempotency header. The database still contains two separate purchases for those two keys.</p>
<p>Inspect the records from another terminal in the same directory. Stop the API with Ctrl+C when you are done.</p>
<pre><code class="language-sh">python3 - &lt;&lt;'CHECK'
import sqlite3
with sqlite3.connect('checkout-demo.sqlite') as db:
    for row in db.execute(
        "SELECT request_key, id, sku, amount FROM orders ORDER BY request_key"
    ):
        print(row)
CHECK
</code></pre>
<p>If you followed all the commands once with a fresh database, expect three rows in total, one for each of <code>0001</code>, <code>0002</code> and <code>0003</code>. Repeating the commands against that same database reuses those rows.</p>
<p>The client needs to keep a key for the lifetime of one purchase attempt, including uncertain outcomes. If your business needs to recognize the same purchase across tabs, devices or newly generated keys, it also needs a separate business identity and uniqueness rule. Two identical baskets can be two legitimate purchases, so deduplicating by basket contents alone would make a different mistake.</p>
<h2>Reject a changed purchase under the same key</h2>
<p>Reuse <code>reader-attempt-0001</code>, but ask for the other SKU.</p>
<pre><code class="language-sh">curl -i http://127.0.0.1:8765/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: reader-attempt-0001' \
  -d '{"sku":"field-notes"}'
</code></pre>
<p>Expect <code>409</code> and <code>idempotency_conflict</code>. The existing purchase remains unchanged. Sending an extra <code>amount</code> field instead returns <code>400</code>; the client cannot override the server price through this endpoint.</p>
<h2>Where this guarantee ends</h2>
<p>The test suite sends twelve HTTP requests from six client threads and checks that they produce one order ID. It also restarts the server against the same database. Those are useful checks of this local service, not a distributed exactly-once guarantee or a power-loss test.</p>
<p>For a payment checkout, the next boundary is the call to a payment provider or blockchain. A committed SQLite row cannot by itself prevent a second external charge. Define a stable identity for that external operation and a recovery path for an uncertain result before adding real payments.</p>
<p>This sample also lacks authentication and merchant isolation. In a multi-tenant API, derive the key's scope from the authenticated account and operation. Define which request fields must match and how long the key remains valid. Remove the demonstration disconnect header before adapting the pattern to a deployed service.</p>
<p>A practical review question is whether the integration can recover the original order after its response is lost. “We send an idempotency header” leaves that question unanswered.</p>
<p>For an API you maintain, would you return the original <code>201</code> on replay, or use <code>200</code> as this lab does? The important part is documenting the contract so the caller can handle a recovered purchase correctly.</p>
<p>Adapted from <a href="https://fone.cc/insights/python-sqlite-idempotent-order-api/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=hashnode_st_l01_20261031">my original Python and SQLite tutorial</a>. The runnable lab and illustrations are available there as well.</p>
<p><em>This article was prepared with an AI coding agent from my existing tutorial and code. The seven included tests were rerun, and additional checks confirmed that a new key creates a second order and that replay keeps the saved price. This is a local teaching example, not evidence of a production payment deployment.</em></p>
]]></content:encoded></item></channel></rss>