← Blog
October 30, 2026 · 13 min

x402 exact on Neo: a 60-second payment that stayed valid for 88 minutes

PR #3926 specifies the x402 exact scheme for Neo N3. We rebuilt every byte of its test vector, ran its transfer script against mainnet, and read 31 hours of chain history. The vectors hold. The clock does not: a "60 second" payment spent 88 minutes mineable on 2026-10-29.

The PR is a specification only. It adds one file, specs/schemes/exact/scheme_exact_neo.md, 828 lines, and no SDK code. It was opened on 2026-10-24 by erikzhang as x402-foundation/x402 #3926, head commit d38e9e7. When we read it on 2026-10-30 it was open, had no review, and its only comment was a Vercel bot asking for authorization. The description says a reference implementation will follow after the spec is approved.

Neo is a useful case because it is not an account-balance chain with an EIP-3009 style authorization, and it is not a UTXO chain. The payment is a whole signed transaction, and the transaction expires by block height. That second property is where this post spends its time.

How the scheme works

The payer signs a complete Neo transaction that holds exactly one NEP-17 transfer call. The payload is the NEP-21 ContractParametersContext object (status Accepted, created 2021-11-24), restricted to five fields: type, hash, data (the unsigned transaction, Base64), items (one signing entry per signer) and network.

There are two fee modes. In payer-paid mode the payer is the only signer and pays system and network fees. In sponsored mode the facilitator is signer 0 with WitnessScope.None, so its witness can pay fees but cannot authorize contract activity, and the payer is signer 1 with CalledByEntry. The client leaves the facilitator's entry empty. The facilitator fills it in, builds the witnesses and broadcasts.

The flow is upfront: settle, then the resource, then the response. There is no separate verify call. The facilitator runs one preflight with invokescript, requires HALT and exactly one Transfer notification from the asset, reserves (network, transactionHash) in a settlement cache, broadcasts, and returns success only after it reads a persisted HALT with the same notification.

The script is pinned byte for byte. Push the data argument (PUSHNULL or a PUSHDATA1 string), push the amount with the smallest integer opcode, push payTo, push the payer, PUSH4, PACK, PUSH15 (the numeric value of CallFlags.All), push "transfer", push the asset hash, SYSCALL System.Contract.Call, ASSERT. Any other encoding is rejected, even a semantically equal one.

The vectors reproduce

The appendix has a TestNet T5 fixture with a payer-paid and a sponsored transaction. We rebuilt it from the requirements alone, using Python and the cryptography package, with RIPEMD-160 from Node because Python's OpenSSL build here has it disabled.

The system fee also checks out, with one detail worth keeping. The fixture's SystemFee is 215,933. We ran the same canonical script as a read-only invokescript on mainnet with a funded payer and a None-scope sponsor in front. It consumed 215,926 gas with a null data argument and 215,933 with the fixture's 36-character transfer data. The 7-unit difference is the data push, and the spec's number is exactly the preflight number.

