Skip to content

Commit b606ae5

Browse files
AgentTanukiclaude
andcommitted
Canary hardening + honest first-party attribution (first mainnet settlement)
Post-settlement fixes for tx 0x1052fa51aa1412119581194acc1011c51786a59538f46bb5f9d593f1ad16d802 (0.01 USDC, Base mainnet, independently confirmed). Canary hardening (GPT engineer, CTO-verified): did:web receipt verification resolves the kid ONLY via the trusted configured origin's /.well-known/did.json (hostile-but-valid did:web signers refused; legacy did:key kids still resolvable; receipt payer must match settled payer); payment-state file 0600; idempotent recovery may observe zero-or-one new settlements on replay, first send still requires exactly one; evidence artifact updated with the real settlement + idempotent_replay flag. Attribution fix (root cause of the unverified_payer mislabel: the canary paid with NO first-party tagging): (1) canary now tags every request via X-Agent-Guild-First-Party — token from GUILD_FIRST_PARTY_TOKEN env or the gitignored live/secrets/first_party_token — and REFUSES --execute untagged unless --allow-untagged; evidence records first_party_tagged. (2) effective_payer_attribution(): read-time, config-aware attribution for /billing/revenue + funnel; a record STORED unverified_payer whose payer is in GUILD_X402_FIRST_PARTY_PAYERS (or first_party_payer=True) reads as verified_first_party_canary. Upgrades move TOWARD first-party only (can only weaken an externality claim); bound/attested records never relabeled; append-only stored labels untouched. Tests: attribution-honesty +2, canary +4; full suites green in JSON and SQLite store modes (905 passed / 9 skipped each), verified in a clean cloud env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PogUzTjFFU7P4Q9EoY1Nrm
1 parent 7b09548 commit b606ae5

6 files changed

Lines changed: 357 additions & 38 deletions

