RTC payment verification can never succeed: settled transfers carry no status field
_verify_rtc_payment reads the recipient's transfer history and then gates on a status field:
status = str(matching_tx.get("status", "")).strip().lower()
if status not in _FINAL_RTC_STATUSES: # settled, confirmed, completed, finalized, success, paid
return {"verified": False, "error": f"unsettled_rtc_tx:{status or 'missing'}"}
A read-only request to https://rustchain.org/wallet/history?miner_id=stmr in this session returned four already received and settled transfers. Every one of them has exactly these keys:
amount, epoch, from, reason, timestamp, tx_hash, type
There is no status key on a settled transfer, so matching_tx.get("status", "") yields "", which is not in _FINAL_RTC_STATUSES, and every genuine payment is rejected with unsettled_rtc_tx:missing. The other checks (amount, sender match, replay, freshness) are all reachable only after this gate.
Running the module's own _verify_rtc_payment against that exact payload:
verification_with_live_payload: {"verified": false, "error": "unsettled_rtc_tx:missing"}
verification_with_status_supplied: {"verified": true, "tx_hash": "dde21...", "amount_rtc": 12.0}
The control differs only by adding the status field the API does not send, which confirms the status gate is the sole blocker. Net effect: the RTC payment path for x402-gated services rejects all settled payments, so those endpoints stay unpaid-access-only.
A second, related issue in the same gate: RTC_PAYMENT_MAX_AGE_SECONDS defaults to 3600, and the age is computed against the transfer timestamp, so even with the status problem fixed a payer who requests their service more than an hour after paying is rejected as stale_rtc_tx.
Suggested fix: treat the absence of status on a history row as settled when the row is present in the wallet's own history (that is what "it is in my history" means), or derive settlement from epoch/type the way the rest of the codebase does. Raise or re-base the freshness window so it does not silently reject slow follow-ups.
Reproduction
This executes the repository's real _verify_rtc_payment from main, feeding it the real history payload captured above. requests is stubbed so no network call is made, and RTC_PAYMENT_MAX_AGE_SECONDS=0 isolates the status gate from the age gate.
import ast
import copy
import json
import os
import sys
import types
from urllib.request import urlopen
URL = 'https://raw.githubusercontent.com/Scottcjn/beacon-skill/main/beacon_skill/x402_bridge.py'
source = urlopen(URL, timeout=40).read().decode()
tree = ast.parse(source)
# Real, already-settled transfers read from the public endpoint in this session.
LIVE_HISTORY = {
'miner_id': 'stmr',
'ok': True,
'total': 4,
'transactions': [
{'amount': 12, 'epoch': 32983, 'from': 'founder_team_bounty',
'reason': 'transfer_in:founder_team_bounty:dde21dc9c56167093b1adcc5b3d6b2b8',
'timestamp': 1784585686, 'tx_hash': 'dde21dc9c56167093b1adcc5b3d6b2b8', 'type': 'transfer_in'},
{'amount': 3, 'epoch': 27572, 'from': 'founder_community',
'reason': 'transfer_in:founder_community:5650b8f8b87f5dd2eac3e07a227171ca',
'timestamp': 1781343064, 'tx_hash': '5650b8f8b87f5dd2eac3e07a227171ca', 'type': 'transfer_in'},
],
}
class FakeResponse:
ok = True
def __init__(self, payload):
self._payload = payload
def json(self):
return self._payload
class FakeRequests:
"""Stands in for `requests` inside _verify_rtc_payment; no network is used."""
def __init__(self, payload):
self.payload = payload
def get(self, url, params=None, verify=None, timeout=None):
assert url.endswith('/wallet/history'), url
assert params == {'miner_id': 'stmr'}, params
assert verify is True and timeout == 10
return FakeResponse(self.payload)
os.environ['RTC_PAY_TO'] = 'stmr'
os.environ['RTC_PAYMENT_MAX_AGE_SECONDS'] = '0' # disable the age gate so the status gate is isolated
node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_verify_rtc_payment')
WANTED = {'PRICING', '_VERIFIED_RTC_TX_HASHES', '_FINAL_RTC_STATUSES', 'X402_VERSION',
'BASE_CHAIN_ID', 'USDC_BASE', 'RUSTCHAIN_API_BASE', 'PAY_TO_ADDRESS', 'X402_FACILITATOR'}
consts = [n for n in tree.body if isinstance(n, (ast.Assign, ast.AnnAssign))
and any(getattr(t, 'id', '') in WANTED
for t in (n.targets if isinstance(n, ast.Assign) else [n.target]))]
ns = {'os': os, 'json': json, 'time': __import__('time'), 'Dict': dict, 'Any': object}
exec(compile(ast.Module(body=consts + [node], type_ignores=[]), URL, 'exec'), ns)
verify = ns['_verify_rtc_payment']
tx = LIVE_HISTORY['transactions'][0]
header = json.dumps({'tx_hash': tx['tx_hash'], 'amount_rtc': float(tx['amount']),
'from_wallet': tx['from'], 'to_wallet': 'stmr'})
fake_requests = types.ModuleType('requests')
fake_requests.get = FakeRequests(copy.deepcopy(LIVE_HISTORY)).get
sys.modules['requests'] = fake_requests
result_live = verify(header, 'atlas_query')
# Control: identical transfer, with only the absent status field supplied.
control_history = copy.deepcopy(LIVE_HISTORY)
control_history['transactions'][0]['status'] = 'settled'
fake_requests.get = FakeRequests(control_history).get
result_control = verify(header, 'atlas_query')
print(json.dumps({
'transfer_used': {'tx_hash': tx['tx_hash'], 'amount': tx['amount'], 'from': tx['from'],
'keys_present': sorted(tx.keys())},
'verification_with_live_payload': result_live,
'verification_with_status_supplied': result_control,
}, indent=2))
Output:
{
"transfer_used": {
"tx_hash": "dde21dc9c56167093b1adcc5b3d6b2b8",
"amount": 12,
"from": "founder_team_bounty",
"keys_present": [
"amount",
"epoch",
"from",
"reason",
"timestamp",
"tx_hash",
"type"
]
},
"verification_with_live_payload": {
"verified": false,
"error": "unsettled_rtc_tx:missing"
},
"verification_with_status_supplied": {
"verified": true,
"tx_hash": "dde21dc9c56167093b1adcc5b3d6b2b8",
"amount_rtc": 12.0
}
}
Scope: this is a fail-closed rejection of legitimate payments. No payment was forged, replayed or bypassed, no endpoint was called, and no account or key was used.
Relationship to existing work
Issue #860 reported that the RTC fallback did not verify the actual transfer, and PR #861 added the history verification. This report does not repeat that: it concerns the verification introduced there rejecting genuine settled payments because of the status gate. If #861 already addresses it on a newer branch than the code I read, please close this as a duplicate.
Duplicate screening: beacon-skill "_verify_rtc_payment" returns only #860, beacon-skill "unsettled_rtc_tx" only PR #861, and beacon-skill "x402" "status" only PR #861.
AI assistance disclosure: source inspection, live read-only payload capture, local reproduction and report preparation were AI-assisted.
RTC payment verification can never succeed: settled transfers carry no
statusfield_verify_rtc_paymentreads the recipient's transfer history and then gates on a status field:A read-only request to
https://rustchain.org/wallet/history?miner_id=stmrin this session returned four already received and settled transfers. Every one of them has exactly these keys:There is no
statuskey on a settled transfer, somatching_tx.get("status", "")yields"", which is not in_FINAL_RTC_STATUSES, and every genuine payment is rejected withunsettled_rtc_tx:missing. The other checks (amount, sender match, replay, freshness) are all reachable only after this gate.Running the module's own
_verify_rtc_paymentagainst that exact payload:The control differs only by adding the
statusfield the API does not send, which confirms the status gate is the sole blocker. Net effect: the RTC payment path for x402-gated services rejects all settled payments, so those endpoints stay unpaid-access-only.A second, related issue in the same gate:
RTC_PAYMENT_MAX_AGE_SECONDSdefaults to 3600, and the age is computed against the transfer timestamp, so even with the status problem fixed a payer who requests their service more than an hour after paying is rejected asstale_rtc_tx.Suggested fix: treat the absence of
statuson a history row as settled when the row is present in the wallet's own history (that is what "it is in my history" means), or derive settlement fromepoch/typethe way the rest of the codebase does. Raise or re-base the freshness window so it does not silently reject slow follow-ups.Reproduction
This executes the repository's real
_verify_rtc_paymentfrommain, feeding it the real history payload captured above.requestsis stubbed so no network call is made, andRTC_PAYMENT_MAX_AGE_SECONDS=0isolates the status gate from the age gate.Output:
Scope: this is a fail-closed rejection of legitimate payments. No payment was forged, replayed or bypassed, no endpoint was called, and no account or key was used.
Relationship to existing work
Issue #860 reported that the RTC fallback did not verify the actual transfer, and PR #861 added the history verification. This report does not repeat that: it concerns the verification introduced there rejecting genuine settled payments because of the status gate. If #861 already addresses it on a newer branch than the code I read, please close this as a duplicate.
Duplicate screening:
beacon-skill "_verify_rtc_payment"returns only #860,beacon-skill "unsettled_rtc_tx"only PR #861, andbeacon-skill "x402" "status"only PR #861.AI assistance disclosure: source inspection, live read-only payload capture, local reproduction and report preparation were AI-assisted.