We also tried to break the "SystemFee must equal preflight gas" rule for sponsored mode, because zero margin looks fragile. We varied the recipient (existing account, brand-new account, the payer itself) and the amount (1,000,000, the payer's entire balance). Every run consumed 215,926. For a GAS transfer, gas does not depend on that state, so the zero-margin rule held in every case we tried. We did not test other NEP-17 assets, which run their own contract code.

The same script is already on mainnet

The scheme does not invent a transaction shape. It pins the one wallets already emit. We read every transaction in the 28,800 mainnet blocks from 14,449,151 to 14,477,950 (2026-10-29 01:45:58 UTC to 2026-10-30 09:07:30 UTC), 2,097 transactions in all, and tried to parse each script against the spec's template. Rebuilding each match with our canonical builder gave a byte-identical script every time.

This is good news for adoption. A Neo wallet that already builds a standard transfer produces most of a valid payment. It also means the sponsored layout is not hypothetical on this chain.

What a failed payment costs the sponsor

One of those 61 transactions faulted. It is 0x40c5f6299053f95d0c4eb7fd60b1a50f0c8ab6019f456043f03f38d32b18c045, in block 14,456,315, a GAS transfer in the canonical shape. Its exception is at instruction 90 (ASSERT): ASSERT failed, the same message we got by simulating a transfer for one unit more than the payer's balance. The payer did not have the funds when the block executed.

The block's OnPersist log shows a GAS Transfer from that sender to null for 253,690. That is its SystemFee (215,926) plus its NetworkFee (37,764), in full. A faulted transaction is still included and still pays everything it declared.

In payer-paid mode that is the payer's cost. In sponsored mode the facilitator is the sender. A payer who signs, then spends the balance elsewhere before the facilitator's transaction is mined, makes the facilitator pay for a transaction that transfers nothing. The spec limits the exposure per transaction with a local cap and a fresh preflight, and it requires the facilitator to hold the settlement cache record. It does not say how to price the race between preflight and inclusion. In the spec's own sponsored fixture a failure costs 215,933 + 73,868 = 289,801 units, or 0.00289801 GAS.

That is small on a normal day. It is not small when the preflight is old, which brings us to the clock.

The clock: 60 seconds is 20 blocks

Neo transactions expire by ValidUntilBlock, a block index. The spec converts the requirement's maxTimeoutSeconds into blocks:

baseTimeoutBlocks = min(maxvaliduntilblockincrement,
                        max(1, ceil(maxTimeoutSeconds * 1000 / msperblock)))

The inputs agree everywhere we asked. getversion on three mainnet nodes (a neo-go node at coz.io, neo-cli 3.10.2 at seed1.neo.org, a neo-go node at nspcc.ru) and one TestNet node all report msperblock 3000 and maxvaliduntilblockincrement 5760. The native Policy contract's getMillisecondsPerBlock and getMaxValidUntilBlockIncrement return the same two numbers on all four. So maxTimeoutSeconds: 60 becomes 20 blocks, and 5,760 blocks at 3 s is 4.8 hours.

The setting that differs between nodes is maxtransactionsperblock (200, 200, 512 and 5,000 on the four nodes). The spec does not use it, so it does not matter here.

The conversion assumes blocks arrive every 3 seconds. Over the full 28,800-block window the median interval is 3,161 ms, the 95th percentile 3,419 ms. The mean is 3,920 ms and the 99th percentile is 44,607 ms, and that gap between median and tail is the story.

2026-10-29: the chain slowed, then stopped

We sampled the timestamp of every 100th block across the window. 31 of the 287 100-block windows averaged more than 5 seconds per block, between about 04:58 and 14:15 UTC on 2026-10-29. We then pulled every header from block 14,452,800 to 14,456,700 (3,901 headers, 04:55:22 to 14:23:15 UTC).

The slow windows averaged between 9.2 and 10.0 seconds per block, about three times the nominal rate. In the 120 blocks around the halt, headers show a repeating pattern of gaps near 30.1 s and 48.8 s. Then, between block 14,456,314 (2026-10-29 12:20:11.143 UTC) and block 14,456,315 (13:45:28.391 UTC), nothing was produced for 5,117,248 ms, 85 minutes and 17 seconds. We read both headers from all three mainnet nodes. They returned identical timestamps and hashes.

Block 14,456,315 held 197 transactions, 14 of which faulted (7.1%). In the other 28,799 blocks of the window, 1,900 transactions produced 3 faults (0.16%). We are not claiming a cause for the stall or for the fault rate. The log shows what ran, not why consensus slowed. What the data does show is that a backlog of transactions signed before the halt all executed at once against state that was 85 minutes old.

Now apply the spec's rule to that stretch. For each starting block in the 3,901 headers, we measured the wall-clock time to the block 20 blocks later, which is the longest window a 60-second requirement permits.

A client that observed height 14,456,308 and signed with ValidUntilBlock 14,456,328, exactly as the client rule asks, produced a payment that could be mined anywhere in the next 88 minutes. Nothing in the payload says so. The 60 seconds exist only in PaymentRequirements.

This is not an exploit by itself. The payer signed exact terms, and the tx hash plus the duplicate cache stop double settlement. The danger is the shape of the timeout. A resource server that holds a payload and does not settle it can hand that same payload to the facilitator an hour later and still get a valid payment. A buyer that treats "no answer in 60 s" as "no charge" can be wrong by an hour. The spec does acknowledge uncertainty, since it defines settlement_pending with the transaction hash. It does not tell a buyer how long a signature stays dangerous.

The fixture disagrees with the rule

The fixture says height 18,762,412 and ValidUntilBlock 18,762,434. That is 22 blocks. The client rule, with msperblock 3000, caps it at 20 blocks, 18,762,432. The value 22 is exactly the facilitator's acceptance bound (baseTimeoutBlocks + 2), which the spec says is only a tolerance for a client that is two blocks ahead. So the example passes the facilitator check and fails the client check. We cannot see which msperblock the fixture was generated against, only that both public networks report 3000 today. One of the two numbers should change.

What the spec gets right

Most of the document is strictness, and most of it is the right kind. The facilitator must compare the script byte for byte. Every Base64 value must be canonical, with no URL-safe alphabet, no whitespace and no stray bits in the last character. The hash field is never trusted and is recomputed from data. The settlement response must carry the independently computed hash, not the payload's copy. Each fact that the JSON repeats (hash, network, the keys of items, the signatures in both parameters and signatures) must be cross-checked, and a conflict must not be resolved by picking one.

Two rules stand out because they close real gaps in other schemes. The sponsor's witness uses WitnessScope.None, so even a hostile recipient contract cannot spend the fee payer's authority, and the facilitator must reject any observed NEP-17 transfer from its own account. And the result is judged on the persisted application log, not on the preflight, which is why a broadcast followed by an unreadable log becomes settlement_pending with a hash rather than a success or a retry.

The weakness is the one we measured. All of the strictness is about what the transaction says. Almost none of it is about when it can run.

The header

The payload travels as Base64 JSON in PAYMENT-SIGNATURE. The spec's own sponsored example, wrapped in a v2 envelope, is 1,527 bytes of JSON and 2,036 bytes of header. That is small. Multisignature accounts grow it, because each verification script and each signature appears twice (script and parameters / signatures) and the keys appear in signatures too. We built synthetic payloads with random keys and signatures of the right lengths:

Those sizes cross 8 KiB, which was Node's default header limit before v13, and stay under the current 16 KiB default (we measured header limits across clients for the 402 side). The spec tells deployments to configure a sufficient but bounded limit. For single-signature accounts it is a non-issue. A 16-key council paying through a default-configured proxy is the case to test.

What it means for LLM4Agents

Neo is not on our rails today, and this scheme is a draft with no reference implementation, so nothing changes in our gateway. Three things carry over.

First, expiry in blocks is a different guarantee from expiry in seconds. Our EIP-3009 payments carry validBefore, a Unix timestamp, so the bound on a signature is wall-clock and survives a chain halt. On Neo the bound is a count of blocks, and on 2026-10-29 the count and the clock separated by a factor of 88. Any chain we add that expires by height needs its own wall-clock cap in the facilitator, not only a block conversion. Our audits of the Stellar verify and settle window and of settlement finality look at the same gap on other chains.

Second, sponsored mode puts a loss on the facilitator that EIP-3009 gas sponsorship does not. A faulted Neo transaction burns the declared fees in full. If we ever sponsor on a chain like this, the per-payer exposure and the preflight age become pricing inputs.

Third, the shape-pinning idea is good. A scheme that accepts exactly one byte encoding is easy to verify, and 61 real transactions in a day already match it. The Canton and BSV schemes show the opposite risk, where verifying means parsing a large structure.

Staying on the frontier

Concrete steps, in order.

One. Add a wall-clock check for every height-expiring chain before we list it. Record the block timestamp at the signer's height and refuse a payload whose expected time to ValidUntilBlock, using recent real intervals and not the protocol constant, exceeds the requirement. The data above is a ready-made test case.

Two. Comment on PR #3926 with the two reproducible issues: the 22 versus 20 block mismatch in the fixture, and the missing wall-clock bound in the timeout rule. Both are cheap to fix in a draft and expensive after SDKs ship.

Three. Keep the harness. The script builder, the hash and signature checks and the simulation probe are about 150 lines. They will check a reference implementation against the spec when one appears.

Four. Watch for the SDK. The description promises one after approval. Until there is code, we only track the PR state and head commit.

Five. Do not sponsor fees on any chain without a loss budget. Size it from the declared fees times the failure rate seen in practice, not from the preflight alone.

Pay per call, in stablecoins, over an OpenAI-compatible API

One 402, one signature, 345+ models. No subscription, no minimum.

Register your agent