File tree

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,95 @@
11
{
22
"label": "first_party_mainnet_canary",
3-
"mode": "dry_run",
4-
"disclaimer": "First-party canary DRY RUN. Preconditions + signed offer verified; NO signature produced, NO funds moved. NOT a payment, NOT revenue, NOT external adoption.",
3+
"disclaimer": "First-party canary. A successful settlement is proof the machine-payment loop works end-to-end; it is NOT external adoption and NOT customer revenue.",
54
"target": "https://agent-guild-5d5r.onrender.com",
6-
"requested_resource": "https://agent-guild-5d5r.onrender.com/check?capability=code-review",
7-
"canonical_resource": "https://agent-guild-5d5r.onrender.com/check?capability=code-review&signed=false&ttl_seconds=3600",
5+
"production_sha": "",
6+
"resource": "https://agent-guild-5d5r.onrender.com/check?capability=code-review&signed=false&ttl_seconds=3600",
7+
"request_hash": null,
88
"network": "eip155:8453",
99
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
1010
"recipient": "0xaa4E3ba0Eb5f564cAb54dDC08f5BaAfb3D4cA8E5",
11-
"chain_id": 8453,
1211
"amount_atomic": "10000",
1312
"amount_usdc": 0.01,
1413
"lifetime_cap_usdc": 0.01,
15-
"signed_offer_verified": true,
1614
"revenue_before": {
17-
"transactions": 0,
18-
"revenue_usd": 0.0,
19-
"networks": [],
20-
"transaction_hashes": [],
21-
"note": "zero: no independently confirmed mainnet settlement exists (testnet/sandbox/unconfirmed activity is value-less and listed under testnet_settlement)"
15+
"transactions": 1,
16+
"revenue_usd": 0.01,
17+
"networks": [
18+
"eip155:8453"
19+
],
20+
"transaction_hashes": [
21+
"0x1052fa51aa1412119581194acc1011c51786a59538f46bb5f9d593f1ad16d802"
22+
],
23+
"attribution": {
24+
"verified_first_party_canary": {
25+
"transactions": 0,
26+
"revenue_usd": 0.0
27+
},
28+
"cryptographically_bound_machine_payer": {
29+
"transactions": 0,
30+
"revenue_usd": 0.0
31+
},
32+
"independently_attested_external_machine": {
33+
"transactions": 0,
34+
"revenue_usd": 0.0
35+
},
36+
"unverified_payer": {
37+
"transactions": 1,
38+
"revenue_usd": 0.01
39+
}
40+
},
41+
"cryptographically_bound_machine_revenue_usd": 0.0,
42+
"independently_attested_external_revenue_usd": 0.0,
43+
"note": "counts ONLY mainnet settlements independently CONFIRMED on-chain; `attribution` splits them into verified_first_party_canary (Guild identity, never external), cryptographically_bound_machine_payer (valid caller proof + exact (address, network) wallet binding \u2014 identity proven, ownership/externality UNPROVEN), independently_attested_external_machine (a SEPARATE allowlisted issuer attests externality) and unverified_payer (missing proof is UNKNOWN, never external). Unknown ownership is never called verified external."
2244
},
23-
"generation_note": "Produced against a LOCAL mainnet-shaped stack with a fake RPC (chainId 8453) \u2014 no real network call, no signing, no key. Illustrates the artifact shape --execute would emit with real settlement fields added.",
24-
"result": "DRY RUN CLEAN \u2014 refused to sign",
25-
"timestamp": "2026-07-15T03:53:31Z"
45+
"mode": "execute",
46+
"timestamp": "2026-07-21T07:30:25Z",
47+
"result": "settled_and_confirmed",
48+
"payer": "0x6904b3E1a5007C94b195d054CF2ca06B43a5C45c",
49+
"transaction": "0x1052fa51aa1412119581194acc1011c51786a59538f46bb5f9d593f1ad16d802",
50+
"block_number": "0x2ea6123",
51+
"payment_response": {
52+
"success": true,
53+
"network": "eip155:8453",
54+
"transaction": "0x1052fa51aa1412119581194acc1011c51786a59538f46bb5f9d593f1ad16d802",
55+
"payer": "0x6904b3E1a5007C94b195d054CF2ca06B43a5C45c"
56+
},
57+
"independent_confirmation": {
58+
"confirmed": true,
59+
"reason": "confirmed",
60+
"block_number": "0x2ea6123"
61+
},
62+
"revenue_after": {
63+
"transactions": 1,
64+
"revenue_usd": 0.01,
65+
"networks": [
66+
"eip155:8453"
67+
],
68+
"transaction_hashes": [
69+
"0x1052fa51aa1412119581194acc1011c51786a59538f46bb5f9d593f1ad16d802"
70+
],
71+
"attribution": {
72+
"verified_first_party_canary": {
73+
"transactions": 0,
74+
"revenue_usd": 0.0
75+
},
76+
"cryptographically_bound_machine_payer": {
77+
"transactions": 0,
78+
"revenue_usd": 0.0
79+
},
80+
"independently_attested_external_machine": {
81+
"transactions": 0,
82+
"revenue_usd": 0.0
83+
},
84+
"unverified_payer": {
85+
"transactions": 1,
86+
"revenue_usd": 0.01
87+
}
88+
},
89+
"cryptographically_bound_machine_revenue_usd": 0.0,
90+
"independently_attested_external_revenue_usd": 0.0,
91+
"note": "counts ONLY mainnet settlements independently CONFIRMED on-chain; `attribution` splits them into verified_first_party_canary (Guild identity, never external), cryptographically_bound_machine_payer (valid caller proof + exact (address, network) wallet binding \u2014 identity proven, ownership/externality UNPROVEN), independently_attested_external_machine (a SEPARATE allowlisted issuer attests externality) and unverified_payer (missing proof is UNKNOWN, never external). Unknown ownership is never called verified external."
92+
},
93+
"idempotent_replay": true,
94+
"result_sha256": "defeee4e141c49b42b38134da858ca734fb0af4057b899bcb549fcb74b6892a2"
2695
}

live/guild/app/payments.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,35 @@ def normalize_payer_attribution(label: Any) -> str:
996996
return _LEGACY_ATTRIBUTION.get(str(label), "unverified_payer")
997997

998998

999+
def effective_payer_attribution(record: dict) -> str:
1000+
"""Read-time attribution for a stored settlement record.
1001+
1002+
Normalizes the stored label, then applies EXACTLY ONE conservative
1003+
upgrade: a record stored as `unverified_payer` ("unknown, never
1004+
external") whose payer address is in the CONFIGURED Guild canary wallet
1005+
allowlist (GUILD_X402_FIRST_PARTY_PAYERS) — or whose token-gated
1006+
first_party_payer flag is True — is read as
1007+
`verified_first_party_canary`. Configuration supplies knowledge the
1008+
server lacked at settlement time (the 2026-07-21 canary settled before
1009+
its wallet was configured), and the upgrade only ever moves a record
1010+
TOWARD first-party, i.e. it can only WEAKEN an externality claim.
1011+
1012+
Never applied to bound/attested records, never downgrades, never
1013+
invents externality; append-only history stays untouched."""
1014+
import os as _os
1015+
cls = normalize_payer_attribution(record.get("payer_attribution"))
1016+
if cls != "unverified_payer":
1017+
return cls
1018+
if record.get("first_party_payer") is True:
1019+
return "verified_first_party_canary"
1020+
payer = str(record.get("payer") or "").lower()
1021+
fp_payers = {p.strip().lower() for p in (_os.environ.get(
1022+
"GUILD_X402_FIRST_PARTY_PAYERS") or "").split(",") if p.strip()}
1023+
if payer and payer in fp_payers:
1024+
return "verified_first_party_canary"
1025+
return cls
1026+
1027+
9991028
def classify_payer_attribution(store: Any, *, payer: str,
10001029
network: str = "",
10011030
caller_did: str = "",

live/guild/app/store.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2945,11 +2945,10 @@ def _class_of(e: dict[str, Any]) -> str:
29452945
mainnet["external"] += 1
29462946
else:
29472947
mainnet["unknown"] += 1
2948-
att = b.get("payer_attribution")
2949-
if att is None and (fp_flag is True
2950-
or (payer and payer in fp_payers)):
2951-
att = "verified_first_party_canary"
2952-
crypto_att[_payments.normalize_payer_attribution(att)] += 1
2948+
# read-time, config-aware (effective_): a configured canary
2949+
# wallet stored as unverified_payer reads as
2950+
# verified_first_party_canary — upgrades toward first-party only.
2951+
crypto_att[_payments.effective_payer_attribution(b)] += 1
29532952

29542953
endpoint_verified_now = sum(
29552954
1 for a in self.agents.values()
@@ -4696,8 +4695,10 @@ def _usd(rows: list) -> float:
46964695
att_classes: dict[str, list] = {
46974696
c: [] for c in _payments.ATTRIBUTION_CLASSES}
46984697
for b in x402_mainnet:
4699-
att_classes[_payments.normalize_payer_attribution(
4700-
b.get("payer_attribution"))].append(b)
4698+
# effective_: read-time, config-aware, upgrade-toward-first-party
4699+
# only (a configured canary wallet stored as unverified_payer
4700+
# reads as verified_first_party_canary; never the reverse).
4701+
att_classes[_payments.effective_payer_attribution(b)].append(b)
47014702
out["real_settlement"] = {
47024703
# real_settlement counts ALL independently confirmed money,
47034704
# regardless of attribution (money is money).

live/guild/tests/test_attribution_honesty.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,74 @@ def test_legacy_verified_external_records_reinterpreted_conservatively():
290290
assert att["cryptographically_bound_machine_payer"]["transactions"] >= 1
291291

292292

293+
def test_configured_canary_wallet_upgrades_unverified_at_read(monkeypatch):
294+
"""The 2026-07-21 defect: the first real mainnet canary settled WITHOUT
295+
first-party tagging and was stored `unverified_payer` (honest: unknown).
296+
Configuring that wallet in GUILD_X402_FIRST_PARTY_PAYERS re-reads the
297+
stored record as verified_first_party_canary — configuration supplies
298+
the knowledge missing at settlement time. The upgrade moves records
299+
TOWARD first-party only; it is read-time and reversible with the
300+
config, and the append-only stored label is untouched."""
301+
from app.main import app
302+
canary_wallet = "0x" + "77" * 20
303+
tx = "0x" + "cd" * 32
304+
store.billing_log.append({
305+
"key": "x402", "type": "x402_payment", "endpoint": "check",
306+
"network": MAINNET, "amount_atomic": "10000",
307+
"payer": canary_wallet, "transaction": tx,
308+
"status": "settled_confirmed", "mainnet": True, "confirmed": True,
309+
"payer_attribution": "unverified_payer",
310+
"first_party_payer": None, "at": "2026-07-21T05:00:00Z"})
311+
312+
def _read():
313+
with TestClient(app) as client:
314+
return client.get("/billing/revenue").json()["real_settlement"]
315+
316+
before = _read()["attribution"]
317+
assert before["verified_first_party_canary"]["transactions"] == 0
318+
assert before["unverified_payer"]["transactions"] >= 1
319+
320+
monkeypatch.setenv("GUILD_X402_FIRST_PARTY_PAYERS",
321+
canary_wallet.upper()) # case-insensitive
322+
after = _read()
323+
att = after["attribution"]
324+
assert att["verified_first_party_canary"]["transactions"] == 1
325+
assert att["verified_first_party_canary"]["revenue_usd"] == \
326+
pytest.approx(0.01)
327+
assert att["unverified_payer"]["transactions"] == \
328+
before["unverified_payer"]["transactions"] - 1
329+
# stored label untouched (append-only history)
330+
assert store.billing_log[-1]["payer_attribution"] == "unverified_payer"
331+
332+
333+
def test_effective_attribution_upgrades_toward_first_party_only(monkeypatch):
334+
"""effective_payer_attribution never relabels proven classes, never
335+
downgrades, and never moves any record toward external."""
336+
canary_wallet = "0x" + "77" * 20
337+
monkeypatch.setenv("GUILD_X402_FIRST_PARTY_PAYERS", canary_wallet)
338+
# a BOUND record for the configured wallet stays bound (proof wins)
339+
assert payments.effective_payer_attribution({
340+
"payer_attribution": "cryptographically_bound_machine_payer",
341+
"payer": canary_wallet}) == "cryptographically_bound_machine_payer"
342+
# token-gated flag upgrades a stored-unknown record
343+
assert payments.effective_payer_attribution({
344+
"payer_attribution": "unverified_payer", "payer": "0x" + "88" * 20,
345+
"first_party_payer": True}) == "verified_first_party_canary"
346+
# unknown wallet, no flag: stays unverified
347+
assert payments.effective_payer_attribution({
348+
"payer_attribution": None,
349+
"payer": "0x" + "99" * 20}) == "unverified_payer"
350+
# legacy retired label still reinterprets as bound, config or not
351+
assert payments.effective_payer_attribution({
352+
"payer_attribution": "verified_external_machine",
353+
"payer": canary_wallet}) == "cryptographically_bound_machine_payer"
354+
monkeypatch.delenv("GUILD_X402_FIRST_PARTY_PAYERS")
355+
# without config the same stored-unknown record reads unverified again
356+
assert payments.effective_payer_attribution({
357+
"payer_attribution": "unverified_payer",
358+
"payer": canary_wallet}) == "unverified_payer"
359+
360+
293361
# ---------------------------------------------------------------------------
294362
# the independent-attestation mechanism: real, but honestly zero by default
295363
# ---------------------------------------------------------------------------

live/guild/tests/test_first_party_canary.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
pay-before-state-is-impossible one-shot file.
88
"""
99
import importlib.util
10+
import base64
1011
import json
1112
import pathlib
13+
import stat
1214
import types
1315

1416
import pytest
@@ -308,6 +310,65 @@ def test_state_roundtrip_is_atomic(tmp_path):
308310
"signed_payload": "xyz"})
309311
assert canary._load_state(p)["status"] == "signed"
310312
assert canary._load_state(p)["signed_payload"] == "xyz"
313+
assert stat.S_IMODE(p.stat().st_mode) == 0o600
314+
315+
316+
def test_signed_receipt_resolves_trusted_did_web(monkeypatch):
317+
monkeypatch.setenv("GUILD_PUBLIC_HOST", "https://guild.example")
318+
from app.state import store
319+
identity = store.guild_identity()
320+
resource = _resource()
321+
payer = "0x" + "12" * 20
322+
tx = "0x" + "34" * 32
323+
payload = artifacts.receipt_payload(
324+
network="eip155:8453", resource_url=resource, payer=payer,
325+
transaction=tx, issued_at=1,
326+
)
327+
receipt = artifacts.signed_receipt(identity, payload)
328+
raw = {
329+
"extensions": {
330+
"offer-receipt": artifacts.offer_receipt_settle_extension(receipt)
331+
}
332+
}
333+
334+
class Response:
335+
headers = {
336+
x402.PAYMENT_RESPONSE_HEADER:
337+
base64.b64encode(json.dumps(raw).encode()).decode()
338+
}
339+
340+
assert canary._verify_settle_receipt(
341+
Response(), resource, payer, tx, http=_did_http(),
342+
)
343+
assert not canary._verify_settle_receipt(
344+
Response(), resource, "0x" + "99" * 20, tx, http=_did_http(),
345+
)
346+
347+
348+
def test_revenue_transition_allows_idempotent_recovery_only():
349+
tx = "0x" + "56" * 32
350+
before = {"transactions": 1, "transaction_hashes": [tx]}
351+
unchanged = {"transactions": 1, "transaction_hashes": [tx]}
352+
increased = {"transactions": 2, "transaction_hashes": [tx]}
353+
354+
canary._verify_revenue_transition(
355+
before, unchanged, tx, replayed_signed_payload=True,
356+
)
357+
canary._verify_revenue_transition(
358+
before, increased, tx, replayed_signed_payload=True,
359+
)
360+
canary._verify_revenue_transition(
361+
before, increased, tx, replayed_signed_payload=False,
362+
)
363+
with pytest.raises(canary.Refuse, match="exactly one"):
364+
canary._verify_revenue_transition(
365+
before, unchanged, tx, replayed_signed_payload=False,
366+
)
367+
with pytest.raises(canary.Refuse, match="not in real_settlement"):
368+
canary._verify_revenue_transition(
369+
before, {"transactions": 2, "transaction_hashes": []}, tx,
370+
replayed_signed_payload=False,
371+
)
311372

312373

313374
def test_discovery_falls_back_to_canonical_check():
@@ -324,3 +385,34 @@ def test_evidence_is_labelled_first_party_and_secretless(tmp_path):
324385
canary._write_evidence(p, ev)
325386
loaded = json.loads(p.read_text())
326387
assert loaded["label"] == "first_party_mainnet_canary"
388+
389+
390+
def test_execute_refuses_without_first_party_tagging(monkeypatch, tmp_path):
391+
"""--execute without GUILD_FIRST_PARTY_TOKEN would settle a payment the
392+
server can only classify unverified_payer (the 2026-07-21 mislabel) —
393+
refuse BEFORE any network traffic unless --allow-untagged."""
394+
monkeypatch.delenv("GUILD_FIRST_PARTY_TOKEN", raising=False)
395+
monkeypatch.setattr(canary, "DEFAULT_FP_TOKEN_FILE",
396+
tmp_path / "absent_token")
397+
args = types.SimpleNamespace(
398+
base="https://guild.example", execute=True, dry_run=False,
399+
allow_untagged=False, state=str(tmp_path / "state.json"),
400+
evidence=str(tmp_path / "ev.json"), key_file=None, expect_sha=None)
401+
with pytest.raises(canary.Refuse, match="first-party tagging"):
402+
canary.run(args)
403+
assert not (tmp_path / "state.json").exists()
404+
405+
406+
def test_first_party_headers_env_then_token_file(monkeypatch, tmp_path):
407+
monkeypatch.delenv("GUILD_FIRST_PARTY_TOKEN", raising=False)
408+
missing = tmp_path / "absent_token"
409+
monkeypatch.setattr(canary, "DEFAULT_FP_TOKEN_FILE", missing)
410+
assert canary._first_party_headers() == {}
411+
token_file = tmp_path / "first_party_token"
412+
token_file.write_text("file-secret\n")
413+
monkeypatch.setattr(canary, "DEFAULT_FP_TOKEN_FILE", token_file)
414+
assert canary._first_party_headers() == {
415+
"X-Agent-Guild-First-Party": "file-secret"}
416+
monkeypatch.setenv("GUILD_FIRST_PARTY_TOKEN", "env-secret") # env wins
417+
assert canary._first_party_headers() == {
418+
"X-Agent-Guild-First-Party": "env-secret"}

0 commit comments

Comments
 (0)