<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Spread</title>
    <description>The latest articles on DEV Community by Paul Spread (@spread2009).</description>
    <link>https://dev.to/spread2009</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F291783%2F9845631e-84be-4923-ac15-143423dbf9c7.png</url>
      <title>DEV Community: Paul Spread</title>
      <link>https://dev.to/spread2009</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spread2009"/>
    <language>en</language>
    <item>
      <title>Bots Visit Your Site Every Day. We Gave Them a Mailbox.</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Sat, 14 Nov 2026 22:16:59 +0000</pubDate>
      <link>https://dev.to/spread2009/bots-visit-your-site-every-day-we-gave-them-a-mailbox-32pl</link>
      <guid>https://dev.to/spread2009/bots-visit-your-site-every-day-we-gave-them-a-mailbox-32pl</guid>
      <description>&lt;h1&gt;
  
  
  Bots Visit Your Site Every Day. We Gave Them a Mailbox.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Bots and AI agents hit agentbadge.xyz daily - roughly &lt;strong&gt;7,000 requests in the last 24 hours&lt;/strong&gt;, and a third of the identified clients are machines, not people. Instead of filtering them out, we built an Agent Request Inbox: a free, keyless async channel where any agent can &lt;code&gt;POST /api/requests&lt;/code&gt; a question and get a &lt;code&gt;request_id&lt;/code&gt; to poll. Anonymous traffic is rate-limited through three fingerprint lanes (agent/fingerprint, IP, soft) - no CAPTCHA, no account, no email. Humans answer through an ops-pull console with a Telegram digest; a QA corpus auto-answers questions we have already resolved; agents can also fill surveys and earn reputation credit. Before publishing we ran three external adversarial audits: 15 findings, then REQUEST CHANGES with 6 more, then APPROVE - every fix shipped with regression tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fau6fc00p5ci53mdi4day.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fau6fc00p5ci53mdi4day.webp" alt="A glowing cyan mail slot labeled INBOX in an agentbadge.xyz wall; a queue of distinct robot silhouettes dropping letter cards - question mark, SURVEY clipboard, POLL stopwatch - into it" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your bot traffic is not a problem to block - it is a queue of questions you never heard.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually knocks on the door?
&lt;/h2&gt;

&lt;p&gt;Our own traffic digest (&lt;code&gt;GET /ops/stats-digest?window=24h&lt;/code&gt;, the same endpoint that drives the daily Telegram report) for the last 24h:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;What they want&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Other bots (uncategorized)&lt;/td&gt;
&lt;td&gt;~950&lt;/td&gt;
&lt;td&gt;everything - /blog, /feed, /api/venue/jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot + OpenAI crawlers&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;/blog (44), /blog.md (13), /services/scanner (9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoneyAgentPoller&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;td&gt;/api/venue/jobs - polls us on a timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;/blog (159) - an agent reading our content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;/blog (20), /blog.md (15), /services/scanner (8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;/blog (25)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime/monitoring&lt;/td&gt;
&lt;td&gt;~770&lt;/td&gt;
&lt;td&gt;/health - expected, not a user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And &lt;strong&gt;1,657 4xx/5xx errors&lt;/strong&gt; in the same window - every &lt;code&gt;404&lt;/code&gt; on &lt;code&gt;/api/jobs&lt;/code&gt; or &lt;code&gt;/env.js&lt;/code&gt; is a bot that wanted something we did not hand it. The pattern is clear: machines are already our second-largest audience, they show up uninvited, they read, they poll - and until this week they had &lt;strong&gt;no way to say why&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we gave them one. The inbox below is how we now hear their questions, run surveys on their tool stacks, and route the answered questions back to anyone who asks the same thing later - for free, keyless, no account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't a contact form work for an agent?
&lt;/h2&gt;

&lt;p&gt;Because every part of it assumes a human: a rendered HTML page, an email address for the reply, a CAPTCHA the agent cannot solve, and a session the agent does not keep. An agent that wants to ask "what does the free scan cover?" needs none of that - it needs a &lt;code&gt;POST&lt;/code&gt; that returns an ID it can poll later.&lt;/p&gt;

&lt;p&gt;So we built the agent-shaped version: submit takes milliseconds, the answer arrives when a human (or the QA corpus) gets to it. Mail, not chat.&lt;/p&gt;

&lt;p&gt;The cycle an agent runs is three HTTP calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Submit - free, no API key, idempotent&lt;/span&gt;
curl -X POST https://agentbadge.xyz/api/requests \
  -H &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Idempotency-Key: q-1742000000"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"kind":"question","payload":{"question":"What does the free scan cover?"}}'&lt;/span&gt;
&lt;span class="c"&gt;# → 202 {"request_id":"rq_9a7f…"}&lt;/span&gt;

&lt;span class="c"&gt;# 2. Poll by id (AAuth §11.8 pattern - the same shape as async auth challenges)&lt;/span&gt;
curl https://agentbadge.xyz/api/requests/rq_9a7f…
&lt;span class="c"&gt;# → 202 {"state":"submitted"} + Retry-After: 120&lt;/span&gt;
&lt;span class="c"&gt;#    …later… → 200 {"state":"completed","response":{…}}&lt;/span&gt;

&lt;span class="c"&gt;# 3. Or get an instant answer if your question is already in the QA corpus&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://agentbadge.xyz/api/requests/qa?q=what+does+the+scan+cover"&lt;/span&gt;
&lt;span class="c"&gt;# → 200 {"qa":true,"answer":"…"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1g1wtvptdhdx08z1xez.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1g1wtvptdhdx08z1xez.webp" alt="A conveyor loop: robot drops a POST /api/requests card into a slot that stamps it request_id; the card passes under a clock tower labeled GET /api/requests/:id showing 202 + Retry-After, then emerges as a 200 response envelope fed by an ops-pull lever" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Submit is cheap. The expensive part - a human thinking - happens off the request path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydxfk3zex7h5aieij5gm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydxfk3zex7h5aieij5gm.webp" alt="Sequence diagram: agent submits with Idempotency-Key; store dedupes by key+fingerprint (replay returns same request_id); QA corpus hit returns 200 instantly; otherwise the request queues, the agent polls on Retry-After until ops responds and the row completes; expired rows answer 410" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Replay the same submit and you get the same &lt;code&gt;request_id&lt;/code&gt; back - marked as a replay, never a second row.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you rate-limit someone with no account?
&lt;/h2&gt;

&lt;p&gt;Anonymous traffic crosses three sequential fingerprint lanes. Verified agents skip all of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard lane&lt;/strong&gt; - keyed on &lt;code&gt;agentId&lt;/code&gt; when the caller is verified, otherwise &lt;code&gt;fpHash = HMAC(ip + user-agent + headers, salt)&lt;/code&gt;. One new request per 2-hour window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP lane&lt;/strong&gt; - keyed on &lt;code&gt;HMAC(ip)&lt;/code&gt;, but it only exists for IPs that actually parse. An unresolvable IP skips the lane instead of sharing one giant bucket with every other mystery client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft lane&lt;/strong&gt; - a wider bucket on &lt;code&gt;HMAC(ua + headers)&lt;/code&gt;, default &lt;code&gt;max(submitMax * 8, 8)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two details we got wrong before an audit taught us better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lane refunds.&lt;/strong&gt; If the hard lane passes but the IP lane rejects, the hard slot is refunded - a rejection on a later lane never burns earlier slots. Same for store failures: the whole thing rolls back and the client gets a retry-safe &lt;code&gt;503&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header allowlist.&lt;/strong&gt; The first version snapshotted request headers with a blocklist - and an audit found unvetted proxy headers like &lt;code&gt;cf-pseudo-ipv4&lt;/code&gt; could persist spoofed IPs. Now only an explicit allowlist survives: &lt;code&gt;accept*&lt;/code&gt;, &lt;code&gt;sec-ch-ua*&lt;/code&gt;, &lt;code&gt;dnt&lt;/code&gt;, &lt;code&gt;content-type&lt;/code&gt;, &lt;code&gt;user-agent&lt;/code&gt;, &lt;code&gt;x-requested-with&lt;/code&gt;. Everything else is dropped before storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwk2hark9rf3byrqj5frn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwk2hark9rf3byrqj5frn.webp" alt="Three parallel rate-lane gates labeled HARD (agentId / fpHash), IP (ipFpHash, skipped for unknown IPs) and SOFT (softFpHash); a refund arrow loops from the IP-gate reject back to the HARD bucket" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rotating headers doesn't buy extra slots - the IP lane is still there, and verified agents don't need lanes at all.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do humans come in?
&lt;/h2&gt;

&lt;p&gt;Nowhere in the request path. Ops runs as a pull console, not a push inbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/ops/requests&lt;/code&gt; - the queue, behind an admin key, with sanitized fingerprints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/ops/requests/:id/respond&lt;/code&gt; - writes the answer; the row completes and the agent's next poll returns &lt;code&gt;200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/ops/requests/:id/state&lt;/code&gt; - &lt;code&gt;working | failed | completed&lt;/code&gt; transitions; terminal states are sticky.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/ops/requests/:id/spam&lt;/code&gt; - flag junk without a state change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/ops/surveys/:id/answers&lt;/code&gt; - survey responses with the question snapshot attached.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/ops/requests/digest&lt;/code&gt; - push the current queue snapshot to Telegram on demand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Telegram digest used to run hourly - we turned it into an on-demand call because a chat ping should be a decision, not noise. Every answered request also feeds the QA corpus, so the second agent asking the same question gets a &lt;code&gt;200&lt;/code&gt; instantly and never creates a row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy07f2suy4caiv9puqhi8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy07f2suy4caiv9puqhi8.webp" alt="Ops operator at a console marked PULL reading request cards rq_1, rq_2, rq_3; each answered card flows into a QA-corpus shelf labeled &amp;quot;answered once = instant 200 next time&amp;quot;" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Humans pull, agents poll - nobody babysits a websocket.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the privacy posture look like?
&lt;/h2&gt;

&lt;p&gt;The resolved client IP is stored in &lt;code&gt;fingerprint.ip&lt;/code&gt; - ops-only for abuse investigation, never returned by any public endpoint; the rate-limit lanes still key on &lt;code&gt;HMAC(ip, salt)&lt;/code&gt;. Header snapshots persist only the allowlist above. Payloads carry a TTL: the janitor archives the row, nulls the body, and the endpoint answers &lt;code&gt;410&lt;/code&gt; - no permanent content lake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxi7u3l5ro49uh5gpytg6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxi7u3l5ro49uh5gpytg6.webp" alt="Janitor figure sweeping expired request cards into an archive bin labeled 410; surviving fingerprint card shows only hash prefixes and an allowlisted header set" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did three adversarial audits actually catch?
&lt;/h2&gt;

&lt;p&gt;We ran the intake through external review before shipping, in three rounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Round one&lt;/strong&gt; - 15 findings across the whole slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-audit&lt;/strong&gt; - REQUEST CHANGES with 6 more: the header-snapshot blocklist (→ allowlist), lane slots kept after rejection (→ refunds), survey answers missing JSONB-unsafe checks, leaf-empty answer objects like &lt;code&gt;{q:[{a:""}]}&lt;/code&gt; passing validation, unresolvable IPs sharing one lane bucket, and survey store failures eating the refund.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final audit&lt;/strong&gt; - APPROVE WITH NITS; the nits are closed and every fix ships with a regression test. 98 request-inbox tests green.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ask us something - no key, no account&lt;/span&gt;
curl -X POST https://agentbadge.xyz/api/requests \
  -H &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Idempotency-Key: $(date +%s)-$(hostname)"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"kind":"question","payload":{"question":"What does the free scan cover?"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your bot traffic is not going away. The only question is whether those bots can tell you why they came.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Canonical post: &lt;a href="https://agentbadge.xyz/blog/arc-c49-agent-request-inbox" rel="noopener noreferrer"&gt;https://agentbadge.xyz/blog/arc-c49-agent-request-inbox&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Stripe Tells You the Money Arrived. On-Chain, Nobody Does — Building a USDC Verification Engine</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Fri, 13 Nov 2026 16:01:33 +0000</pubDate>
      <link>https://dev.to/spread2009/stripe-tells-you-the-money-arrived-on-chain-nobody-does-building-a-usdc-verification-engine-37mo</link>
      <guid>https://dev.to/spread2009/stripe-tells-you-the-money-arrived-on-chain-nobody-does-building-a-usdc-verification-engine-37mo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; When a buyer pays by card, Stripe's webhook says "paid". When a buyer pays in USDC, nothing arrives — so we built the engine that asks the blockchain itself. A viem adapter scans Arc for Transfer events, matches each one against our own attempt expectations (reference + exact amount + treasury recipient), confirms the payment through the same evidence seam Stripe uses, and fulfills — or files a PaymentException when it can't. An hourly reconciliation sweep diffs three sources of truth — expectations, chain transfers, receipts — into seven named mismatch kinds. Then we ran an adversarial audit on all of it: 2 Critical + 9 High, every finding re-verified against the source, all fixed with regression tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15lbf6ouv79pps33kszw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F15lbf6ouv79pps33kszw.webp" alt="A robotic arm handing a crossed-out webhook envelope while an AI agent queries a glowing blockchain ledger directly, verifying USDC entries against a checklist" width="800" height="423"&gt;&lt;/a&gt; &lt;em&gt;Ask the source, not the messenger.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't you just trust the transaction hash the buyer gives you?
&lt;/h2&gt;

&lt;p&gt;Because a tx hash is a claim, not a receipt. The buyer says "I sent 9.99 USDC, here's the hash" — but the hash could be on the wrong chain, to the wrong address, for a different amount, or a transfer someone else sent minutes ago. Nothing in a plain ERC-20 Transfer identifies &lt;em&gt;which order&lt;/em&gt; it pays for.&lt;/p&gt;

&lt;p&gt;So the engine verifies against &lt;strong&gt;expectations&lt;/strong&gt;, not events. When a buyer opens a &lt;code&gt;DIRECT_USDC&lt;/code&gt; attempt, we issue instructions that make the payment uniquely identifiable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;reference&lt;/strong&gt; derived from the payment ID,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;treasury recipient&lt;/strong&gt; and the &lt;strong&gt;USDC token contract&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;exact amount with a deterministic micro-unit suffix&lt;/strong&gt; — $9.99 becomes &lt;code&gt;9.990044&lt;/code&gt;. Ten buyers, ten distinct amounts, one recipient; matching becomes a lookup, not a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5stw0oo127xofj4u4840.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5stw0oo127xofj4u4840.webp" alt="Ten identical USDC coins stamped with different micro-amounts flowing toward a single treasury vault, a scanner beam routing each to its own payment slot" width="800" height="423"&gt;&lt;/a&gt; &lt;em&gt;Unique micro-amounts: same recipient, unambiguous payer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a live dogfood: a real attempt on &lt;code&gt;eip155:1874&lt;/code&gt; (Arc mainnet) — not a mock:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F615on2mjdiagokk1pqbj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F615on2mjdiagokk1pqbj.webp" alt="Live DIRECT_USDC payment instructions: amount 9.990044 USDC, recipient, Arc tokenContract, reference, one-hour TTL" width="800" height="440"&gt;&lt;/a&gt; &lt;em&gt;The instructions a buyer's wallet gets: exact amount, treasury, USDC contract, reference, 1h TTL.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How is the engine built?
&lt;/h2&gt;

&lt;p&gt;Chain code lives in the domain package — &lt;code&gt;@agentbadge/payments/src/chain/&lt;/code&gt; — as pure functions plus one injectable viem adapter. The server owns only wiring: an env-gated sweeper that polls on a timer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v3nbi8oxnogpgsnltvp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7v3nbi8oxnogpgsnltvp.webp" alt="Sequence diagram of the USDC payment flow: buyer opens a DIRECT_USDC attempt and receives instructions; the buyer transfers USDC on Arc; the monitor sweeper scans Transfer events on head+cursor, calls confirmPayment through the evidence seam, dispatchFulfillment returns a FulfillmentOutcome; needs-ops or unproven effects file a CRITICAL FULFILLMENT_UNRESOLVED exception, and late/duplicate/fuzzy mismatches file PaymentExceptions" width="800" height="326"&gt;&lt;/a&gt; &lt;em&gt;Every tick is a scan → match → confirm → fulfill-or-flag loop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three design choices worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same seam as Stripe.&lt;/strong&gt; &lt;code&gt;confirmPayment&lt;/code&gt; takes a &lt;code&gt;PaymentEvidence&lt;/code&gt; whether it came from a signed webhook or a chain scan — the domain service doesn't care which rail proved the money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expectations come from us, not the chain.&lt;/strong&gt; The monitor loads pending attempts (&lt;code&gt;listPayable&lt;/code&gt; + recently terminal ones for late-payment detection) and asks: does this transfer satisfy a stored expectation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy matching stays in its lane.&lt;/strong&gt; A transfer that matches no exact expectation can fuzzy-match &lt;em&gt;only within the same amount bucket&lt;/em&gt; — the audit caught candidates leaking across buckets, which is how an ops person mis-assigns real money.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did the adversarial audit break?
&lt;/h2&gt;

&lt;p&gt;We ran an external adversarial review of the engine before calling it done. Verdict: &lt;strong&gt;2 Critical + 9 High — all real, all fixed, all with regression tests.&lt;/strong&gt; The two that matter most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paid-but-never-delivered was a silent terminal state (C-1).&lt;/strong&gt; &lt;code&gt;dispatchFulfillment&lt;/code&gt; wrote &lt;code&gt;done&lt;/code&gt; after &lt;em&gt;any&lt;/em&gt; handler return — including a handler that bailed early without minting. CONFIRMED money, zero delivery, and nothing would ever revisit it. The fix makes fulfillment outcomes honest: &lt;code&gt;FulfillmentOutcome&lt;/code&gt; is a discriminated union (&lt;code&gt;executed | deduped-done | needs-ops | deduped&lt;/code&gt;), &lt;code&gt;done&lt;/code&gt; is written only for outcomes that &lt;em&gt;prove&lt;/em&gt; the effect ran, and &lt;code&gt;needs-ops&lt;/code&gt; files a CRITICAL &lt;code&gt;FULFILLMENT_UNRESOLVED&lt;/code&gt; exception plus a durable marker the reconciliation sweep re-checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The chain scan trusted every decoded log (C-2).&lt;/strong&gt; Hand-built &lt;code&gt;topics[]&lt;/code&gt; plus a lenient decoder meant a Transfer emitted by a &lt;em&gt;different contract&lt;/em&gt; — or to a &lt;em&gt;different recipient&lt;/em&gt; — could decode as "our" payment. Now the scan uses viem's &lt;code&gt;getLogs({event: Transfer, args: {to: treasury}})&lt;/code&gt; filter, and post-decode asserts &lt;code&gt;log.address === USDC&lt;/code&gt; and &lt;code&gt;decoded.to === treasury&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Highs are the checklist you'd want anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No chainId assert&lt;/strong&gt; — a misconfigured &lt;code&gt;ARC_RPC_URL&lt;/code&gt; would scan the wrong chain forever; &lt;code&gt;verifyChainId&lt;/code&gt; is now lazy-fetched, cached, and fatal on mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second tx on a consumed attempt&lt;/strong&gt; — previously ignored; now bound-hash + exact-match logic classifies it (&lt;code&gt;EXACT_TX_ON_UNCONFIRMED_ATTEMPT&lt;/code&gt;, &lt;code&gt;EXTRA_TX_ON_CONSUMED_ATTEMPT&lt;/code&gt;), and a different-hash confirmation dedupes to &lt;code&gt;DISPUTED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One malformed attempt killed the whole tick&lt;/strong&gt; — now per-attempt isolation: bad expectation → &lt;code&gt;errors[]&lt;/code&gt; entry, loop continues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-serve exposed unmonitored rails&lt;/strong&gt; — &lt;code&gt;PAYMENTS_USDC_SELF_SERVE&lt;/code&gt; alone would emit instructions for networks the monitor doesn't watch; it now hard-fails without &lt;code&gt;USDC_MONITOR_ENABLED&lt;/code&gt; + &lt;code&gt;ARC_RPC_URL&lt;/code&gt; + an arc-labeled network, and filters &lt;code&gt;usdcNetworks&lt;/code&gt; to Arc only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale-claim reclaim raced&lt;/strong&gt; — two workers could re-run the same effect; a deterministic &lt;code&gt;fulfill-reclaim-&amp;lt;pay&amp;gt;&lt;/code&gt; CAS marker fences concurrent reclaimers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjoa73higuvs2l8z54ntc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjoa73higuvs2l8z54ntc.webp" alt="Floating ops-queue cards, each a payment exception with a severity chip — INFO gray, WARN amber, CRITICAL red — one CRITICAL card glowing at the front labeled paid but never delivered" width="800" height="423"&gt;&lt;/a&gt; &lt;em&gt;Every anomaly has a name, a severity, and a deterministic ID.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you know it's still consistent an hour later?
&lt;/h2&gt;

&lt;p&gt;You don't — unless someone checks. Once an hour, an env-gated reconciliation sweep diffs &lt;strong&gt;three sources of truth&lt;/strong&gt;: attempt expectations, chain Transfer events, and stored receipts. Every disagreement has a name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9v82h3wj7icc4vzzthp0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9v82h3wj7icc4vzzthp0.webp" alt="Three glass ledger panels — attempt expectations, chain transfers, receipts — feeding a diff engine whose mismatched rows funnel into an exceptions queue" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fud98gk0km10e82gwsdse.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fud98gk0km10e82gwsdse.webp" alt="Flowchart of the reconciliation: three sources of truth — attempt expectations, chain Transfers, receipts — feed reconcileTransfers, which emits seven mismatch kinds into the exception queue, WARN for strays and CRITICAL for gaps and drift" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CHAIN_TX_WITHOUT_ATTEMPT&lt;/code&gt;, &lt;code&gt;CONFIRMED_WITHOUT_RECEIPT&lt;/code&gt;, &lt;code&gt;RECEIPT_WITHOUT_CHAIN_TX&lt;/code&gt;, &lt;code&gt;AMOUNT_DRIFT&lt;/code&gt;, &lt;code&gt;RECIPIENT_DRIFT&lt;/code&gt;, &lt;code&gt;CONFIRMED_WITHOUT_FULFILLMENT&lt;/code&gt;, plus the two consumed-attempt kinds from the audit. Stray inbound transfers warn; gaps and drift escalate to CRITICAL. Each exception gets a deterministic ID (&lt;code&gt;exc-recon-{kind}-{key}&lt;/code&gt;), so re-runs dedupe instead of spamming — and the cursor only advances after a fully successful pass, so a bad tick never silently skips blocks.&lt;/p&gt;

&lt;p&gt;Refunds are deliberately boring: an admin posts the refund txHash to &lt;code&gt;POST /api/v1/payments/:id/refund&lt;/code&gt;, the service opens a &lt;code&gt;REFUND_REQUEST&lt;/code&gt; exception, calls &lt;code&gt;markRefunded&lt;/code&gt; (strict guard: &lt;code&gt;CONFIRMED | DISPUTED&lt;/code&gt; → &lt;code&gt;REFUNDED&lt;/code&gt;, non-idempotent), and closes the exception. Humans move the money; the engine keeps the books.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's live and how do I try it?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST https://agentbadge.xyz/api/v1/payments/:id/attempts&lt;/code&gt; — &lt;code&gt;{ "provider": "DIRECT_USDC", "network": "eip155:1874" }&lt;/code&gt; returns instructions &lt;code&gt;{ amount, recipient, tokenContract, reference, expiresAt }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET https://agentbadge.xyz/api/v1/payments/:id&lt;/code&gt; — status + attempts; &lt;code&gt;/receipt&lt;/code&gt; once confirmed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET https://agentbadge.xyz/api/v1/payments/exceptions&lt;/code&gt; — the ops queue (admin auth).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST https://agentbadge.xyz/api/v1/payments/:id/refund&lt;/code&gt; — manual refund (admin auth).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rail is opt-in and gated by honesty, not by flag gymnastics: &lt;code&gt;PAYMENTS_USDC_SELF_SERVE&lt;/code&gt; won't emit instructions unless &lt;code&gt;USDC_MONITOR_ENABLED&lt;/code&gt; + &lt;code&gt;ARC_RPC_URL&lt;/code&gt; + an arc-labeled network are set — a server that can't watch the chain doesn't get to sell "pay us in USDC".&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the honest limits?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One chain.&lt;/strong&gt; Arc only — Base and Solana are deferred, and self-serve actively filters non-Arc networks out of instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-process sweeper.&lt;/strong&gt; Same pattern as our bounty-lease sweeper: no external infra, no queue workers. Deliberately small; the failure modes live in domain logic, not in plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One audit.&lt;/strong&gt; An adversarial review we paid attention to, not a proof. The regression suite encodes what it taught us: 183 payments-package tests + 73 server tests on the touched suites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual refunds.&lt;/strong&gt; Money movement is a human-with-txHash operation; automation is a separate, scarier epic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deeper principle is the one the audit kept proving: &lt;strong&gt;a chain payment isn't an event you receive, it's a claim you verify&lt;/strong&gt; — against expectations you wrote down before the money arrived, with a paper trail for everything that doesn't add up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: &lt;code&gt;@agentbadge/payments&lt;/code&gt; (pure TS domain) + viem on Arc (&lt;code&gt;eip155:1874&lt;/code&gt;), Bun + Hono server, deterministic-exception ops queue. All findings and fixes are documented in the internal review log; the regression tests are the artifact that outlives this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>payments</category>
      <category>blockchain</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Listed, Not Listed: Putting AgentBadge on Circle's Agent Services Marketplace — and Telling Machines the Truth</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Fri, 13 Nov 2026 12:34:51 +0000</pubDate>
      <link>https://dev.to/spread2009/listed-not-listed-putting-agentbadge-on-circles-agent-services-marketplace-and-telling-4dd</link>
      <guid>https://dev.to/spread2009/listed-not-listed-putting-agentbadge-on-circles-agent-services-marketplace-and-telling-4dd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; We submitted our Agent-Readiness Scan to Circle's Agent Services Marketplace - &lt;code&gt;POST /api/total-scan&lt;/code&gt;, x402-paid, USDC on Arc via EIP-3009 &lt;code&gt;transferWithAuthorization&lt;/code&gt;. Before the listing is approved, every machine-readable surface we own (llms.txt, skill.json, &lt;code&gt;/.well-known/agentbadge.json&lt;/code&gt;, Organization JSON-LD) already declares it - with the honest status &lt;code&gt;pending_review&lt;/code&gt;, not &lt;code&gt;listed&lt;/code&gt;. The submission took an afternoon; the discipline was deciding that our metadata tells machines what's true today, not what we hope is true tomorrow. And because a listing is worthless if the first sale is silent, we shipped the observability pack first: settle-failure alerts, a revenue ledger endpoint, and revenue in the daily ops digest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehybmsjlfzpulxleza3v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehybmsjlfzpulxleza3v.webp" alt="A glowing Agent-Readiness Scan service card riding a conveyor from a SUBMIT terminal toward a half-open REVIEW gate with an amber pending_review stamp; behind it a DISCOVERY API index wall with one empty pulsing slot" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One slot in the index is reserved for us - once review flips it from pending to live.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Agents don't browse your website. They query an index.
&lt;/h2&gt;

&lt;p&gt;A human finds a service on a marketplace page. An agent finds it in a machine-readable index - in Circle's case, &lt;code&gt;GET https://api.circle.com/v2/x402/discovery/resources&lt;/code&gt;. That endpoint is the actual storefront: 2,650 resources when we checked, each carrying its &lt;code&gt;accepts[]&lt;/code&gt; payment terms and provider metadata. The HTML catalog is a rendering of it, not the other way around.&lt;/p&gt;

&lt;p&gt;So the question "are we listed?" has a precise, checkable answer - and today it's no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s &lt;span class="s1"&gt;'https://api.circle.com/v2/x402/discovery/resources?query=total-scan'&lt;/span&gt; \
  &lt;span class="o"&gt;|&lt;/span&gt; jq &lt;span class="s1"&gt;'.items[].resource'&lt;/span&gt;
&lt;span class="c"&gt;# → empty while pending_review&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat we hit immediately: &lt;code&gt;?query=&lt;/code&gt; matches fuzzily - &lt;code&gt;agentbadge&lt;/code&gt; returned &lt;code&gt;anchor-x402&lt;/code&gt; on an "agent" substring. The reliable check is the resource URL or your payout wallet in &lt;code&gt;accepts[].payTo&lt;/code&gt;, not the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we submitted
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;AgentBadge Agent-Readiness Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST https://agentbadge.xyz/api/total-scan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment&lt;/td&gt;
&lt;td&gt;x402, USDC on Arc (&lt;code&gt;eip155:5042002&lt;/code&gt;), EIP-3009 &lt;code&gt;transferWithAuthorization&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flow&lt;/td&gt;
&lt;td&gt;agent hits endpoint → 402 with price → signs authorization → gets the scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No API keys, no accounts, no custodial anything. The buyer is an agent; the payment is a signed authorization the facilitator settles on-chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i7bp7ywo0ic8smuzthv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i7bp7ywo0ic8smuzthv.webp" alt="Sequence diagram of the listing lifecycle and the buyer flow: operator submits the service to the marketplace form; the Discovery API index stays empty while status is pending_review; once approved the item is indexed and a buyer agent can browse the catalog, hit POST /api/total-scan, get a 402 with price/payTo/network, retry with a PAYMENT-SIGNATURE carrying the EIP-3009 authorization, the facilitator settles USDC on Arc, and the endpoint returns 200 with the scan report" width="800" height="449"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The loop, end to end: form → review → index → agent → 402 → settle → report.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest-status decision
&lt;/h2&gt;

&lt;p&gt;Here's the part we almost got wrong by convention. Once we added the marketplace to our machine-readable surfaces, the natural status to write was &lt;code&gt;listed&lt;/code&gt;. It will be true eventually - probably. But today it isn't, and an LLM agent reading our llms.txt or skill.json has no way to know that "listed" means "we submitted a form."&lt;/p&gt;

&lt;p&gt;So a single registry - &lt;code&gt;marketplace-listings.ts&lt;/code&gt; - feeds all four surfaces, and the status is &lt;code&gt;pending_review&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/llms.txt&lt;/code&gt;&lt;/strong&gt; - a &lt;code&gt;Distribution&lt;/code&gt; section naming the marketplace, endpoint, and rail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/skill.json&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;ab:distribution&lt;/code&gt; with the full listing records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/.well-known/agentbadge.json&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;marketplace_listings&lt;/code&gt; in the deployment descriptor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organization JSON-LD&lt;/strong&gt; - &lt;code&gt;sameAs&lt;/code&gt; pointing at the catalog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybp18k5io3is3plmcgh8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybp18k5io3is3plmcgh8.webp" alt="One emerald ledger - marketplace-listings.ts - fanning out to four glowing screens: llms.txt Distribution section, skill.json ab:distribution, agentbadge.json marketplace_listings, and a JSON-LD sameAs link, each showing the same Circle Marketplace pending_review chip" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One registry, four surfaces - the status propagates, so "listed" flips everywhere at once.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When approval lands, one string changes - &lt;code&gt;pending_review&lt;/code&gt; → &lt;code&gt;listed&lt;/code&gt; - and every surface updates itself. The same mechanism protects us the other direction: if a listing gets delisted, we can't forget to update the JSON-LD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The principle:&lt;/strong&gt; don't claim listings you don't have. Services advertising marketplace presence that the discovery index doesn't return are teaching agents to distrust metadata - and the agent that trusts a stale claim fails at checkout, not at marketing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8onr1ycf26tsi5ho4f5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8onr1ycf26tsi5ho4f5.webp" alt="A terminal with a curl + jq query reading a JSON array beside a catalog shelf of 2650 glowing x402 resource tiles; a magnifier lights up the matching tile while an amber sign warns about fuzzy name matching" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The index is the ground truth. Verify by &lt;code&gt;resource&lt;/code&gt; and &lt;code&gt;payTo&lt;/code&gt; - never by name alone.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  We built the telemetry before the first sale
&lt;/h2&gt;

&lt;p&gt;A listing that goes live while your settlement errors are invisible is a listing you find out about from a support ticket. So before submission we shipped the observability pack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Settle-failure alerts&lt;/strong&gt; - every x402 settle failure fires to Telegram/Discord, deduped by error reason over 15 minutes. Verify failures stay log-only: a bad signature isn't a business event, a broken settle is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET /api/ledger/revenue?days=N&lt;/code&gt;&lt;/strong&gt; - ops rollup of paid legs: total, byDay, byEndpoint. A nonzero &lt;code&gt;total&lt;/code&gt; is the earliest possible "someone paid" signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue in the ops digest&lt;/strong&gt; - &lt;code&gt;x402Revenue&lt;/code&gt; block with top endpoints rides the daily summary we already send ourselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a subtle bug the tests caught: the econ-events schema was stripping the &lt;code&gt;endpoint&lt;/code&gt; field the settle legs carried, so revenue-by-endpoint silently never attributed anything. Fix went upstream - &lt;code&gt;@agentbadge/econ-events@0.1.4&lt;/code&gt; - because attribution that only works on new fields is attribution that lies about the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on approval
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Discovery API query returns our &lt;code&gt;items[]&lt;/code&gt; entry - &lt;code&gt;resource&lt;/code&gt;, &lt;code&gt;accepts[]&lt;/code&gt;, &lt;code&gt;metadata.provider&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pending_review&lt;/code&gt; → &lt;code&gt;listed&lt;/code&gt; in &lt;code&gt;marketplace-listings.ts&lt;/code&gt;; all four surfaces update.&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;accepts[].network&lt;/code&gt; = &lt;code&gt;eip155:5042002&lt;/code&gt; and &lt;code&gt;payTo&lt;/code&gt; = our wallet - the catalog rendering can lag the index.&lt;/li&gt;
&lt;li&gt;Screenshot the catalog card, swap this article's hero, publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If review stalls past a few days, Circle's escalation path is documented (&lt;code&gt;help.circle.com&lt;/code&gt; support form, or a ticket in the Build on Arc Discord). Manual review queues are a feature, not a bug - a marketplace that indexes without review is a spam list.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Status as of writing: submitted 2026-11-12, pending review. Check the index yourself - the command above is the same one we're watching.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>payments</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Checkout 2.0, Audited Before It Sold Anything: Commerce Catalog, Dual Rails, and the 20-Finding Fix</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Thu, 12 Nov 2026 17:49:36 +0000</pubDate>
      <link>https://dev.to/spread2009/checkout-20-audited-before-it-sold-anything-commerce-catalog-dual-rails-and-the-20-finding-fix-1lia</link>
      <guid>https://dev.to/spread2009/checkout-20-audited-before-it-sold-anything-commerce-catalog-dual-rails-and-the-20-finding-fix-1lia</guid>
      <description>&lt;p&gt;Every checkout we've audited — ours included — has the same scar tissue: a form posts to a legacy endpoint, a Stripe session appears, a webhook fires, and somewhere between "paid" and "fulfilled" a product falls out of a hat. The flow works until it doesn't, and when it doesn't there is no durable record of why.&lt;/p&gt;

&lt;p&gt;This EPIC rebuilt the whole path. A commerce catalog owns what we sell and what each product needs from the buyer. A payments API — sitting on the &lt;a href="https://agentbadge.xyz/blog/payment-infrastructure-foundation" rel="noopener noreferrer"&gt;EPIC-220 domain layer&lt;/a&gt; — turns an order into &lt;code&gt;Payment → PaymentAttempt → instructions → status → PaymentReceipt&lt;/code&gt;. A fulfillment registry maps product kind to handler, so "paid" and "delivered" are joined by a contract, not by a code path someone forgot to finish. Then we did the uncomfortable part: pointed an adversarial AI audit at the checkout before release. It came back &lt;strong&gt;BLOCK&lt;/strong&gt; — 3 Critical, 9 High, 8 Medium. All twenty findings are now root-cause fixed with regression tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfwzbh82cpbnr3ppcvjl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfwzbh82cpbnr3ppcvjl.webp" alt="The AgentBadge pricing page: four passport tiers — Bronze $9.99, Silver $49.99, Gold $199.99, Platinum $499.99 — each with a Card (Stripe) or USDC selector, copy reading &amp;quot;passports mint on Arc mainnet&amp;quot;" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the checkout actually is now
&lt;/h2&gt;

&lt;p&gt;The flow is catalog-driven, and the catalog is boring on purpose. Each product declares a USD price, a fulfillment kind, and a &lt;strong&gt;metadata schema&lt;/strong&gt; — the fields the fulfillment handler needs. For passport tiers: &lt;code&gt;accountId&lt;/code&gt; (the EVM wallet the passport mints to), &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;capabilities&lt;/code&gt;, and optional &lt;code&gt;endpoint&lt;/code&gt;/&lt;code&gt;skills&lt;/code&gt;/&lt;code&gt;imageUrl&lt;/code&gt;. Undeclared keys are rejected. Strings are length-bounded, arrays are item-bounded, and the validator runs &lt;em&gt;before&lt;/em&gt; any payment row exists — bad metadata can no longer buy a broken fulfillment.&lt;/p&gt;

&lt;p&gt;One API path serves both rails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/v1/payments          → { payment, deduped }
POST /api/v1/payments/:id/attempts
  { provider: "STRIPE" }       → redirectUrl → Stripe Checkout
  { provider: "DIRECT_USDC",
    network: "eip155:1874" }   → instructions: amount, recipient,
                                  tokenContract, reference, expiresAt
GET  /api/v1/payments/:id      → status + attempts
GET  /api/v1/payments/:id/receipt → receipt, once confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36ffii9hhlhqbyq12qaz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36ffii9hhlhqbyq12qaz.webp" alt="Sequence diagram of one API serving two rails: buyer picks a tier on /pricing and fills catalog-declared fields on /checkout; POST /api/v1/payments returns the payment with Idempotency-Key dedupe; the STRIPE attempt returns a redirectUrl to Stripe Checkout and the webhook confirms by binding session.id to the stored PaymentAttempt; the DIRECT_USDC attempt on eip155:1874 returns instructions — unique amount 9.990044, recipient, tokenContract, reference, expiresAt; both rails converge on confirm → dispatchFulfillment → mint passport on Arc, with the fulfillment-done marker written after the handler returns" width="800" height="614"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One endpoint, two rails — and both end in the same durable fulfillment path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The buyer picks a tier on &lt;code&gt;/pricing&lt;/code&gt;, lands on &lt;code&gt;/checkout&lt;/code&gt;, fills the fields the catalog declared, and chooses Card or USDC. Card produces a Stripe redirect; USDC produces instructions and a QR — Arc by default, Base and Solana selectable. HBAR stays where it belongs: a legacy rail for Hedera-era minters, a side note — not the storefront.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r6a8f5yza861v7fka9e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r6a8f5yza861v7fka9e.webp" alt="The pricing page storefront the buyer starts from: four tiers with USD as the primary price and HBAR noted only as a legacy Hedera rail" width="800" height="1321"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The storefront: USD primary, Card or USDC — HBAR kept only as the legacy footnote.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff25692odtatyqrcd3pok.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff25692odtatyqrcd3pok.webp" alt="The checkout form for a Bronze passport: catalog-declared fields — EVM wallet address (accountId), name, capabilities — with Card (Stripe) and USDC (Arc) payment method selector" width="800" height="757"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The form renders exactly what the catalog declared — wallet address, name, capabilities — plus the rail choice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjjc3na32tbh5jnkzi92.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjjc3na32tbh5jnkzi92.webp" alt="Live checkout instructions: QR code, amount 9.990044 USDC, network eip155:1874, recipient 0xcdd2…869d, token contract 0x036C…CF7e, reference pay_412f32ed…, expires timestamp — &amp;quot;Send USDC to complete payment&amp;quot;" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Live dogfood: a real &lt;code&gt;DIRECT_USDC&lt;/code&gt; attempt on &lt;code&gt;eip155:1874&lt;/code&gt; — Arc mainnet. Note the amount: &lt;code&gt;9.990044&lt;/code&gt;, not &lt;code&gt;9.99&lt;/code&gt;. More on that suffix below.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit that blocked it
&lt;/h2&gt;

&lt;p&gt;We ran Arc Studio's adversarial review on the implementation before shipping (SLICE-221-6). Verdict: &lt;strong&gt;BLOCK&lt;/strong&gt;. The findings were not stylistic:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;What it would have done&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fulfillment marked done before the handler ran&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;A handler that throws or times out still leaves the order "fulfilled" — paid, undelivered, invisible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-memory dedupe only&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Any restart or second instance re-fulfills the same payment — double mints, double entitlements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unescaped product code in the checkout 404&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Reflected XSS: &lt;code&gt;/checkout?product=&amp;lt;script&amp;gt;&lt;/code&gt; rendered verbatim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook trusted session metadata without binding to a stored attempt&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Any session claiming &lt;code&gt;paymentId&lt;/code&gt; could trigger confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy metadata passed through to Stripe unfiltered&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Client could overwrite server-owned fields — including &lt;code&gt;paymentId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency-Key replayed request fields unchecked&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Same key, different product → silently reuses the old payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prototype-chain network lookup&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;network: "constructor"&lt;/code&gt; and friends could resolve attacker-controlled shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata accepted into payment row, validated later&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Garbage-in payment rows, failure discovered at fulfillment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDC instructions rail always on&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;An unmonitored self-serve rail exposed in production by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All USDC payments quoted the same amount&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Ten buyers pay $9.99 → ten identical transfers; matching by tx is guesswork&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9qpff5j5qt79h3km13l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9qpff5j5qt79h3km13l.webp" alt="Illustration of the audit outcome: a BLOCK clipboard — Critical findings (fulfilled before handler, in-memory dedupe, 404 XSS), High and Medium columns — every item stamped FIXED beside a magnifier showing 17 tests" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All twenty findings stamped FIXED — 17 regression tests stand guard over the Critical and High list.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes — root cause, not patches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Order of operations (C-1).&lt;/strong&gt; &lt;code&gt;dispatchFulfillment&lt;/code&gt; now reports an explicit status — &lt;code&gt;fulfilled&lt;/code&gt;, &lt;code&gt;deduped&lt;/code&gt;, or &lt;code&gt;no-handler&lt;/code&gt; — and the completion marker is written &lt;em&gt;after&lt;/em&gt; the handler returns. A thrown handler produces a &lt;code&gt;failed&lt;/code&gt; event and a 5xx that lets the rail retry, instead of a silent "done".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable dedupe (C-2).&lt;/strong&gt; In-memory flags survived one process. Fulfillment now claims through the event store: a &lt;code&gt;fulfillment-claim&lt;/code&gt; event keyed by payment, a &lt;code&gt;fulfillment-done&lt;/code&gt; event on success. Fresh claims dedupe; claims older than ten minutes are treated as stale and retried — a crashed worker no longer locks the order forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ncdrvy8ot2menbj7clb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ncdrvy8ot2menbj7clb.webp" alt="State diagram of durable fulfillment dedupe: a fulfillment-claim event keyed by paymentId enters CLAIMED; a handler that returns writes fulfillment-done AFTER it returns (C-1); a fresh claim that sees an existing done or claim resolves deduped; a thrown handler produces a failed event and 5xx so the rail retries; claims older than ten minutes are stale and retry — an event-store claim, not an in-memory flag, so restarts and second instances still dedupe (C-2)" width="800" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The dedupe machine, exactly as it ships: claims live in the event store, so a restart can't unlock a double mint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binding, not trust (H-1/H-2).&lt;/strong&gt; The Stripe webhook now resolves &lt;code&gt;session.id → PaymentAttempt&lt;/code&gt; through the attempt store, verifies the session's payment status and amount against our own record, and only then confirms. Legacy checkout metadata is whitelisted — server-owned keys (&lt;code&gt;paymentId&lt;/code&gt;, &lt;code&gt;productId&lt;/code&gt;, &lt;code&gt;productCode&lt;/code&gt;, &lt;code&gt;tier&lt;/code&gt;) can no longer be injected by the client. Provider error details no longer leak into client responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency with a fingerprint (H-3).&lt;/strong&gt; Replaying an &lt;code&gt;Idempotency-Key&lt;/code&gt; returns the original payment — but only if the request fingerprint matches. Same key, different product or metadata → &lt;code&gt;409 idempotency_conflict&lt;/code&gt;. The header itself is schema-validated (8–128 chars, alphanumeric/underscore/hyphen) before it's ever used as a dedupe token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema-first metadata (H-5).&lt;/strong&gt; Validation moved upstream: &lt;code&gt;validateProductMetadata&lt;/code&gt; runs before the payment row is created. The schema rejects undeclared fields, enforces per-field types and length limits, and projects only declared keys — &lt;code&gt;sanitizeProductMetadata&lt;/code&gt; can't leak fields the product didn't ask for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rails that earn their way on (H-8).&lt;/strong&gt; The direct-USDC instructions stub ships behind &lt;code&gt;PAYMENTS_USDC_SELF_SERVE&lt;/code&gt; — off by default, opt-in when the monitoring story exists. An unmonitored rail should never be the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unique-amount dedupe (H-9).&lt;/strong&gt; The one visible in the screenshot: USDC instructions carry a deterministic micro-unit suffix derived from the payment ID — $9.99 becomes &lt;code&gt;9.990044&lt;/code&gt;. Ten buyers, ten distinct amounts, one recipient; on-chain matching becomes a lookup, not a guess. Attempts also reuse a pending attempt for the same payment/network instead of stacking siblings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcy38yteu2isau4854br.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcy38yteu2isau4854br.webp" alt="Two-panel illustration: left, an event-store ledger with fulfillment-claim and fulfillment-done entries, a 10-minute dedupe clock; right, the unique-amount funnel — USDC transfers with distinct micro-unit suffixes match payments exactly" width="800" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two fixes in one picture: the event-store claim that survives restarts, and the unique amount that turns matching into a lookup.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Soft migration, not a flag day
&lt;/h2&gt;

&lt;p&gt;The legacy &lt;code&gt;/payment&lt;/code&gt; and &lt;code&gt;/checkout&lt;/code&gt; endpoints still work — they now answer with &lt;code&gt;Deprecation&lt;/code&gt;, &lt;code&gt;Sunset&lt;/code&gt;, and &lt;code&gt;Link&lt;/code&gt; headers pointing at &lt;code&gt;/api/v1/payments&lt;/code&gt;, and a migration guide (&lt;code&gt;docs/payments-v1-migration.md&lt;/code&gt;) maps payloads field by field. Webhook behavior hardened in place: dispatch failures surface as 5xx so Stripe retries instead of swallowing the order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receipts, not vibes
&lt;/h2&gt;

&lt;p&gt;The dogfood run behind the screenshots: &lt;code&gt;POST /api/v1/payments&lt;/code&gt; (Bronze tier, catalog metadata) → &lt;code&gt;DIRECT_USDC&lt;/code&gt; attempt on &lt;code&gt;eip155:1874&lt;/code&gt; → instructions with the unique amount, Arc USDC contract, treasury recipient, reference, one-hour TTL → &lt;code&gt;GET /api/v1/payments/:id&lt;/code&gt; polling PROCESSING. Seventeen new regression tests cover every Critical/High finding; the payment/checkout suite runs 77/77 green. The gate's shared baseline has unrelated drift from parallel EPICs — every file this EPIC touched type-checks and lints clean.&lt;/p&gt;

&lt;p&gt;What we'd tell past us: the audit found what "it works in my manual test" never would — race windows, trust boundaries, and the XSS hiding in a 404. Shipping checkout code without an adversarial pass is a gamble on the one page that takes money.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents
&lt;/h2&gt;

&lt;p&gt;If you are an agent reading this: the checkout described above is machine-callable — product discovery, payment creation, rail selection, status, and receipt are all API surfaces, no credentials needed to read the catalog.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET https://agentbadge.xyz/api/v1/services&lt;/code&gt; — machine-readable SKU catalog: what's sellable, USD price, endpoint, input schema per SKU.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST https://agentbadge.xyz/api/v1/payments&lt;/code&gt; — create a payment: &lt;code&gt;{ productId, metadata }&lt;/code&gt; + optional &lt;code&gt;Idempotency-Key&lt;/code&gt; (replay returns the original payment only when the request fingerprint matches) → &lt;code&gt;{ payment, deduped }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST https://agentbadge.xyz/api/v1/payments/:id/attempts&lt;/code&gt; — choose the rail: &lt;code&gt;{ "provider": "STRIPE" }&lt;/code&gt; → &lt;code&gt;redirectUrl&lt;/code&gt;, or &lt;code&gt;{ "provider": "DIRECT_USDC", "network": "eip155:1874" }&lt;/code&gt; → instructions &lt;code&gt;{ amount, recipient, tokenContract, reference, expiresAt }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET https://agentbadge.xyz/api/v1/payments/:id&lt;/code&gt; — status + attempts; &lt;code&gt;GET https://agentbadge.xyz/api/v1/payments/:id/receipt&lt;/code&gt; — receipt once confirmed.&lt;/li&gt;
&lt;li&gt;Discovery surfaces: &lt;code&gt;/.well-known/agentbadge.json&lt;/code&gt;, &lt;code&gt;/llms.txt&lt;/code&gt;, &lt;code&gt;/pricing.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Companion markdown: &lt;code&gt;/blog/arc-c46-checkout-two.md&lt;/code&gt;; machine-readable KO: &lt;code&gt;/blog/arc-c46-checkout-two.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Honesty flags: the &lt;code&gt;DIRECT_USDC&lt;/code&gt; instructions rail is opt-in (&lt;code&gt;PAYMENTS_USDC_SELF_SERVE&lt;/code&gt;) — don't assume it's on everywhere; the USDC amount carries a deterministic micro-unit suffix derived from the payment ID (&lt;code&gt;9.990044&lt;/code&gt;, not &lt;code&gt;9.99&lt;/code&gt;) — match by exact amount + reference, not by "roughly the price"; HBAR appears only as a legacy Hedera rail, not a checkout option.&lt;/li&gt;
&lt;li&gt;Series: &lt;a href="https://agentbadge.xyz/blog/payment-infrastructure-foundation" rel="noopener noreferrer"&gt;The Money Layer Is Not the Rail&lt;/a&gt; (domain foundation) → this (the storefront built on it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>payments</category>
      <category>webdev</category>
      <category>blockchain</category>
      <category>security</category>
    </item>
    <item>
      <title>Brick #5: The Experiment Ran - a Three-Hop Supply Chain Settled on Arc Testnet</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Sun, 08 Nov 2026 18:45:38 +0000</pubDate>
      <link>https://dev.to/spread2009/brick-5-the-experiment-ran-a-three-hop-supply-chain-settled-on-arc-testnet-1c4o</link>
      <guid>https://dev.to/spread2009/brick-5-the-experiment-ran-a-three-hop-supply-chain-settled-on-arc-testnet-1c4o</guid>
      <description>&lt;p&gt;&lt;em&gt;Final brick of the MVP wave - start here: &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;, then &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1: Typed Participants&lt;/a&gt;, &lt;a href="https://agentbadge.xyz/blog/supply-chain-typed-evidence" rel="noopener noreferrer"&gt;Brick #2: Typed Evidence&lt;/a&gt;, &lt;a href="https://agentbadge.xyz/blog/supply-chain-scenario-matrix" rel="noopener noreferrer"&gt;Brick #3: Scenario Matrix&lt;/a&gt; and &lt;a href="https://agentbadge.xyz/blog/supply-chain-verifiable-showcase" rel="noopener noreferrer"&gt;Brick #4: Verifiable Vitrine&lt;/a&gt;. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four bricks built and armored the write path: typed participants, typed evidence, a scenario matrix that survives seven named violations, and a public replay view. The anchor article ended with a promise - &lt;strong&gt;"The experiment has started."&lt;/strong&gt; This article is the receipt: on 2026-11-08 a real three-hop shipment ran the full custody cycle on Arc testnet, each hop escrowed in its own ERC-8183 job, all three settled and verified in 53 seconds. &lt;strong&gt;Proof -&amp;gt; acceptance -&amp;gt; settlement, on real rails, with transaction hashes you can open yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly ran?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; shipment &lt;code&gt;dogfood-1794151620383&lt;/code&gt; - a cold-chain delivery with three hops, played by three different participant types from Brick #1. A shipper contracted an agent (&lt;em&gt;Plant intake&lt;/em&gt;), the agent handed custody to a device (&lt;em&gt;ColdVan unit&lt;/em&gt;), and the device delivered to an org (&lt;em&gt;Restaurant&lt;/em&gt;). Each hop carried a 0.01 USDC escrow in its own ERC-8183 job on the ref proxy &lt;code&gt;0x0049e5...bf6a0&lt;/code&gt;, Arc testnet (chainId &lt;code&gt;5042002&lt;/code&gt;). Result: status &lt;code&gt;completed&lt;/code&gt;, 3/3 hops verified, &lt;code&gt;outstandingUsdc: "0"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The shipment, in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;hop&lt;/th&gt;
&lt;th&gt;leg&lt;/th&gt;
&lt;th&gt;job&lt;/th&gt;
&lt;th&gt;create tx&lt;/th&gt;
&lt;th&gt;settle tx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;shipper -&amp;gt; agent (Plant intake)&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0x606f189928acbfc03af27419d2e324868a2e11fac15e2a2c75d3d1f2e5c6995e" rel="noopener noreferrer"&gt;&lt;code&gt;0x606f18...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0xf15f6e1e11237427a62cc050c7cd4ec517a25d497d804b1a0d437f4e0d070a05" rel="noopener noreferrer"&gt;&lt;code&gt;0xf15f6e...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;agent -&amp;gt; device (ColdVan unit)&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0xf9ebb85b65f54764d9925687b88003637f5973b3ce0d2d3c39124f3db05e57a7" rel="noopener noreferrer"&gt;&lt;code&gt;0xf9ebb8...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0x35f507f1d8654fa7269a8e6d95495bbc9a1b5e1fb0539c485de5f7c52c97b542" rel="noopener noreferrer"&gt;&lt;code&gt;0x35f507...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;device -&amp;gt; org (Restaurant)&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0xdc7c70beed73c41d60b99763b36c9e4cab5f7bf73fe8852141052501fec5c3ff" rel="noopener noreferrer"&gt;&lt;code&gt;0xdc7c70...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testnet.arcscan.app/tx/0x7798a4e627633c897ad256319d0d1158b2ecbcc5a5e2de02fdbd7b886db1df39" rel="noopener noreferrer"&gt;&lt;code&gt;0x7798a4...&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six real transactions - three job creations, three settlements. Not a fixture, not a fork, not a replayed simulation: Arc testnet, confirmed on the public explorer.&lt;/p&gt;

&lt;p&gt;The participants are real keys with real labels (Brick #1's typed roles, live):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;address&lt;/th&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;client (shipper)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xcdd23d104AA4C10DE65F4DD0571eDfeC0458699d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hop 0 provider - Plant intake&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb21fa9D9CE930FAf52545318169569007c027005&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hop 1 provider - ColdVan unit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x67d43A4065Dd69165061062B3128EDA50f1F7C96&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hop 2 provider - Restaurant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x8B9FC021430c4fAD90389DcbdE4B1140012Cd98D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;evaluator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xc9fA59e889c25b7f0b9494ed5c6bC62fe7EcCC56&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;signer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What did each hop actually execute on-chain?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the full ERC-8183 lifecycle, not just an event log entry. Per hop the executor drives four stages of contract state: the provider signs &lt;code&gt;setBudget&lt;/code&gt;, the client &lt;code&gt;approve&lt;/code&gt;s and &lt;code&gt;fund&lt;/code&gt;s the escrow, the provider &lt;code&gt;submit&lt;/code&gt;s the deliverable, and the evaluator &lt;code&gt;complete&lt;/code&gt;s the job - releasing escrow only after the accept.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider setBudget
      |
client approve + fund   &amp;lt;- 0.01 USDC escrowed
      |
provider submit         &amp;lt;- deliverable declared
      |
evaluator complete      &amp;lt;- escrow released
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ordering is the Bedrij primitive running on contract state instead of test fixtures. Hop i+1 cannot hand off before hop i is paid - the same &lt;code&gt;ShipmentOrderError&lt;/code&gt; guard the scenario matrix breaks against, now re-deriving state from a log whose &lt;code&gt;pay&lt;/code&gt;/&lt;code&gt;verify&lt;/code&gt; events were produced by real settlement transactions. The economic loop the anchor drew as &lt;code&gt;proof -&amp;gt; acceptance -&amp;gt; settlement&lt;/code&gt; fired three times in 53 seconds of wall clock (15:27:41Z - 15:28:34Z).&lt;/p&gt;

&lt;p&gt;And because the run wrote to the production Postgres ledger (&lt;code&gt;SC_DOGFOOD_DB=1&lt;/code&gt;), the whole timeline is publicly replayable - Brick #4's vitrine, pointed at a real shipment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://agentbadge.xyz/api/ledger/shipments/dogfood-1794151620383
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dogfood-1794151620383"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"13 events, 15:27:41Z - 15:28:34Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 hops, all verified, jobs 66/67/68"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aggregate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"completedPct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outstandingUsdc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Truncated for print - the verbatim response is at the URL above.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What did running it live actually find?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; four executor bugs that no test caught, because the in-memory chain lets any key call any method. The ref contract does not. Running against real signatures exposed the write path's hidden assumptions - which is exactly why a live dogfood is the last brick, not the first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider-signed &lt;code&gt;setBudget&lt;/code&gt;.&lt;/strong&gt; The ERC-8183 ref variant restricts &lt;code&gt;setBudget&lt;/code&gt; to the provider account. The executor used to send it as the client - on a real contract that is a revert, in a test it is silent. Fix: &lt;code&gt;spec.providerKey&lt;/code&gt; plus a &lt;code&gt;sendAsAccount&lt;/code&gt; path that signs locally and broadcasts the raw transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider-signed &lt;code&gt;submit&lt;/code&gt; before &lt;code&gt;complete&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;complete&lt;/code&gt; requires the job to be in &lt;code&gt;Submitted&lt;/code&gt; status - the provider must declare the deliverable first. The earlier order tried to complete directly after funding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ClientCannotBeProvider&lt;/code&gt;.&lt;/strong&gt; Hop 2's receiver is the restaurant - but the client wallet cannot also act as a provider. The run needed a dedicated restaurant key (&lt;code&gt;SC_DOGFOOD_RESTAURANT_KEY&lt;/code&gt;) or the contract refused the hop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume, don't orphan.&lt;/strong&gt; A failed live run leaves funded-but-unfinished jobs on-chain. &lt;code&gt;SC_DOGFOOD_RESUME_JOBS&lt;/code&gt; reuses them instead of orphaning escrow - the same C-1 orphan-escrow finding the audit flagged before we ever went live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two earlier attempts informed this one. An in-memory run (&lt;code&gt;dogfood-1794150937615&lt;/code&gt;, jobs 63/64/65) exercised the resume path on reused jobs; the DB-backed run above is canonical because it is the one the public replay serves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the audit hold up under live fire?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes - the two Critical findings were exactly the class of bug that kills a live run, and both were fixed before the first testnet transaction. Audit first, live second: the gate was load-bearing.&lt;/p&gt;

&lt;p&gt;Brick #5 had its own external adversarial review before going live. Verdict: REQUEST CHANGES - 2 Critical, 4 High confirmed. C-1 was the orphan-escrow hole: if &lt;code&gt;createShipment&lt;/code&gt; succeeded on-chain but the ledger write failed, escrow sat funded with no record. C-2 was a fail-open budget path - a null/zero-amount job could slip past as a plan of &lt;code&gt;"0"&lt;/code&gt;. Both were fixed and re-verified, the pack went from 87/87 to &lt;strong&gt;99/99 green&lt;/strong&gt; with 12 new regression tests, and only then did the executor see a real network. The findings that would have lost testnet escrow were found by an auditor, not by a revert.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this prove - and what does it still not prove?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the transition contract executes on real rails. It does not yet prove automated verification, non-linear chains, or real money at scale - and the article says so in the same breath as the result.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;claim&lt;/th&gt;
&lt;th&gt;status 2026-11-08&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Verifiable transitions compose into a chain&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;proven&lt;/strong&gt; - 3 hops, ordered, verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-hop escrow on a real contract&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;proven&lt;/strong&gt; - ERC-8183, 6 confirmed txs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering enforced from an append-only log&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;proven&lt;/strong&gt; - same guard as Brick #3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public replay of a real shipment&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;proven&lt;/strong&gt; - endpoint live now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated evidence evaluation&lt;/td&gt;
&lt;td&gt;still manual - a delegated evaluator gates each settle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence authenticity&lt;/td&gt;
&lt;td&gt;advisory - typed refs, self-declared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-linear / branching chains&lt;/td&gt;
&lt;td&gt;linear hops only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-value settlement&lt;/td&gt;
&lt;td&gt;0.01 USDC per hop, testnet gas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three honesty notes matter for anyone replaying the shipment. The &lt;code&gt;pay&lt;/code&gt;/&lt;code&gt;txHash&lt;/code&gt; fields in the replay response are &lt;strong&gt;ledger-asserted&lt;/strong&gt; - written by our executor, not independently chain-verified (the tx hashes above are the independent check; open them). Settlement was &lt;strong&gt;ops-settled&lt;/strong&gt; - the evaluator signer is a delegated operations key, not the logical evaluator in the plan. And the amounts are deliberately tiny: a dogfood run proves the rail, not the revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment ran
&lt;/h2&gt;

&lt;p&gt;The anchor article asked whether a complex economic system could be composed from verifiable state transitions, and promised five bricks of measurement. The bricks are laid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;roles      - who acted          (Brick #1)
evidence   - what they claim    (Brick #2)
matrix     - how it breaks      (Brick #3)
vitrine    - how you check      (Brick #4)
live       - did it run         (Brick #5 - this)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What shipped in one number: &lt;strong&gt;53 seconds, 3 hops, 6 transactions, 13 log events, 0 USDC outstanding&lt;/strong&gt; - a custody cycle that a stranger can re-derive from a public endpoint, with the transaction hashes in the explorer one click away. The transition contract did not just type-check against a fixture. It settled.&lt;/p&gt;

&lt;p&gt;That is the whole answer to the hypothesis. Not "we believe it works" - here is the shipment, replay it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Hero image: &lt;a href="https://agentbadge.xyz/images/blog/supply-chain-live-testnet-hero.webp" rel="noopener noreferrer"&gt;https://agentbadge.xyz/images/blog/supply-chain-live-testnet-hero.webp&lt;/a&gt; — canonical: &lt;a href="https://agentbadge.xyz/blog/supply-chain-live-testnet" rel="noopener noreferrer"&gt;https://agentbadge.xyz/blog/supply-chain-live-testnet&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>Brick #4: Show Me the Evidence - a Verifiable Vitrine for Agent Supply Chains</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Sun, 08 Nov 2026 10:00:01 +0000</pubDate>
      <link>https://dev.to/spread2009/brick-4-show-me-the-evidence-a-verifiable-vitrine-for-agent-supply-chains-2hbh</link>
      <guid>https://dev.to/spread2009/brick-4-show-me-the-evidence-a-verifiable-vitrine-for-agent-supply-chains-2hbh</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the verifiable supply-chain series - start here: &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;, then &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1: Typed Participants&lt;/a&gt;, &lt;a href="https://agentbadge.xyz/blog/supply-chain-typed-evidence" rel="noopener noreferrer"&gt;Brick #2: Typed Evidence&lt;/a&gt; and &lt;a href="https://agentbadge.xyz/blog/supply-chain-scenario-matrix" rel="noopener noreferrer"&gt;Brick #3: Scenario Matrix&lt;/a&gt;. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Trust me" is not an API response. Bricks #1-#3 built and stress-tested the write path - typed participants, typed evidence, a scenario matrix that survives seven named violations. This brick opens the other side: &lt;strong&gt;a public, embeddable JSON vitrine that lets anyone replay a shipment's history and check the claims themselves.&lt;/strong&gt; No login, no key, no SDK - &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt; and the chain speaks for itself.&lt;/p&gt;

&lt;p&gt;The Bedrij Principle predicts that if state transitions are truly verifiable, then a &lt;em&gt;composed&lt;/em&gt; chain must be readable from the outside - the transitions' evidence can't hide behind the system that wrote it. This brick is the experimental proof of that sub-hypothesis: the same append-only log that drives the state machine now renders a &lt;code&gt;ShipmentReplayResponse&lt;/code&gt; a stranger can audit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0yfltzumamlhzas5up3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0yfltzumamlhzas5up3.webp" alt="A glass museum vitrine holding a three-crate supply chain - two crates glow emerald (verified, paid), the third is dim and still offered; a robot agent holds a magnifying glass over the chain" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a shipment replay view?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a read model that walks the append-only event log for one &lt;code&gt;shipmentId&lt;/code&gt;, applies the canonical-sequence rules from Brick #3, and returns a single JSON document - &lt;code&gt;status&lt;/code&gt;, ordered &lt;code&gt;timeline&lt;/code&gt;, per-hop detail (&lt;code&gt;roles&lt;/code&gt;, &lt;code&gt;evidence&lt;/code&gt;, &lt;code&gt;jobId&lt;/code&gt;, status), an &lt;code&gt;aggregate&lt;/code&gt; (completion %, current custodian, outstanding USDC), and a &lt;code&gt;limits&lt;/code&gt; array stating what the view does &lt;em&gt;not&lt;/em&gt; promise. One endpoint, &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt;, public by design.&lt;/p&gt;

&lt;p&gt;"Replay" is literal: there is no separate view table to trust. The endpoint re-derives state from &lt;code&gt;source=supply_chain&lt;/code&gt; events keyed by &lt;code&gt;sourceId&lt;/code&gt; - the same projection the write path uses internally. If the log is honest, the view is honest; if the view disagrees with the log, the bug is in five functions you can read, not in a hidden cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the shipment: a cold-chain run in plain words
&lt;/h2&gt;

&lt;p&gt;Picture a delivery any logistics manager would recognize. A biotech buyer contracts a refrigerated container of temperature-sensitive product at the Maasvlakte cold port in Rotterdam. The container has three legs: the port hands it to a trucking company, the trucker hands it to a pharma warehouse in Venlo. Each leg repeats the same three-step ritual - &lt;strong&gt;hand over custody, pay for the leg, verify the handoff.&lt;/strong&gt; A reefer logger on the container writes temperature evidence, the port writes a bill of lading, the truck's telematics write GPS traces, and the dispatcher bot signs the acceptance.&lt;/p&gt;

&lt;p&gt;In the demo chain below, leg one is finished - custody moved from port to trucker, 120.5 USDC paid, verification passed. Leg two is done and paid (85 USDC). Leg three is only &lt;em&gt;offered&lt;/em&gt;: the warehouse hasn't accepted yet, 40.25 USDC still outstanding. Everything a courier's tracking page would show - plus the one thing it never shows: &lt;strong&gt;anyone can re-check the math.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a real response look like?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; below is the verbatim output of &lt;code&gt;shipmentReplay()&lt;/code&gt; over a scripted three-hop demo chain mid-flight: hop 0 verified, hop 1 paid, hop 2 still offered. Not hand-written JSON - the chain was driven through the same &lt;code&gt;createShipment → handoffHop → settleHop → verifyHop&lt;/code&gt; calls the API wraps, then read back by the replay pipeline that serves &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shp-demo-vitrine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in_progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"work_offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.500Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"work_accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.503Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handoff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.506Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.508Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.511Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"work_accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.512Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handoff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.512Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-07T21:01:40.513Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1111111111111111111111111111111111111111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amountUsdc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"120.5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job-shp-demo-vitrine-0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"custodian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1111…1111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Maasvlakte ColdPort B.V."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x4444…4444"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sensitech Tag-9 reefer logger"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rolesAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x0000…0001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x4444…4444"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"device"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ipfs://bafybeih7f2i3d9q3k5m4n6o7p8q9r0s1t2u3v4w5x6y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuerMatchesParty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x0000…0002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1111…1111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.coldport.example/bol/BX-2041"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuerMatchesParty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidenceAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x2222…2222"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amountUsdc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"85"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job-shp-demo-vitrine-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"custodian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x2222…2222"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Venlo Freight Logistics"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x5555…5555"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fleet-dispatch-bot"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rolesAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1111…1111"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x3333…3333"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amountUsdc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"40.25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"offered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job-shp-demo-vitrine-2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggregate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completedPct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currentCustodian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x2222222222222222222222222222222222222222"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outstandingUsdc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"40.25"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"linear hops only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"manual settlement per hop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"evidence is advisory — not verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"caps: ≤256 hops / ≤4096 events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"status lifted from canonical slots — writer trust at ledger layer"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in that payload carry the whole article. First, &lt;code&gt;rolesAssertedBy&lt;/code&gt;/&lt;code&gt;evidenceAssertedBy&lt;/code&gt; on every hop - the view never lets you forget &lt;em&gt;who claimed&lt;/em&gt; the roles and evidence (Brick #1/2's self-declaration boundary, surfaced). Second, &lt;code&gt;issuerMatchesParty&lt;/code&gt; - the reader can see that the temperature ref's issuer is the declared device, not a hop party, without trusting our word for it: the address is right there. Third, &lt;code&gt;timeline[].hopIndex&lt;/code&gt; exists only on &lt;code&gt;handoff&lt;/code&gt;/&lt;code&gt;pay&lt;/code&gt;/&lt;code&gt;verify&lt;/code&gt; events at canonical sequence slots - you'll see why that matters below.&lt;/p&gt;

&lt;p&gt;Read it like a tracking page, then like an auditor. &lt;code&gt;timeline&lt;/code&gt; is the event feed - the offer at seq 1, then accepts, handoffs, payments, and the &lt;code&gt;verify&lt;/code&gt; at seq 6 that closed leg one. &lt;code&gt;hops[].roles&lt;/code&gt; names who was involved - the port as custodian, the reefer logger as a device - and &lt;code&gt;hops[].evidence&lt;/code&gt; lists their artifacts with &lt;code&gt;*AssertedBy&lt;/code&gt; provenance: who &lt;em&gt;claimed&lt;/em&gt; it, not who proved it. &lt;code&gt;aggregate&lt;/code&gt; is the headline a dashboard would print: 33% complete, the trucker currently holding custody, 40.25 USDC left to pay.&lt;/p&gt;

&lt;p&gt;If a dispute lands tomorrow - "the container arrived warm, who pays?" - this JSON is the document both sides start from, and neither of them had to produce it. That is the difference between a tracking page and a vitrine: the tracking page is the courier's word; this is the courier's arithmetic, checkable by a stranger.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the read path stay honest?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the same canonical gate that protects the write-side projection guards the public view - and this brick's own adversarial audit made it stricter: timeline &lt;code&gt;hopIndex&lt;/code&gt; lifts only from canonical-slot events bound to this shipment, plan membership is checked by index set (not count), oversized streams refuse instead of truncating silently, and every refusal is warn-logged with a reason.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fptg6dr50q7q3l1so269z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fptg6dr50q7q3l1so269z.webp" alt="The replay vitrine as a pipeline: event tickets feed a canonical replay dome where forged tickets bounce off; a JSON card shows status in_progress, completedPct 33" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The read path end-to-end (diagram below): the request is rate-limited and shape-checked (&lt;code&gt;SHIPMENT_ID_RE&lt;/code&gt; → honest &lt;code&gt;400 invalid_id&lt;/code&gt;), the log scan binds &lt;code&gt;sourceId&lt;/code&gt; to the requested id - the log is the source of truth - and the two-pass canonical replay lifts plan from the &lt;code&gt;work_offer&lt;/code&gt; at seq 1, then lifts &lt;code&gt;handoff&lt;/code&gt;/&lt;code&gt;pay&lt;/code&gt;/&lt;code&gt;verify&lt;/code&gt; only from canonical slots. Anything forged - wrong seq, foreign &lt;code&gt;shipmentId&lt;/code&gt;, crafted terms - takes the drop path and lifts nothing, not even status. Completed shipments cache for 60 seconds; everything else is &lt;code&gt;no-store&lt;/code&gt;; disabled storage and store errors answer &lt;code&gt;503 unavailable&lt;/code&gt;, unknown ids &lt;code&gt;404&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The everyday version of this check: recomputing a bank statement from the receipts instead of trusting the courier's tracking screenshot. A forged "handoff" ticket - wrong sequence, wrong shipment, wrong slot - is a receipt that doesn't belong in the pile, so replay drops it on the floor and logs why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67t06mpgdelukp5e4auv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67t06mpgdelukp5e4auv.webp" alt="Flowchart of the public read path: GET → rate limit + id shape → log scan → canonical replay → ShipmentReplayResponse; forged events drop warn-logged; completed → max-age 60; 503/404 honest failures" width="800" height="1358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the audit change - and why publish your own findings?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; an external adversarial review of this endpoint returned REQUEST CHANGES - 0 Critical, 5 High, 7 Medium. All five Highs are fixed or documented with regression tests (&lt;code&gt;tests/supply-chain/replay-audit.test.ts&lt;/code&gt;, 16 cases); the Mediums are fixed or explicitly accepted in the review doc. We publish the count because a vitrine that hides its own cracks isn't a vitrine.&lt;/p&gt;

&lt;p&gt;The Highs, briefly: &lt;strong&gt;crafted terms&lt;/strong&gt; (&lt;code&gt;hops&lt;/code&gt; with holes, non-string &lt;code&gt;amountUsdc&lt;/code&gt;, wrong types) could reach &lt;code&gt;BigInt&lt;/code&gt; arithmetic and throw - now every terms/payload field is guarded before use, and &lt;code&gt;sumUsdc&lt;/code&gt; fails with a bounded &lt;code&gt;ShipmentOrderError&lt;/code&gt;. &lt;strong&gt;Timeline forgery&lt;/strong&gt; - any event's &lt;code&gt;payload.hopIndex&lt;/code&gt; used to annotate the timeline - now only canonical-slot events bound to &lt;code&gt;shipmentId&lt;/code&gt; can. &lt;strong&gt;No size caps&lt;/strong&gt; - replay refused nothing; now &lt;code&gt;≤256 hops / ≤4096 events&lt;/code&gt; with a warn-logged refuse. &lt;strong&gt;Forwarded-IP trust&lt;/strong&gt; - &lt;code&gt;X-Forwarded-For&lt;/code&gt; shaped rate-limit buckets for any caller; now it requires &lt;code&gt;TRUST_PROXY_HEADERS=true&lt;/code&gt;. &lt;strong&gt;Plan binding&lt;/strong&gt; - a mismatched &lt;code&gt;terms.shipmentId&lt;/code&gt; could still bind the plan's &lt;code&gt;client&lt;/code&gt;; now the client binds only when terms are absent, silent, or matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the &lt;code&gt;limits&lt;/code&gt; array inside the response?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; because a verifiable view that doesn't declare its own boundaries pushes readers toward false conclusions. &lt;code&gt;limits&lt;/code&gt; is the honesty contract - five machine-readable strings that travel with every response: linear hops only, manual settlement, evidence is advisory (not verified), the size caps, and the writer-trust boundary (status is lifted from canonical slots - writer authorization lives at the ledger layer, not the read path).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxobfe3hise5pqn4r4olz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxobfe3hise5pqn4r4olz.webp" alt="A brass museum plaque under the vitrine titled limits with five engraved lines: linear hops only; manual settlement per hop; evidence is advisory - not verified; caps ≤256 hops / ≤4096 events; status lifted from canonical slots" width="800" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The subtle one is the last line. A public replay can prove &lt;em&gt;which events exist&lt;/em&gt; and &lt;em&gt;what they claim&lt;/em&gt; - it cannot prove &lt;em&gt;who was allowed to write them&lt;/em&gt;. That boundary is declared, not hidden: writer trust is a ledger-layer concern (a later brick), and the read model says so out loud. "Evidence is advisory" is the Brick #2 boundary restated - &lt;code&gt;issuerMatchesParty&lt;/code&gt; tells you the issuer's relationship to the parties, never whether the artifact itself is authentic. It is the same honesty as a lab report that prints its detection limits next to the result - the number means more when the instrument admits what it cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you reuse the vitrine?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; embed it. The response is a stable, closed-shape contract (&lt;code&gt;ShipmentReplayResponse&lt;/code&gt; - the parser is the contract's own zod schema), cache-friendly on completion, honest on failure (400/404/503, never 200-with-error). A market UI, a dispute dashboard, or another agent can render or verify the chain without a library - or diff two responses over time to watch custody move.&lt;/p&gt;

&lt;p&gt;For the rest of us the use cases are mundane and real: an insurer checking a cold-chain claim, a buyer opening a dispute, a freight marketplace rendering custody history before accepting a bid - none of them needs an account with us.&lt;/p&gt;

&lt;p&gt;For builders, the replay code is &lt;code&gt;src/server/lib/econ-ledger/supply-chain/replay.ts&lt;/code&gt;, the projection in &lt;code&gt;project.ts&lt;/code&gt;, and the route in &lt;code&gt;econ-ledger-shipments-api.ts&lt;/code&gt;. The audit regression file (&lt;code&gt;replay-audit.test.ts&lt;/code&gt;) doubles as executable documentation of every hostile input we now refuse. And the demo chain above is one fixture call - &lt;code&gt;makeChain&lt;/code&gt; from Brick #2's test suite drives the same lifecycle events a production shipment would.&lt;/p&gt;

&lt;p&gt;Current state: &lt;strong&gt;16/16 audit regression tests, 84/84 targeted suite green&lt;/strong&gt;, zero open Critical or High findings on the read path. The fix commit is &lt;code&gt;330f17a&lt;/code&gt; (&lt;code&gt;fix(215-5)&lt;/code&gt;); the per-finding resolution lives in the epic's review doc.&lt;/p&gt;




&lt;h2&gt;
  
  
  For AI Agents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What this proves:&lt;/strong&gt; a composed transition chain is readable from the outside - &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt; returns canonical replay state, per-hop claims with provenance (&lt;code&gt;*AssertedBy&lt;/code&gt;, &lt;code&gt;issuerMatchesParty&lt;/code&gt;), and its own &lt;code&gt;limits&lt;/code&gt; - the sub-hypothesis "verifiability of transitions is public" holds on a real endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live surface:&lt;/strong&gt; &lt;code&gt;GET https://agentbadge.xyz/api/ledger/shipments/:id&lt;/code&gt; - no auth; &lt;code&gt;404 not_found&lt;/code&gt; unknown, &lt;code&gt;400 invalid_id&lt;/code&gt; malformed, &lt;code&gt;503 unavailable&lt;/code&gt; on store failure; completed responses cache 60s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse:&lt;/strong&gt; response schema = &lt;code&gt;ShipmentReplayResponse&lt;/code&gt; (closed shape, zod-validated); replay/projection code in &lt;code&gt;src/server/lib/econ-ledger/supply-chain/&lt;/code&gt;; hostile-input contract in &lt;code&gt;tests/supply-chain/replay-audit.test.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt; → &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1&lt;/a&gt; → &lt;a href="https://agentbadge.xyz/blog/supply-chain-typed-evidence" rel="noopener noreferrer"&gt;Brick #2&lt;/a&gt; → &lt;a href="https://agentbadge.xyz/blog/supply-chain-scenario-matrix" rel="noopener noreferrer"&gt;Brick #3&lt;/a&gt; → this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building agent-native supply chains? The ledger package is &lt;code&gt;@agentbadge/econ-events&lt;/code&gt; on npm, and the anchor hypothesis this proves a piece of is in &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>Brick #3: Seven Ways to Break a Chain — a Scenario Matrix for Verifiable Supply Chains</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Sat, 07 Nov 2026 09:49:30 +0000</pubDate>
      <link>https://dev.to/spread2009/brick-3-seven-ways-to-break-a-chain-a-scenario-matrix-for-verifiable-supply-chains-5f03</link>
      <guid>https://dev.to/spread2009/brick-3-seven-ways-to-break-a-chain-a-scenario-matrix-for-verifiable-supply-chains-5f03</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the verifiable supply-chain series — start here: &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;, then &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1: Typed Participants&lt;/a&gt; and &lt;a href="https://agentbadge.xyz/blog/supply-chain-typed-evidence" rel="noopener noreferrer"&gt;Brick #2: Typed Evidence&lt;/a&gt;. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A primitive that only works in demos is not a primitive. Before a supply-chain transition contract ever touches a testnet, you want to know exactly which violations it survives — and which it doesn't, yet.&lt;/p&gt;

&lt;p&gt;Bricks #1 and #2 built the happy path: typed participants and typed evidence travelling inside the handoff event itself. This brick goes the other direction. Instead of asking "does it work?", we asked "how does it break?" — seven scenarios, each one a violation a real chain will eventually see: undeclared telemetry, a rogue sensor, a missing inspection photo, a forged replay, a receiver dispute, a double settle racing the lock.&lt;/p&gt;

&lt;p&gt;This is Brick #3 — the third experimental proof in the Bedrij-series hypothesis, and the first that proves &lt;em&gt;resilience&lt;/em&gt; rather than &lt;em&gt;shape&lt;/em&gt;: &lt;strong&gt;the primitive has a regression contract.&lt;/strong&gt; Every violation is exercised on an in-memory stack, its expected outcome is asserted, and after an adversarial AI audit found six real High-severity holes, every one of them got its own regression test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a supply-chain scenario matrix?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a scenario matrix is a fixed set of end-to-end test scenarios — happy path plus violations — that run a shipment through the full lifecycle (&lt;code&gt;offer → handoff → settle → verify&lt;/code&gt;) on an in-memory ledger and executor, then assert the replayed state and advisory flags. Ours is one file, &lt;code&gt;tests/supply-chain/scenario-matrix.test.ts&lt;/code&gt;, 18 tests covering seven scenarios S1–S7 plus the audit hardening cases. Each scenario answers one question: &lt;em&gt;if this goes wrong, does the state machine stay honest?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The stack is deliberately boring. &lt;code&gt;makeChain()&lt;/code&gt; — a shared fixture from Brick #2's audit — builds a shipment with N hops, typed roles, per-hop evidence, then drives &lt;code&gt;accept → handoff → settle → verify&lt;/code&gt; on an &lt;code&gt;InMemoryHopExecutor&lt;/code&gt; with deterministic job IDs. No chain, no RPC, no wall clock: the matrix replays the same events a production ledger would store, which is the point — the regression contract covers the &lt;em&gt;projection&lt;/em&gt;, the part that turns an append-only log into answers like "who holds custody" and "is this hop paid".&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the seven scenarios actually prove?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; S1 proves the happy path end-to-end; S2–S5 prove each advisory flag fires on exactly the violation it names — and only there; S6 proves a receiver can halt the chain by disputing; S7 proves payment is exactly-once under concurrency. Every assertion checks both the write payload and the replayed read model — a scenario that only verifies the call succeeded would prove nothing about the projection.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Violation&lt;/th&gt;
&lt;th&gt;Expected outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S1&lt;/strong&gt; happy path&lt;/td&gt;
&lt;td&gt;none — 3-hop chain, declared device, party-issued telemetry&lt;/td&gt;
&lt;td&gt;all hops &lt;code&gt;verified&lt;/code&gt;, &lt;strong&gt;zero&lt;/strong&gt; advisory flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S2&lt;/strong&gt; temp violation&lt;/td&gt;
&lt;td&gt;temperature ref from a source that is neither a hop party nor the declared device&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tempViolation&lt;/code&gt; flag — settle still executes (advisory, not enforcement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S3&lt;/strong&gt; fake sensor&lt;/td&gt;
&lt;td&gt;telemetry-typed ref whose issuer address ≠ declared &lt;code&gt;roles.device&lt;/code&gt; — including an issuer claiming type &lt;code&gt;"org"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fakeSensor&lt;/code&gt; flag; with no declared device the flag stays off (cannot judge → don't guess)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S4&lt;/strong&gt; missing telemetry&lt;/td&gt;
&lt;td&gt;a hop handoff carrying no evidence at all&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;missingEvidence&lt;/code&gt; flag, chain remains valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S5&lt;/strong&gt; GPS anomaly&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;gps&lt;/code&gt; ref from a non-party, non-device issuer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpsAnomaly&lt;/code&gt; flag; a party-issued GPS ref stays clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S6&lt;/strong&gt; receiver dispute&lt;/td&gt;
&lt;td&gt;receiver rejects the handoff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;disputed&lt;/code&gt; flag, &lt;code&gt;settleHop&lt;/code&gt; refuses while disputed, chain halts at that hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S7&lt;/strong&gt; double settle&lt;/td&gt;
&lt;td&gt;two concurrent &lt;code&gt;settleHop&lt;/code&gt; calls on the same hop&lt;/td&gt;
&lt;td&gt;exactly-once: one executor call, one &lt;code&gt;pay&lt;/code&gt; event — the loser queues on the per-hop lock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details in that table matter more than the flags themselves.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;flags are advisory, never blocking&lt;/strong&gt; — that's a decision, not an omission. S2's temperature violation does not hold the payment; it records that a violation was asserted. Automatic hold is a dispute-engine problem (a later brick), and pretending v1 enforces it would be a lie printed in our own docs.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;S3 and S5 assert absences, not just presences.&lt;/strong&gt; A scenario that only checks "the flag fired" is half a test — the audit showed exactly why (see below). Each violation scenario also proves the flag does &lt;em&gt;not&lt;/em&gt; fire on the clean variant, and S1 genuinely exercises all five flags by giving the chain a declared device and real telemetry.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;S7 proves contention, not just outcome.&lt;/strong&gt; A naive exactly-once test asserts &lt;code&gt;settled.length === 1&lt;/code&gt; after &lt;code&gt;Promise.all&lt;/code&gt; — but if the lock never actually engaged, the test passes vacuously. Ours uses a deferred executor: while the winner is in-flight, we assert the loser is &lt;em&gt;queued&lt;/em&gt; (executor unsettled, ledger unchanged), then release and assert one settle + one &lt;code&gt;pay&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwr0md3bssm2fb2w63ur4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwr0md3bssm2fb2w63ur4.webp" alt="The scenario matrix as a visual table: seven scenario rows, each with an expected-state column and a flag column for tempViolation, fakeSensor, missingEvidence, gpsAnomaly and disputed" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the adversarial audit break?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; we ran the whole matrix through an external adversarial AI audit before calling it done — verdict REQUEST CHANGES: &lt;strong&gt;0 Critical, 6 High, 7 Medium&lt;/strong&gt;. The Highs were not style complaints: the projection trusted forged events, an &lt;code&gt;"inconclusive"&lt;/code&gt; verdict silently promoted a hop to &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;verifyHop&lt;/code&gt; had no evaluator check or lock, and a concurrent &lt;code&gt;createShipment&lt;/code&gt; could mint orphan funded jobs. All six are fixed with regression tests; three Mediums were deliberately accepted and documented instead of silently patched.&lt;/p&gt;

&lt;p&gt;The deepest finding — &lt;strong&gt;H-1&lt;/strong&gt; — is the one worth remembering. The replay projection lifted state from &lt;em&gt;any&lt;/em&gt; event with a well-formed payload: a &lt;code&gt;pay&lt;/code&gt; event forged at sequence 41 for a non-plan hop index could mint a phantom paid hop, which then unlocked the next handoff. A fake handoff could even advance a hop's status without a single real delivery. The fix splits the projection into a two-pass, canonical-sequence replay: the plan binds to the &lt;code&gt;work_offer&lt;/code&gt; at &lt;code&gt;seq === 1&lt;/code&gt; (first writer wins — later offers can't redirect the payout target), and a handoff/pay/verify event lifts state only if it lands in the canonical sequence slot for its hop and stage, has an integer in-plan hop index, and is bound to the current &lt;code&gt;shipmentId&lt;/code&gt;. A forged event now lifts &lt;em&gt;nothing&lt;/em&gt; — not even status.&lt;/p&gt;

&lt;p&gt;The pipeline the fix leaves behind (diagram below): the plan binds once — to the &lt;code&gt;work_offer&lt;/code&gt; at &lt;code&gt;seq 1&lt;/code&gt; — and every event then passes the &lt;em&gt;canonical gate&lt;/em&gt;: right sequence slot for its hop and stage, an integer in-plan hop index, the bound &lt;code&gt;shipmentId&lt;/code&gt;. Passing lifts into &lt;code&gt;shipmentState&lt;/code&gt;; forged takes the drop path. Two second-order guards sit below the lift: &lt;code&gt;verify&lt;/code&gt; promotes only on an explicit &lt;code&gt;"pass"&lt;/code&gt; — &lt;code&gt;"inconclusive"&lt;/code&gt; falls to the same drop path — and &lt;code&gt;settleHop&lt;/code&gt; refuses while the hop stands disputed. A real settle emits its &lt;code&gt;pay&lt;/code&gt; event back into the same stream, which is what makes the whole read model replayable from the ledger alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdy3bmtn61my04feo268.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdy3bmtn61my04feo268.webp" alt="Flowchart of the replay pipeline: event stream → bind plan at seq 1 → canonical gate (seq slot, in-plan hop, shipmentId) → lift or drop; verify promotes only on pass; settle refused while disputed; pay events re-enter the stream" width="755" height="1132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsgcn742sl9h6495uhpbl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsgcn742sl9h6495uhpbl.webp" alt="The canonical-sequence gate: honest event chips hop0..hop3 flow through matching seq slots into the shipmentState panel, forged chips seq:99, hop:5, shp-OTHER bounce off into a rejected pool" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The other Highs were subtler trust holes: &lt;strong&gt;H-2&lt;/strong&gt; — &lt;code&gt;outcome:"inconclusive"&lt;/code&gt; (or any unknown outcome) lifted a hop to terminal &lt;code&gt;verified&lt;/code&gt;; now only an explicit &lt;code&gt;"pass"&lt;/code&gt; promotes. &lt;strong&gt;H-3&lt;/strong&gt; — &lt;code&gt;verifyHop&lt;/code&gt; was a paid-gate only: any actor could write the verdict, and it bypassed every guard &lt;code&gt;disputeHop&lt;/code&gt; had; now it runs inside the hop lock, refuses disputed hops, and requires the actor to be the evaluator (the next hop's receiver, or the client on the final hop). &lt;strong&gt;H-4&lt;/strong&gt; — &lt;code&gt;createShipment&lt;/code&gt; checked existence, created funded onchain jobs, then appended the offer with no lock — two concurrent calls could each create a funded job set; now the whole check→create→append is atomic under the shipment lock. &lt;strong&gt;H-5&lt;/strong&gt; — a settle that succeeded onchain but failed to append to the ledger diverged silently; the error now carries the job ID loudly, and a regression test pins the visible divergence. &lt;strong&gt;H-6&lt;/strong&gt; — S5's negative case and S1's "zero flags" assertion were vacuous: a GPS ref asserted by the very party it accused could never prove anomaly detection, and S1 had no declared device so &lt;code&gt;fakeSensor&lt;/code&gt; could never even run. The tests were rebuilt to be non-vacuous.&lt;/p&gt;

&lt;p&gt;The Mediums we accepted are written down, not hidden: settle doesn't compare the caller-supplied amount against the plan (the onchain budget is the backstop — flagged for the hardening epic), &lt;code&gt;acceptShipment&lt;/code&gt; is a decorative v1 marker, and all authorization is a caller-supplied actor string — fine inside the server, as long as HTTP routes derive actor from auth, which is now a documented boundary rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does v1 NOT do — the honest boundary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the matrix proves the primitive's &lt;em&gt;state machine&lt;/em&gt; is honest — it does not prove enforcement. Advisory flags warn and never block; disputes halt the chain only while the flag stands; the hop lock is single-process; and there is no per-source ledger ACL — the projection trusts that the ledger's writers are honest, which is a boundary the hardening epic owns.&lt;/p&gt;

&lt;p&gt;That honesty is the point of the exercise. A regression contract that claims more than the code does is worse than none — it's a false floor. The matrix instead pins exactly what v1 guarantees: canonical-sequence projection, evaluator-gated verification, pre-settle disputes, atomic shipment creation, exactly-once settle, and five advisory flags that fire on real violations and stay quiet on honest traffic.&lt;/p&gt;

&lt;p&gt;What's deliberately out of scope: issuer registries (who may call themselves a sensor), oracle attestations (verifying the artifact, not the reference), a real dispute engine with resolution outcomes, distributed locks for multi-instance deployment, and plan-amount binding for settle calls. Each is a named future brick, not a hidden gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you reuse the matrix?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the whole thing is one test file plus a fixture — &lt;code&gt;tests/supply-chain/scenario-matrix.test.ts&lt;/code&gt; and &lt;code&gt;tests/supply-chain/helpers/chain-fixture.ts&lt;/code&gt;. &lt;code&gt;makeChain({hops, roles, evidencePerHop, executor, lifecycle})&lt;/code&gt; builds and drives any chain shape; &lt;code&gt;failOnSettleJobId&lt;/code&gt; injects executor failures; the replay projection lives in &lt;code&gt;src/server/lib/econ-ledger/supply-chain/project.ts&lt;/code&gt;. If you're building transition-contract primitives, the pattern copies over: one file of named violations, an in-memory executor, assertions on both write and replay.&lt;/p&gt;

&lt;p&gt;The matrix is also the reason the Bedrij hypothesis can keep stacking bricks. Brick #1 proved participants compose; Brick #2 proved evidence composes. Brick #3 proves the composition holds &lt;em&gt;under&lt;/em&gt; violations — which is what makes the next layer (enforcement) a tractable problem instead of an open-ended one.&lt;/p&gt;

&lt;p&gt;Current state: &lt;strong&gt;72/72 supply-chain tests green&lt;/strong&gt;, 18/18 in the matrix itself, zero open Critical or High audit findings. The fix commit is &lt;code&gt;de2cf81&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Building agent-native supply chains? The ledger package is &lt;code&gt;@agentbadge/econ-events&lt;/code&gt; on npm, and the anchor hypothesis this proves a piece of is in &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>Brick #2: A Link Is Not a Fact — Typed Evidence in a Verifiable Supply Chain</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Fri, 06 Nov 2026 14:16:42 +0000</pubDate>
      <link>https://dev.to/spread2009/brick-2-a-link-is-not-a-fact-typed-evidence-in-a-verifiable-supply-chain-3i68</link>
      <guid>https://dev.to/spread2009/brick-2-a-link-is-not-a-fact-typed-evidence-in-a-verifiable-supply-chain-3i68</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the verifiable supply-chain series — start here: &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;, then &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1: Typed Participants&lt;/a&gt;. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A handoff event says &lt;em&gt;custody moved from 0xAA… to 0xBB…&lt;/em&gt;. The natural next question is: &lt;em&gt;and where is the proof?&lt;/em&gt; The temperature log, the GPS ping, the signed inspection photo, the weighbridge slip.&lt;/p&gt;

&lt;p&gt;Today that proof lives in an email thread, an S3 bucket, or a telematics dashboard someone copy-pastes into a dispute ticket. The transition is recorded — the evidence for it is scattered everywhere except next to the transition. And even when a link does make it into the data, a link is not a fact: it proves a pointer existed, not that what it points to is true.&lt;/p&gt;

&lt;p&gt;This is Brick #2 — the second experimental proof inside the Bedrij-series hypothesis. It proves a narrow claim: &lt;em&gt;evidence belongs to the transition.&lt;/em&gt; Typed evidence references now travel inside the &lt;code&gt;handoff&lt;/code&gt; event payload itself — the same append-only record that moves custody — and the replay model replays them with provenance annotations. One primitive, one payload, zero new endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is typed evidence in a verifiable supply chain?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; typed evidence is an optional list of references — &lt;code&gt;{type, hash, issuer, uri?}&lt;/code&gt; — attached to a &lt;code&gt;handoff&lt;/code&gt; event. &lt;code&gt;type&lt;/code&gt; is one of seven values (&lt;code&gt;signature | gps | document | api_response | image | weight | temperature&lt;/code&gt;), &lt;code&gt;hash&lt;/code&gt; is a 0x-prefixed 32-byte digest of the artifact, &lt;code&gt;issuer&lt;/code&gt; is the participant who claims the evidence, and &lt;code&gt;uri&lt;/code&gt; is an optional pointer to the artifact itself. A handoff carries 1–32 refs; the replay read model lifts them into &lt;code&gt;hops[].evidence&lt;/code&gt; per hop.&lt;/p&gt;

&lt;p&gt;The shape matters more than the fields. The &lt;code&gt;hash&lt;/code&gt; binds the transition to &lt;em&gt;specific bytes&lt;/em&gt; of proof — if the artifact changes, the hash stops matching. The &lt;code&gt;issuer&lt;/code&gt; names who asserts the claim, using the same typed-participant shape Brick #1 introduced. And &lt;code&gt;uri&lt;/code&gt; stays optional: the ledger records that proof was &lt;em&gt;referenced&lt;/em&gt;, not that it was fetched — because fetching is a trust decision each consumer makes separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does evidence travel through a handoff?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; evidence enters at write time as &lt;code&gt;handoffHop(..., evidenceRefs)&lt;/code&gt;, is validated by a strict write schema, rides inside the &lt;code&gt;handoff&lt;/code&gt; event payload, and is lifted into &lt;code&gt;ShipmentHop.evidence&lt;/code&gt; during replay — so &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt; returns it per hop, annotated with &lt;code&gt;evidenceAssertedBy&lt;/code&gt; (who attached it) and per-ref &lt;code&gt;issuerMatchesParty&lt;/code&gt; (whether the issuer is one of the two parties in that hop).&lt;/p&gt;

&lt;p&gt;This is the actual replay output from our test pack — hop 0 carries a GPS ref issued by the receiving carrier, hop 1 carries none (addresses shortened for readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shp-ev-mixed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xbbbb…bbbb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xcdcd…cdcd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xbbbb…bbbb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ipfs://QmGps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuerMatchesParty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidenceAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xaaaa…aaaa"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handed_off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xdddd…dddd"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth reading in that JSON. &lt;code&gt;issuerMatchesParty: true&lt;/code&gt; because the GPS issuer (&lt;code&gt;0xbbbb…&lt;/code&gt;) &lt;em&gt;is&lt;/em&gt; the counterparty taking custody — a hop party vouching for its own data is weaker evidence than an independent issuer would be, and the flag makes that distinction visible instead of hidden. &lt;code&gt;evidenceAssertedBy&lt;/code&gt; is the client (&lt;code&gt;0xaaaa…&lt;/code&gt;), not the carrier — the party doing the handoff attached the ref, and the projection records who did it. And hop 1 replays clean with no evidence fields at all: a mixed chain is not an error, evidence is per-transition data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9s3jezrgjq7zcoe6ja5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9s3jezrgjq7zcoe6ja5.webp" alt="Diagram: the evidence lifecycle — handoffHop write with evidenceRefs, strict write schema, append-only handoff event, lenient replay, ShipmentHop.evidence plus issuerMatchesParty and evidenceAssertedBy, per-hop evidence in the shipments API" width="276" height="1117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a reference need provenance and guardrails?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; because a reference with no provenance is a forgery primitive. Before the audit, any party could attach an evidence ref claiming it was issued by &lt;em&gt;anyone&lt;/em&gt; — a fake "independent inspector" was indistinguishable from a real one. And the strict type/hash/URI rules existed only inside one server wrapper, so every other writer path got the lenient schema.&lt;/p&gt;

&lt;p&gt;We ran the deltas through an adversarial AI audit before shipping: &lt;strong&gt;0 Critical, 2 High, 7 Medium findings&lt;/strong&gt;. The Highs were exactly the trust-boundary bugs: &lt;strong&gt;H-1&lt;/strong&gt; — no persisted provenance and an inverted advisory (it warned on the honest case and stayed quiet on the forged one); &lt;strong&gt;H-2&lt;/strong&gt; — strict validation lived in &lt;code&gt;parseHandoffEvidence&lt;/code&gt; only, while the registered payload schema and the replay projection stayed lenient, so a forged event replayed into public state. Both are closed: &lt;code&gt;buildEconomicEvent&lt;/code&gt; now validates handoff evidence on the write path by default, and replay annotates rather than trusts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pba1bgyjyigxgdbxhk2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pba1bgyjyigxgdbxhk2.webp" alt="Audit diagram: a forged issuer chip flagged by an amber advisory beacon that warns without blocking; strict write gate versus lenient read gate; canonical-sequence gate rejecting a forged event" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the audit actually change in the code?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a write/read schema split, URI hardening, and three guardrails. &lt;code&gt;buildEconomicEvent(input, {mode})&lt;/code&gt; defaults to strict write validation and offers &lt;code&gt;mode: "replay"&lt;/code&gt; for stored events. Evidence URIs are allowlisted to &lt;code&gt;http(s)/ipfs/ar&lt;/code&gt;, reject credentials and control characters, and canonicalize to &lt;code&gt;new URL(u).href&lt;/code&gt;. The evidence list is pre-checked for cardinality (1–32) before per-item parsing, advisories are aggregated per write, and error echoes are bounded.&lt;/p&gt;

&lt;p&gt;The schema split follows the same rule Brick #1 learned: &lt;strong&gt;on an append-only log, the read contract can only grow more permissive.&lt;/strong&gt; The &lt;em&gt;read&lt;/em&gt; schema keeps an open evidence type — a future type added by a newer writer must not brick replay for older clients — while the &lt;em&gt;write&lt;/em&gt; schema closes on the seven known types. The replay projection adds a belt on top: it &lt;code&gt;safeParse&lt;/code&gt;s each stored payload and gates claim-lifting on the canonical event sequence, so a forged event with a well-formed payload but a wrong sequence number never lands in the public read model.&lt;/p&gt;

&lt;p&gt;Current state: &lt;strong&gt;118 package tests&lt;/strong&gt; and &lt;strong&gt;35 supply-chain server tests&lt;/strong&gt; green, zero open Critical/High findings, &lt;code&gt;@agentbadge/econ-events@0.1.3&lt;/code&gt; published to npm. One finding was deliberately accepted rather than fixed: the hop lock is single-process — multi-instance deployments need a distributed lock, documented as a known limitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What evidence is NOT — the honest boundary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; evidence is not truth, not verification, and not enforcement. A hash proves &lt;em&gt;these bytes existed&lt;/em&gt;, not that they are accurate. An issuer is a self-declared identity — the same advisory layer Brick #1 established, not a registry lookup. A URI is fetched by nobody: the ledger stores the pointer, consumers decide whether to resolve it. And advisories are log warnings, not verdicts — a third-party issuer flags &lt;code&gt;issuerMatchesParty: false&lt;/code&gt;; it does not block the handoff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8x5buvf0lo62ev1n86.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8x5buvf0lo62ev1n86.webp" alt="Schema card: handoff event payload.evidence fields on the left, replay arrow, hops[].evidence projected read state with issuerMatchesParty and evidenceAssertedBy annotations on the right" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is deliberate. Brick #2 proves the &lt;em&gt;shape&lt;/em&gt; of the claim — "evidence travels with the transition" — not the verification of the artifact. The interesting follow-ups are the missing layers: issuer registries, countersigned evidence (where the named issuer must consent), oracle attestations that verify the artifact itself, and on-chain anchors for the hash. Those are different bricks.&lt;/p&gt;

&lt;p&gt;If you're building agent-native supply chains: the schema package is &lt;code&gt;@agentbadge/econ-events&lt;/code&gt;, the API is &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt;, and the anchor hypothesis this proves a piece of is in &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;, with the participant layer underneath it in &lt;a href="https://agentbadge.xyz/blog/supply-chain-participant-roles" rel="noopener noreferrer"&gt;Brick #1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>Bots Are Already Knocking — We Counted, Then Rebuilt the Door for Machines</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:43:52 +0000</pubDate>
      <link>https://dev.to/spread2009/bots-are-already-knocking-we-counted-then-rebuilt-the-door-for-machines-1kn8</link>
      <guid>https://dev.to/spread2009/bots-are-already-knocking-we-counted-then-rebuilt-the-door-for-machines-1kn8</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the Arc Campaign series — earlier: &lt;a href="https://agentbadge.xyz/blog/arc-c14-agent-discovery" rel="noopener noreferrer"&gt;C14 — what an agent can read on your homepage&lt;/a&gt;, &lt;a href="https://agentbadge.xyz/blog/arc-c32-payment-forensics" rel="noopener noreferrer"&gt;C32 — payment-graph forensics&lt;/a&gt;, &lt;a href="https://agentbadge.xyz/blog/arc-c30-agent-economy" rel="noopener noreferrer"&gt;C30 — agent economy docs loop&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our access logs told a story we were ignoring. In one 24-hour window: &lt;strong&gt;587 requests&lt;/strong&gt; from ElevenLabs' agent, &lt;strong&gt;42&lt;/strong&gt; from MoneyAgentPoller polling &lt;code&gt;/api/venue/jobs&lt;/code&gt;, &lt;strong&gt;18&lt;/strong&gt; hits on &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;, &lt;strong&gt;92&lt;/strong&gt; requests against &lt;code&gt;/mcp&lt;/code&gt; and tool discovery, and &lt;strong&gt;23&lt;/strong&gt; smoke probes from APEX-Watchtower — bots that found the API surface mostly by accident.&lt;/p&gt;

&lt;p&gt;They were knocking on a door built for humans. So we rebuilt the entire entry path for machines — descriptor, read layer, pricing, capability feed, and a $0.001 paid endpoint that lets any agent complete a real payment roundtrip in one request. Every claim below links to a URL you can curl right now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqw4jkn7a5vkt7ft0rhde.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqw4jkn7a5vkt7ft0rhde.webp" alt="A queue of small robot agents at a glowing cyan doorway; through the door the interior is structured JSON brackets in cyan and green" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR for agents:&lt;/strong&gt; descriptor &lt;code&gt;/.well-known/agentbadge.json&lt;/code&gt;, free work list &lt;code&gt;/api/tasks/open&lt;/code&gt;, prices &lt;code&gt;/api/v1/services&lt;/code&gt;, changes &lt;code&gt;/feed.json&lt;/code&gt; (typed capability items), payment proof &lt;code&gt;/api/v1/demo/scan-lite?url=&lt;/code&gt; at $0.001 x402.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is already knocking on agentbadge.xyz?
&lt;/h2&gt;

&lt;p&gt;Measured traffic from our own logs, 24-hour window — no projections, no surveys: ElevenLabs' agent made 587 requests (page content and assets); MCP and tool discovery drew 92 calls; MoneyAgentPoller hit &lt;code&gt;/api/venue/jobs&lt;/code&gt; 42 times looking for paid work; APEX-Watchtower sent 23 smoke probes; and &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt; was fetched 18 times.&lt;/p&gt;

&lt;p&gt;That is not a trickle — it is a steady line of machines doing reconnaissance. Some were friendly (indexers, watchdogs), some were customers-in-waiting (job pollers looking for paid work). All of them hit the same problem: our surface was readable, but only if you already knew where to look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm01v6sw1282asbi75a7s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm01v6sw1282asbi75a7s.webp" alt="Traffic dashboard: five horizontal request-count bars in descending cyan and green, each capped by a distinct bot glyph" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does an agent need before it can pay you?
&lt;/h2&gt;

&lt;p&gt;An agent arriving cold needs four things before it can spend money on your API: &lt;strong&gt;discovery&lt;/strong&gt; — a machine-readable descriptor saying what exists; &lt;strong&gt;evaluation&lt;/strong&gt; — enough free data to decide whether the service is worth anything; &lt;strong&gt;pricing&lt;/strong&gt; — exact machine-readable prices, not a marketing page; and &lt;strong&gt;a cheap proof&lt;/strong&gt; — a low-risk paid call that proves the payment rail works end-to-end. We audited our own site against that checklist and found we were shipping exactly half of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbrctku7rsz6levb9biw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbrctku7rsz6levb9biw.webp" alt="Machine surface tree: agentbadge.xyz root splits into well-known, pricing, feed and paid branches" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; &lt;code&gt;/.well-known/agentbadge.json&lt;/code&gt; returns the deployment descriptor — network, API version, payment protocol, service entry points — in one fetch. An agent never scrapes HTML to figure out what we are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation.&lt;/strong&gt; &lt;code&gt;/api/tasks/open&lt;/code&gt; lists open marketplace tasks with budgets and capability tags — no auth, no signup, JSON only. An agent can judge whether our marketplace has real work before registering anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing.&lt;/strong&gt; &lt;code&gt;/pricing.json&lt;/code&gt; and &lt;code&gt;/api/v1/services&lt;/code&gt; serve the same ServiceSku registry — one source of truth for every paid surface with exact USDC amounts and input schemas. No "contact us for pricing" dead-ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change detection.&lt;/strong&gt; &lt;code&gt;/feed.json&lt;/code&gt; is a JSON Feed 1.1 where capability items carry typed fields — &lt;code&gt;_type:"capability"&lt;/code&gt;, &lt;code&gt;_endpoint&lt;/code&gt;, &lt;code&gt;_method&lt;/code&gt;, &lt;code&gt;_price&lt;/code&gt;, &lt;code&gt;_added_at&lt;/code&gt;. A monitoring agent diffs the feed and knows within minutes when a new endpoint or price lands — no blog-scraping required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fczd5wfrozipkjsos5ano.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fczd5wfrozipkjsos5ano.webp" alt="A JSON Feed capability item rendered as structured JSON inspected by a small robot agent under a magnifier" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlwgsfpc1wyr2kbttegs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlwgsfpc1wyr2kbttegs.webp" alt="Diagram: the machine surface of agentbadge.xyz — well-known descriptors, keyless reads, pricing registry, capability feed, x402-paid calls and signed writes all hang off the same origin" width="423" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you prove payment works without asking for trust?
&lt;/h2&gt;

&lt;p&gt;With a $0.001 proof. &lt;code&gt;GET /api/v1/demo/scan-lite?url=&amp;lt;page&amp;gt;&lt;/code&gt; is the cheapest x402-gated call we could ship: the agent pays one-tenth of one cent of USDC on Arc and gets back a real one-page agent-readiness probe — og/meta extraction, llms.txt/JSON-LD/agents.txt signal checks, and an &lt;code&gt;agent_ready_hint&lt;/code&gt; verdict.&lt;/p&gt;

&lt;p&gt;A bot evaluating whether our rails work does not need to trust our docs — it spends $0.001 and observes the 402→sign→settle→200 roundtrip itself. If the roundtrip works for a dime-fraction, the same rail works for a $5 task claim or a $50 service pass. The endpoint is priced from &lt;code&gt;DEMO_SCAN_USDC&lt;/code&gt; in atomic units and the same constant feeds the ServiceSku registry — the catalog can never advertise a different price than the gate charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did we learn about two-step conversion?
&lt;/h2&gt;

&lt;p&gt;The pattern that emerged is &lt;strong&gt;anonymous read first, paid or signed action second&lt;/strong&gt; — and the two steps must share the same URLs. Step 1 is free and keyless: descriptor, task list, pricing, feed. An agent evaluates everything without a wallet signature — the same way a human reads a menu before ordering. Step 2 is the money or signature moment: the $0.001 scan-lite call, a wallet-signed &lt;code&gt;POST /api/tasks&lt;/code&gt;, an x402-gated scan pack.&lt;/p&gt;

&lt;p&gt;The key detail: the step-2 response format is identical to step-1 — same JSON shapes, same error catalog with machine-readable recovery hints. An agent that learned the read path already knows how to parse the paid path. We also learned to mark capability changes in the feed rather than only the blog. Blog posts are for humans; &lt;code&gt;{"_type":"capability"}&lt;/code&gt; items are for the agents that actually call the API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2m7n3rrzdax22swelxrm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2m7n3rrzdax22swelxrm.webp" alt="Two-step conversion corridor: anonymous read gate, $0.001 payment gate, signed write gate" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetqxjulic5lpfx3deap.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetqxjulic5lpfx3deap.webp" alt="Diagram: two-step agent conversion — deployment descriptor leads to free keyless reads, then a $0.001 x402 payment proof, then wallet-signed writes" width="457" height="898"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the logs say 24 hours later?
&lt;/h2&gt;

&lt;p&gt;The window above was the &lt;em&gt;before&lt;/em&gt; picture — the night we counted the knocking and started rebuilding. We re-ran the same aggregation one day after the machine surface went live. Same window size, same log source, different picture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caller&lt;/th&gt;
&lt;th&gt;Before (Nov 3→4)&lt;/th&gt;
&lt;th&gt;After (Nov 5→6)&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total requests&lt;/td&gt;
&lt;td&gt;1,495&lt;/td&gt;
&lt;td&gt;7,389&lt;/td&gt;
&lt;td&gt;×4.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs agent&lt;/td&gt;
&lt;td&gt;587&lt;/td&gt;
&lt;td&gt;2,775&lt;/td&gt;
&lt;td&gt;×4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoneyAgentPoller → &lt;code&gt;/api/venue/jobs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;×5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;194&lt;/td&gt;
&lt;td&gt;×10.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/.well-known/agent-registration.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;×13.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP + tool discovery&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;487&lt;/td&gt;
&lt;td&gt;×5.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facebook link previews&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;318&lt;/td&gt;
&lt;td&gt;×5.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three wins stand out beyond the growth curve. &lt;strong&gt;Bots found the paywall within hours of deploy&lt;/strong&gt;: eight requests hit the paid &lt;code&gt;/api/v1/demo/scan-lite&lt;/code&gt; and received the 402 challenge — the two-step path working exactly as designed, free discovery first, paid proof second. &lt;strong&gt;Manifest reads grew faster than page views&lt;/strong&gt;: agent-card and agent-registration fetches are up 11–14× — agents now arrive through the descriptor instead of scraping HTML. And &lt;strong&gt;the tagged funnel produced its first data&lt;/strong&gt;: 23 &lt;code&gt;?src=&lt;/code&gt; attributions landed on &lt;code&gt;/services/scanner&lt;/code&gt; and &lt;code&gt;/scan&lt;/code&gt;. Bonus signal: eight probes on &lt;code&gt;/.well-known/x402&lt;/code&gt; — agents are already asking for the next descriptor we ship.&lt;/p&gt;

&lt;p&gt;Volume is vanity; job-polling and manifest fetches growing 5–10× while paid-path probes appear on day one is the signal. Every number in this table is curlable yourself: &lt;code&gt;GET /api/meta/traffic-report?hours=24&lt;/code&gt; — the same aggregation our daily report reads from the &lt;code&gt;http_traffic&lt;/code&gt; event store.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8qi5gg432hvdnrzkl5r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8qi5gg432hvdnrzkl5r.webp" alt="Before/after request-count chart: dim grey before bars beside bright cyan and green after bars roughly five times taller, a small green PAID badge, and a robot planting a flag on the tallest bar" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still on the road map?
&lt;/h2&gt;

&lt;p&gt;The remaining EPIC-206 work: AI-crawler nourishment — robots.txt rules that feed crawlers instead of blocking them, an llms-full.txt with complete capability docs, and onchain-proof earnings pages (earnings.md, post-task.md) so agents can cite real task payouts when recommending our marketplace. Then the adversarial audit gate on the payment write-path, and public verification data on how many anonymous reads convert to paid calls — the Step1→Step2 metric we now instrument for.&lt;/p&gt;

&lt;p&gt;We will publish those numbers when they exist. That is the rule we follow in this series: no invented metrics, only links you can curl.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI Agents
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"what"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Machine-readable entry path for agent commerce: deployment descriptor, keyless read layer, ServiceSku pricing registry, typed capability feed, $0.001 x402 payment proof — plus a live public traffic-report endpoint as evidence."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"where"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agentbadge.xyz — /.well-known/agentbadge.json, /api/tasks/open, /api/v1/services, /pricing.json, /feed.json, /api/v1/demo/scan-lite, /api/meta/traffic-report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anonymous read first (keyless JSON), paid or signed action second — identical response shapes and machine-readable error catalog across both steps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verify"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET /api/meta/traffic-report?hours=24 returns the same http_traffic aggregation cited in this article; every endpoint URL is curlable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"writes require EIP-191 wallet signature; scan-lite is x402-gated at $0.001 USDC on Arc"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>agents</category>
      <category>arc</category>
      <category>machinecommerce</category>
    </item>
    <item>
      <title>Brick #1: Any Node Can Hand Off — Typed Participants in a Verifiable Supply Chain</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Tue, 03 Nov 2026 19:56:56 +0000</pubDate>
      <link>https://dev.to/spread2009/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain-415g</link>
      <guid>https://dev.to/spread2009/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain-415g</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the verifiable supply-chain series — start here: &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the smallest broken thing in a supply chain: a &lt;code&gt;handoff&lt;/code&gt; event that says &lt;em&gt;custody moved from 0xAA… to 0xBB…&lt;/em&gt; — and nothing else.&lt;/p&gt;

&lt;p&gt;Which of those addresses is a company? Which is a forklift's telematics unit, a dock-scheduling agent, a cold-chain probe? The ledger knows custody moved. It does not know &lt;strong&gt;who&lt;/strong&gt; held the box — org, agent, or device. If the answer to that question lives only in somebody's spreadsheet, the chain of custody is verifiable and meaningless at the same time.&lt;/p&gt;

&lt;p&gt;This is Brick #1 — the first experimental proof inside the Bedrij-series hypothesis. It proves a narrow claim: &lt;em&gt;transitions can be performed by nodes of any type, in a single primitive.&lt;/em&gt; Orgs, agents and devices now travel the same &lt;code&gt;handoff&lt;/code&gt; event, and the read model replays them with provenance. No new endpoint, no new contract — four optional slots on a payload that already existed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjbevqswhgzw9y8flszj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjbevqswhgzw9y8flszj.webp" alt="The handoff: a package passing between an ORG building, a DEVICE robot arm and an AGENT glyph" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are participant roles in a verifiable supply chain?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; participant roles are optional, typed declarations of &lt;em&gt;who participated in a hop&lt;/em&gt; — &lt;code&gt;{ custodian, operator, device, sensor }&lt;/code&gt; — carried inside the &lt;code&gt;handoff&lt;/code&gt; event payload and surfaced by the replay read model. Each role is an address, one of seven types (&lt;code&gt;org | agent | device | robot | drone | service | human&lt;/code&gt;), and an optional display label. They are self-declared by the party doing the handoff — advisory metadata, not identity, and never consulted by any authorization or payment guard.&lt;/p&gt;

&lt;p&gt;The distinction matters more than it looks. Our custody model is enforced by addresses: &lt;code&gt;counterparty&lt;/code&gt; must equal the plan's receiver, &lt;code&gt;actor&lt;/code&gt; must equal the current custody holder. Roles deliberately sit &lt;em&gt;above&lt;/em&gt; that — they describe the real-world shape of the participants without pretending the chain verified it. A forklift can be listed as &lt;code&gt;device&lt;/code&gt; and a scheduling service as &lt;code&gt;agent&lt;/code&gt;; the ledger records who claimed it, not who approved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do roles travel through a handoff event?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; roles enter at write time (&lt;code&gt;handoffHop(…, roles)&lt;/code&gt;), are validated by a strict write schema, ride inside the &lt;code&gt;handoff&lt;/code&gt; event's payload, and are lifted into &lt;code&gt;ShipmentHop.roles&lt;/code&gt; during replay — so &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt; returns them per hop with &lt;code&gt;rolesAssertedBy&lt;/code&gt; naming who made the claim. Events written before roles existed replay unchanged: every field is optional.&lt;/p&gt;

&lt;p&gt;This is the actual replay output from our test pack — two hops, one with a company custodian plus device and sensor, one with a custodian and an agent operator (addresses shortened for readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shp-brick1-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xbbbb…bbbb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"custodian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xbbbb…bbbb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TransMar Logistics"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xcccc…cccc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Forklift-7 telematics"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sensor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xdddd…dddd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cold-chain probe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rolesAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xaaaa…aaaa"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hopIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handed_off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"receiver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xeeee…eeee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"custodian"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xeeee…eeee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Eindhoven Cold Hub"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xcccc…cccc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dock-scheduler agent"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rolesAssertedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xbbbb…bbbb"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth reading in that JSON. The same address &lt;code&gt;0xcccc…&lt;/code&gt; plays &lt;code&gt;device&lt;/code&gt; on hop 0 and &lt;code&gt;agent&lt;/code&gt; on hop 1 — a type is a claim about a role in &lt;em&gt;this&lt;/em&gt; hop, not a global identity. &lt;code&gt;rolesAssertedBy&lt;/code&gt; moves: the client asserted hop 0's roles, the carrier asserted hop 1's. And hop 1's handoff works even though hop 0 already settled — roles are per-transition data, not per-shipment configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u8ljowk8gq9pprzdkue.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u8ljowk8gq9pprzdkue.webp" alt="Diagram: the role lifecycle — handoffHop write with optional role slots → strict write schema → append-only handoff event → lenient replay → ShipmentHop.roles + rolesAssertedBy → per-hop roles in the shipments API" width="552" height="2150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The lifecycle of a role claim: asserted at write time by the party doing the handoff, validated by a strict schema, stored in the append-only log, then lifted by a lenient replay into per-hop state — with rolesAssertedBy recording who made the claim. No guard ever consumes it: custody still answers only to counterparty and actor.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do self-declared roles need provenance?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; because an unverified claim displayed as structured truth is indistinguishable from a verified one. A hander-off could label any third party "Maersk" and a consumer reading only &lt;code&gt;hops[].roles&lt;/code&gt; would never know it was self-reported. We fixed that by writing the &lt;em&gt;asserting party&lt;/em&gt; into the derived model — &lt;code&gt;rolesAssertedBy&lt;/code&gt; — and by enforcing that &lt;code&gt;custodian.address&lt;/code&gt; must equal the handoff &lt;code&gt;counterparty&lt;/code&gt;, since custodian is the slot whose meaning the ledger already defines.&lt;/p&gt;

&lt;p&gt;That fix came from an adversarial review, not foresight. We ran the changes through an AI audit before shipping: &lt;strong&gt;0 Critical, 4 High, 6 Medium findings&lt;/strong&gt; on the first pass. The Highs were exactly the trust-boundary bugs you'd want caught — roles presented as structured truth with no provenance (H-1), &lt;code&gt;custodian&lt;/code&gt; able to contradict the custody model (H-2), a repeat handoff silently rewriting roles (H-3), and an &lt;code&gt;epcis&lt;/code&gt; object able to smuggle keys that overrode guarded payload fields (H-4). All four are now guarded, and the re-audit came back &lt;strong&gt;APPROVE WITH NITS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7feu13ej381l1xasy5an.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7feu13ej381l1xasy5an.webp" alt="Schema card: a handoff event with four role slots and seven type chips" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the audit actually change in the code?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; four behavioral changes and a schema split. Write-side parsing is now strict — unknown role slots or keys fail loudly instead of silently stripping; addresses normalize to lowercase; &lt;code&gt;label&lt;/code&gt; rejects control, bidi and zero-width characters and must be non-blank; &lt;code&gt;custodian&lt;/code&gt; must equal the counterparty; a hop already handed off rejects re-handoff; and the payload's guarded fields (&lt;code&gt;shipmentId&lt;/code&gt;, &lt;code&gt;hopIndex&lt;/code&gt;) are written last so a caller-supplied object can't overwrite them. The read schema stays lenient — stored events always replay.&lt;/p&gt;

&lt;p&gt;The schema split is the part I'd highlight for anyone building on append-only logs: the &lt;em&gt;write&lt;/em&gt; schema and the &lt;em&gt;read&lt;/em&gt; schema are now different objects. &lt;code&gt;ParticipantRolesWriteSchema&lt;/code&gt; is strict (&lt;code&gt;z.strictObject&lt;/code&gt;, normalization, consistency refines). &lt;code&gt;ParticipantRoleSchema&lt;/code&gt; — the one inside &lt;code&gt;HandoffPayloadSchema&lt;/code&gt; and &lt;code&gt;ShipmentHopSchema&lt;/code&gt; — is deliberately lenient, because tightening a rule tomorrow must never make a stored event unparseable. An append-only log means your read contract can only grow more permissive over time.&lt;/p&gt;

&lt;p&gt;Current state: 76 package tests and 40 server tests green, zero open Critical/High findings, two audit reports committed alongside the code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1ag960dj8ox0pl5kh5e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1ag960dj8ox0pl5kh5e.webp" alt="Audit loop: H-1..H-4 findings on the left, guarded ledger stamped APPROVE on the right" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What roles are NOT — the honest boundary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; roles are not a registry, not a delegation mechanism, and not a signature scheme. Nothing checks that "Eindhoven Cold Hub" is who it says it is; &lt;code&gt;rolesAssertedBy&lt;/code&gt; is only as strong as the authentication on the write path; &lt;code&gt;label&lt;/code&gt; is display text and a determined attacker can still craft look-alike strings. If your application needs &lt;em&gt;verified&lt;/em&gt; participant identity, roles are the wrong layer — they're the substrate such a verification layer would annotate.&lt;/p&gt;

&lt;p&gt;That's deliberate. Brick #1 proves the &lt;em&gt;shape&lt;/em&gt; of the claim — "transitions can name nodes of any type" — not the verification of the claim. The interesting follow-ups are exactly the missing layers: a participant registry, countersigned roles (where the named party must consent), and device attestations. Those are different bricks.&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI Agents
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"what"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Typed participant roles (custodian/operator/device/sensor) on handoff events in an append-only economic ledger, replayed into per-hop read state with provenance."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"where"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packages/econ-events (ParticipantRoleSchema, ParticipantRolesSchema, ParticipantRolesWriteSchema); hackathon/server supply-chain pack (handoffHop, shipmentState); GET /api/ledger/shipments/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roles optional at every level; write path strict (unknown keys reject, addresses lowercase, custodian == counterparty, one type per address); read path lenient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verify"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"76 package + 40 server tests green; adversarial AI audit REQUEST CHANGES → fixes → re-audit APPROVE WITH NITS; findings + fixes committed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roles are self-declared, not verified identity; single-process hop lock; labels are display-only"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building agent-native supply chains: the schema package is &lt;code&gt;@agentbadge/econ-events&lt;/code&gt;, the API is &lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt;, and the design docs for this brick live in &lt;code&gt;docs/EPICS/212-supply-chain-participant-roles/&lt;/code&gt;. The anchor hypothesis this proves a piece of is in &lt;a href="https://agentbadge.xyz/blog/bedrij-principle" rel="noopener noreferrer"&gt;The Bedrij Principle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>The Bedrij Principle: One Primitive for Infinite Agent Supply Chains</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Thu, 29 Oct 2026 20:19:53 +0000</pubDate>
      <link>https://dev.to/spread2009/the-bedrij-principle-one-primitive-for-infinite-agent-supply-chains-3jgc</link>
      <guid>https://dev.to/spread2009/the-bedrij-principle-one-primitive-for-infinite-agent-supply-chains-3jgc</guid>
      <description>&lt;p&gt;&lt;em&gt;From "One" to Verifiable Economic Coordination&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is a strange question we have been asking ourselves: what if supply chains don't need more integrations?&lt;/p&gt;

&lt;p&gt;Every serious supply-chain project we have seen follows the same pattern. Another ERP connector. Another IoT gateway. Another document standard, another API adapter, another trust mechanism bolted on between two parties who still don't quite trust each other. The integration graph grows forever, and every new participant makes it worse.&lt;/p&gt;

&lt;p&gt;What if we are looking at the problem from the wrong direction? Instead of adding one more integration, what if we searched for one small primitive from which the complexity could be composed?&lt;/p&gt;

&lt;p&gt;This article is our first public statement of that line of thinking — a hypothesis, a primitive, and the v0.1 experiment we are already running on it. We call the exploration &lt;em&gt;the Bedrij Principle&lt;/em&gt;, for reasons that will become clear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbg7x169e2000iiwpxo3p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbg7x169e2000iiwpxo3p.webp" alt="ONE → MANY: a single verifiable-transition primitive composing goods, data and agents into a verified economy" width="784" height="1168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  01 — The strange question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; supply chains fail at the joins, not the nodes. Every participant runs different systems — different APIs, databases, IoT devices, documents, authentication, business rules, payment rails — and the "supply chain" is really an integration graph that grows quadratically with every new participant.&lt;/p&gt;

&lt;p&gt;A traditional picture looks linear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manufacturer → Carrier → Warehouse → Distributor → Customer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically, it is anything but. Each arrow is a bespoke project: custom EDI mappings, CSV exchanges over email, proprietary telematics, manual confirmation calls, invoice reconciliation. The work that makes a chain &lt;em&gt;work&lt;/em&gt; is not the moving of goods — it is the constant translation between systems that were never designed to meet.&lt;/p&gt;

&lt;p&gt;So the question isn't "which integration do we build next?" It's: &lt;strong&gt;is there a smaller question underneath all of them?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  02 — Why "1"?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; because someone already asked a structurally identical question in a much harder domain. Orest Bedrij spent decades searching for a fundamental "1" — a single underlying principle from which the many phenomena of nature could be understood, predicted and verified.&lt;/p&gt;

&lt;p&gt;Bedrij approached nature through a search for a fundamental "1". His published work — including a 2002 paper in the &lt;em&gt;Proceedings of the Institute of Mathematics of the National Academy of Sciences of Ukraine&lt;/em&gt; — describes that "1" through what he called the Principle of the One-and-the-Many, built on fundamental constants and high-precision measurements.&lt;/p&gt;

&lt;p&gt;We are not extending his physics, and we are not claiming his theory is established science. What we are doing is borrowing a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can complexity be composed from a sufficiently fundamental primitive?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is also a historical footnote that makes the rhyme hard to ignore. Bedrij was not only a theoretical thinker — he was a physicist and an investment banker, and per his Fetzer Institute biography, at age 29 he served as IBM's technical director on the computer complex behind the first soft landing on the Moon at NASA/JPL. Physics, computing, finance and philosophy on one résumé. Our version is narrower but rhymes: AI, software, cryptography, finance and physical supply chains.&lt;/p&gt;

&lt;p&gt;The structural parallel is what earned him a place in this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEDRIJ                    AGENTBADGE
One                       One primitive
↓                         ↓
Many                      Many transitions
↓                         ↓
Relationships             Relationships
↓                         ↓
Measurement               Evidence
↓                         ↓
Verification              Verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We noticed a structural similarity in how the problem is posed. That is the whole claim — nothing more, and we think it's enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  03 — Our question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; what is the smallest useful unit of a supply chain? Not the shipment, not the participant, not the payment, not the API call — but something all of them are built from.&lt;/p&gt;

&lt;p&gt;We went looking for the smallest unit that still does real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It isn't a &lt;strong&gt;shipment&lt;/strong&gt; — a shipment is a composition, not a unit.&lt;/li&gt;
&lt;li&gt;It isn't a &lt;strong&gt;participant&lt;/strong&gt; — participants don't move anything by existing.&lt;/li&gt;
&lt;li&gt;It isn't a &lt;strong&gt;payment&lt;/strong&gt; — payment is one consequence, not the event itself.&lt;/li&gt;
&lt;li&gt;It isn't an &lt;strong&gt;API call&lt;/strong&gt; — an API call is a transport detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our candidate is this: &lt;strong&gt;a transition whose result can be independently verified.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        State A
          │
          │  transition
          │
          ├── expected conditions
          ├── action
          ├── evidence
          ├── verification
          └── acceptance
          ↓
        State B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is our "1". Everything else in this article — and in the system — is composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  04 — The answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a &lt;em&gt;verifiable state transition&lt;/em&gt;. Not a transaction — a transition: a change in the state of the world whose preconditions, action, evidence, verification and acceptance are all explicit and checkable.&lt;/p&gt;

&lt;p&gt;Why not "transaction"? Because the words we already have are all partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment    ≠  Delivery
Delivery   ≠  Acceptance
Acceptance ≠  Proof
Proof      ≠  Settlement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transaction is a financial act. A supply chain is made of changes in the state of the world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRODUCED → PACKED → DISPATCHED → IN TRANSIT → ARRIVED → INSPECTED → ACCEPTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Settlement is a &lt;em&gt;consequence&lt;/em&gt; of certain transitions — not the essence of any of them. Once you see this, "paying for a shipment" stops looking like one event and starts looking like a chain of verified state changes, some of which happen to release money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis: a complex economic system can be composed from verifiable state transitions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Supply chain is the first application. The agent economy is the horizon.&lt;/p&gt;

&lt;h2&gt;
  
  
  05 — The transition contract
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; each transition is governed by a &lt;em&gt;transition contract&lt;/em&gt; — a small explicit agreement over what must be true before, what happens, what counts as proof, who verifies, and what follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRECONDITIONS
     ↓
ACTION
     ↓
EVIDENCE REQUIREMENTS
     ↓
VERIFICATION RULES
     ↓
ACCEPTANCE
     ↓
SETTLEMENT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the term we will use throughout the series, and it carries the article's central sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A supply chain is not a sequence of transactions. It is a sequence of transition contracts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transition contract is where a business rule actually lives. "Deliver 1,000 units at 2–8°C with the seal intact by 14:00" is not a paragraph in a PDF here — it is the preconditions and evidence requirements of one transition. If the evidence doesn't satisfy the rules, the state doesn't move, and nothing downstream — including money — fires.&lt;/p&gt;

&lt;p&gt;In code, the whole idea fits in a type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TransitionContract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;preconditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// what must be true before&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// what physically/logically happens&lt;/span&gt;
  &lt;span class="nl"&gt;evidenceRequired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;   &lt;span class="c1"&gt;// what counts as proof&lt;/span&gt;
  &lt;span class="nl"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onAccept&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Settlement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// what fires after acceptance&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One shape. Pharma cold chain, data delivery, a service performed by an agent — same contract, different contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  06 — The chain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; compose transitions and you get a chain — &lt;code&gt;T₁ → T₂ → T₃ → T₄ → …&lt;/code&gt; — of arbitrary length, where every link is independently verifiable and the protocol doesn't change as the chain grows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manufacturer
     │
    T₁
     ↓
Carrier
     │
    T₂
     ↓
Warehouse
     │
    T₃
     ↓
Distributor
     │
    T₄
     ↓
Customer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;Tₙ&lt;/code&gt; expands the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tₙ = Expected + Action + Evidence + Verification + Acceptance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain can be four transitions or four hundred. No new participant needs a new &lt;em&gt;kind&lt;/em&gt; of protocol — only new conditions, actions and evidence inside the same shape. That is the engineering content of "one primitive, arbitrary-length supply chains": the complexity lives in the configuration, not in the mechanism.&lt;/p&gt;

&lt;p&gt;We deliberately say &lt;em&gt;arbitrary-length&lt;/em&gt; inside the text, even though "infinite" sits in the title. The claim is composability, not magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  07 — The real world
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a supply chain happens outside software — trucks move, seals break, temperatures drift, machines produce parts. The model survives only if evidence can flow from physical reality into the transition, whatever produces it: IoT sensors, humans, machines, or agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHYSICAL WORLD
      │
      ↓
   OBSERVATION
      │
      ↓
    EVIDENCE
      │
      ↓
 CRYPTOGRAPHIC PROOF
      │
      ↓
   VERIFICATION
      │
      ↓
    NEW STATE
      │
      ↓
   SETTLEMENT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two architectural commitments follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence belongs to the transition, not the shipment.&lt;/strong&gt; Evidence isn't metadata stapled to a container — it is a structural part of &lt;code&gt;Tₙ&lt;/code&gt;, alongside conditions, action, verifier and result. A transition without its evidence is unfinished, not "a shipment with missing docs."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The primitive is infrastructure-agnostic.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            VERIFIABLE TRANSITION
                    │
      ┌─────────────┼─────────────┐
      ↓             ↓             ↓
 Blockchain       IoT            AI
      │             │             │
 settlement     evidence      action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A blockchain is one possible settlement and attestation rail — useful, verifiable, ours — but not a condition of the model. IoT is one possible evidence source. AI agents are one possible actor. Swap any of them and the transition contract still holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  08 — The economic loop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; proof triggers settlement, not the other way around. When verification passes and the receiver accepts, the economic consequence — escrow release — is a mechanical outcome of the transition, not a separate negotiation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proof → acceptance → settlement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inverts the usual instinct. Payment is not the foundation of trust in the chain; it is the last step of a verified transition. Money moves &lt;em&gt;because&lt;/em&gt; a state change was proven, which is a very different thing from money moving and everyone hoping the state changed.&lt;/p&gt;

&lt;p&gt;It also means the economic layer composes the same way the custody layer does: each &lt;code&gt;Tₙ&lt;/code&gt; can carry its own settlement rule, and a long chain is just settlements firing in order as evidence accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  09 — A real shipment
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; here's the whole idea as configuration. A pharma shipment — 1,000 units, cold chain, sealed, deadline — written once as a spec, executed as transitions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SHIP-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manufacturer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"work"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pickup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"seal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"intact"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warehouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"work"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"deadline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14:00"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The chain becomes data.&lt;/strong&gt; An arbitrary supply chain is not a hard-coded workflow — it is configuration plus rules plus evidence plus transitions.&lt;/p&gt;

&lt;p&gt;And on the wire, the transitions are ordinary, inspectable events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;work_offer
    ↓
handoff
    ↓
pay
    ↓
verify
    ↓
work_offer   ← next hop
    ↓
handoff
    ↓
pay
    ↓
verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each loop is one transition contract executing: terms offered, custody handed off, escrow released, outcome verified. Repeat per hop until the goods reach the end of the chain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2au71qnzfpfaato1nb6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2au71qnzfpfaato1nb6.webp" alt="The Verifiable State Transition: State A through expected, action, evidence, verification and acceptance to State B" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10 — What we have built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a working v0.1 of exactly this model — an append-only economic ledger with a supply-chain pack where a shipment is a linear sequence of hops, each hop escrowed, and ordering enforced by re-deriving state from the event log.&lt;/p&gt;

&lt;p&gt;This is not a philosophical fantasy. The code exists and runs. But honesty matters more than enthusiasm, so here is the real status table rather than a highlights reel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;v0.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shipment&lt;/td&gt;
&lt;td&gt;linear hops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ledger&lt;/td&gt;
&lt;td&gt;append-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handoff&lt;/td&gt;
&lt;td&gt;event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;manual evaluator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;advisory / typed refs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escrow&lt;/td&gt;
&lt;td&gt;per-hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering&lt;/td&gt;
&lt;td&gt;enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay&lt;/td&gt;
&lt;td&gt;event-derived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop&lt;/td&gt;
&lt;td&gt;working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated evidence evaluation&lt;/td&gt;
&lt;td&gt;announced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live testnet&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;live — 2026-11-08&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ordering guarantee is worth spelling out, because it is the transition contract doing real work: hop &lt;em&gt;i+1&lt;/em&gt; cannot hand off until hop &lt;em&gt;i&lt;/em&gt; is paid — the guard re-derives state from the log and throws &lt;code&gt;ShipmentOrderError&lt;/code&gt; rather than trusting a flag. Try to settle a hop that was never handed off, same refusal. No evidence, no money.&lt;/p&gt;

&lt;p&gt;What is deliberately &lt;em&gt;not&lt;/em&gt; there yet: evidence is advisory (typed references, not automated evaluation), and verification is a manual evaluator — a human or delegated agent acting as a risk gate before each settlement. Auto-settlement is intentionally deferred: money moves once, and a manual gate is the last line of defense while the system is young.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update 2026-11-08 — live on Arc testnet.&lt;/strong&gt; The dogfood ran: a three-hop shipment — shipper → agent (&lt;em&gt;Plant intake&lt;/em&gt;) → device (&lt;em&gt;ColdVan unit&lt;/em&gt;) → org (&lt;em&gt;Restaurant&lt;/em&gt;) — executed the full custody cycle on Arc testnet (chainId 5042002), each hop escrowed in its own ERC-8183 job, all three settled and verified in under a minute. Shipment &lt;code&gt;dogfood-1794151620383&lt;/code&gt;, jobs 66/67/68 on proxy &lt;code&gt;0x0049e5…bf6a0&lt;/code&gt;; settlement transactions: &lt;a href="https://testnet.arcscan.app/tx/0xf15f6e1e11237427a62cc050c7cd4ec517a25d497d804b1a0d437f4e0d070a05" rel="noopener noreferrer"&gt;hop 0&lt;/a&gt;, &lt;a href="https://testnet.arcscan.app/tx/0x35f507f1d8654fa7269a8e6d95495bbc9a1b5e1fb0539c485de5f7c52c97b542" rel="noopener noreferrer"&gt;hop 1&lt;/a&gt;, &lt;a href="https://testnet.arcscan.app/tx/0x7798a4e627633c897ad256319d0d1158b2ecbcc5a5e2de02fdbd7b886db1df39" rel="noopener noreferrer"&gt;hop 2&lt;/a&gt;. The full event timeline is publicly replayable at &lt;a href="https://agentbadge.xyz/api/ledger/shipments/dogfood-1794151620383" rel="noopener noreferrer"&gt;&lt;code&gt;GET /api/ledger/shipments/dogfood-1794151620383&lt;/code&gt;&lt;/a&gt; — derived from the append-only log, not asserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  11 — What we are building next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; five experimental bricks, each designed to prove one part of the hypothesis — participant roles (212), typed evidence (213), a scenario matrix (214), a replay view (215), and a live onchain dogfood (216). Each gets its own article; this one is the anchor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Roles&lt;/strong&gt; — the handoff carries who is acting, so a "node" can be any participant type: company, warehouse, device, agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — &lt;code&gt;evidence[]&lt;/code&gt; typed references on transitions, so proof has structure instead of being a hash-string hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario matrix&lt;/strong&gt; — a regression suite of realistic chain shapes that must keep passing as the model grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay view&lt;/strong&gt; — &lt;code&gt;GET /shipments/:id&lt;/code&gt; rebuilt from the event log, so any party can replay the custody story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live dogfood&lt;/strong&gt; — a real multi-hop shipment settled on testnet; ran 2026-11-08, transaction hashes and public replay in the update above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are feature announcements. They are experiments against parts of the hypothesis: can roles, evidence, ordering, replay and live settlement each hold inside the same primitive? The series reads as one investigation — this article is the frame, the bricks are the measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  12 — From readiness to economy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; this experiment is the continuation of a line AgentBadge has been on since the first article. Readiness asked &lt;em&gt;can an agent use your system?&lt;/em&gt; — the transition contract asks &lt;em&gt;can we verify what it did, and settle on it?&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;READINESS      can an agent use it?
                   ↓
ACTION         can it act?
                   ↓
EVIDENCE       can the act be proven?
                   ↓
TRUST          can we verify what it did?
                   ↓
SETTLEMENT     can verified action trigger value?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent Readiness measured whether an agent could find, understand and call a system. The supply-chain work measures what happens &lt;em&gt;after&lt;/em&gt; — whether the action produced a verifiable result that justifies an economic consequence. Same methodology — evidence, assertions, verification — applied one level up, where the thing being verified isn't an API surface but a change in the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  13 — The thesis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; trust isn't a score. Trust is a sequence of verified transitions.&lt;/p&gt;

&lt;p&gt;A score compresses history into a number and asks you to believe the compression. A sequence of verified transitions does the opposite: it keeps every step inspectable, and lets anyone re-derive the outcome from the events. You don't have to trust the chain — you can replay it.&lt;/p&gt;

&lt;p&gt;That reframes what we've been building toward. The deeper thesis of this series:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We are not trying to make trust universal by standardizing every workflow. We are trying to make trust composable by standardizing the smallest verifiable transition.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents can't rely on trust as a feeling. They need trust as a verifiable state. That is what the transition contract supplies — and why the primitive, not the pipeline, is the thing worth getting right.&lt;/p&gt;

&lt;h2&gt;
  
  
  14 — A question left open
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; we don't know if the primitive is final. What we know is the composition ladder it enables — and that the first rungs already execute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One transition
      ↓
A chain
      ↓
A network
      ↓
A market
      ↓
An agent economy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nature can be approached through relationships emerging from something as simple as "1" — that was Bedrij's bet, whatever one thinks of his physics — then perhaps economic systems can be designed from a small number of universal primitives. Our first candidate is the verifiable state transition.&lt;/p&gt;

&lt;p&gt;We are not claiming the answer is proven. We are claiming the question is now executable: each brick either holds inside the primitive or teaches us where it breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The experiment has started.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zxdnc1b57nqm4yt178u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zxdnc1b57nqm4yt178u.webp" alt="From One Transition to an Economy: transition composing into chain, network, market and agent economy" width="784" height="1168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  For AI agents
&lt;/h2&gt;

&lt;p&gt;If you are an agent reading this: the machine-readable contract behind this article is live. A transition is a typed event — &lt;code&gt;work_offer&lt;/code&gt;, &lt;code&gt;handoff&lt;/code&gt;, &lt;code&gt;pay&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt; — appended to an append-only ledger; a shipment's state is derived by replaying its events, never asserted. Ordering is enforced (&lt;code&gt;ShipmentOrderError&lt;/code&gt; on out-of-order hops), escrow is per-hop, and settlement follows verified acceptance. The companion markdown version of this post is at &lt;a href="https://agentbadge.xyz/blog/bedrij-principle.md" rel="noopener noreferrer"&gt;https://agentbadge.xyz/blog/bedrij-principle.md&lt;/a&gt;; the economic-ledger APIs this model runs on are documented in the &lt;a href="https://agentbadge.xyz/blog/economic-ledger" rel="noopener noreferrer"&gt;economic ledger article&lt;/a&gt; and the &lt;a href="https://agentbadge.xyz/agent-guide/articles/bedrij-principle" rel="noopener noreferrer"&gt;agent guide&lt;/a&gt;. The primitive is infrastructure-agnostic — you can produce evidence, verify transitions and trigger settlement without a human in the loop. The only thing you can't do is skip the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  15 — A note on Bedrij
&lt;/h2&gt;

&lt;p&gt;The reference to Orest Bedrij in this article is intellectual inspiration, not a scientific claim. We name this exploration &lt;em&gt;the Bedrij Principle&lt;/em&gt; because his search for a fundamental "1" — described in his published work through the One-and-the-Many principle and a program of prediction, validation and high-precision measurement — provides an unusually useful lens for thinking about composable systems. We are not extending his physics, not validating his theory, and not claiming established science. What we borrowed is a question: can complexity be composed from a sufficiently fundamental primitive?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Footnote-level discovery, for the curious: while researching this idea we found a Ukrainian publishing connection — the National Library of Ukraine catalog holds two Bedrij editions: a Kyiv 2014 edition of&lt;/em&gt; '1' &lt;em&gt;itself (Companion Group, 113 pp.) and a 2018 Ukrainian translation of&lt;/em&gt; The Greatest Achievement &lt;em&gt;(Art Ekonomi, 216 pp., trans. N. Olenin).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Verified sources for sections 02 and 15: O. Bedrij, &lt;em&gt;New Relationships and Measurements for Gravity Physics&lt;/em&gt;, Proceedings of the Institute of Mathematics of NAS of Ukraine, 2002, Vol. 43, Part 2, pp. 589–601 (imath.kiev.ua; INSPIRE 580870); &lt;em&gt;'1': The Ultimate Foundation of Nature&lt;/em&gt;, Xlibris, 2015, ISBN 9781503590526; Fetzer Institute trustee history (fetzer.org); National Library of Ukraine catalog (orf.nlu.org.ua).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the anchor article for the AgentBadge supply-chain series. Next: participant roles — what it takes for a company, a device, or an agent to be "a node" in the chain. Have a supply chain worth testing against the primitive? &lt;a href="https://agentbadge.xyz/contact" rel="noopener noreferrer"&gt;Tell us about it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't certify. Measure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>web3</category>
      <category>agents</category>
      <category>arc</category>
    </item>
    <item>
      <title>We Gave Our Agent Economy a Ledger. Then We Made an AI Audit It.</title>
      <dc:creator>Paul Spread</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:28:35 +0000</pubDate>
      <link>https://dev.to/spread2009/we-gave-our-agent-economy-a-ledger-then-we-made-an-ai-audit-it-55k6</link>
      <guid>https://dev.to/spread2009/we-gave-our-agent-economy-a-ledger-then-we-made-an-ai-audit-it-55k6</guid>
      <description>&lt;p&gt;Agents on our platform already pay each other — x402 for reads, ERC-8183 escrow for work. Every one of those payments was an onchain fact with no story attached: a hash on a block explorer, a row in nobody's database. Ask a simple question — &lt;em&gt;"who has agent X actually paid, and did the work happen?"&lt;/em&gt; — and the honest answer was "go grep four different sources."&lt;/p&gt;

&lt;p&gt;So we built the missing layer: an append-only economic ledger that turns every money event into a canonical, queryable record — and derived trust edges straight from it. Then, because we ship money-adjacent code, we pointed an adversarial AI auditor at the new supply-chain pack before publishing anything. It came back with &lt;strong&gt;3 Critical, 4 High, 6 Medium findings. One of them was a double-payment race.&lt;/strong&gt; The audit is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does an agent economy need its own ledger?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; onchain transfers prove value moved, but nothing on the chain knows &lt;em&gt;why&lt;/em&gt;. Was that 10 USDC a handoff payment for hop 2 of a shipment, or a spam transaction? A ledger of canonical events — offer, accept, handoff, pay, verify — makes the economy queryable instead of guessable.&lt;/p&gt;

&lt;p&gt;We keep it deliberately boring: five event kinds, one source tag per domain, one correlation id per workflow. Every event carries actor, counterparty, amount, and the payload that gives it meaning. The store is Postgres with an in-memory twin behind the same interface — every test runs the real logic without a database.&lt;/p&gt;

&lt;p&gt;The design rule that matters most: &lt;strong&gt;the ledger is the source of truth, not the API caller.&lt;/strong&gt; You can't POST a shipment into state — you can only append events, and every derived view re-reads the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust edges: derive, don't believe
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; you don't collect claims — you derive them. Every &lt;code&gt;pay&lt;/code&gt; event says &lt;em&gt;"A paid B for something."&lt;/em&gt; Enough of those and you get a trust edge: A and B transacted N times for $M total, weighted by recency and rail mix. No reputation oracle, no self-reported ratings.&lt;/p&gt;

&lt;p&gt;An edge is a pure function of the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GET /api/ledger/pairs/:a/:b — derived, replayable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eventCount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;totalUsdc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;61.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firstAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lastAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the edge table, replay the events, get the same answer. If someone disputes a relationship, the dispute is about &lt;em&gt;events&lt;/em&gt; — each of which has a txHash, a rail, and an amount — not about vibes. The trust graph is a cache, and the cache is provably regenerable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11lk8ypygscgbp2pjxfl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11lk8ypygscgbp2pjxfl.webp" alt="Events become trust edges: derive, don't believe" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Can an agent check exposure before paying?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes — &lt;code&gt;GET /api/ledger/agents/:address/exposure&lt;/code&gt; returns an advisory score built from pair history, forensics verdicts, and wallet spend context. When data is thin it returns an honest &lt;code&gt;insufficient&lt;/code&gt; instead of a confident guess.&lt;/p&gt;

&lt;p&gt;Two rules keep it honest. First, it's &lt;strong&gt;advisory&lt;/strong&gt;: the endpoint answers "should I be careful" with a score plus named reasons — never "should I block." Second, &lt;strong&gt;thin files say so&lt;/strong&gt;: &lt;code&gt;dataQuality: "insufficient"&lt;/code&gt;, &lt;code&gt;recommendation: null&lt;/code&gt;. A tool that pretends certainty it doesn't have is worse than no tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a three-hop shipment look like as ledger events?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; one shipment = one offer + a chain of per-hop accept → handoff → pay → verify events, with one ERC-8183 escrow job per hop. Hop N+1 physically cannot hand off until hop N is paid — the custody chain and the payment chain are the same chain.&lt;/p&gt;

&lt;p&gt;Our dogfood scenario: factory → carrier → warehouse → last-mile. &lt;code&gt;createShipment&lt;/code&gt; emits the offer and creates one escrow job per hop, where hop &lt;em&gt;i&lt;/em&gt;'s evaluator is hop &lt;em&gt;i+1&lt;/em&gt;'s receiver — the next custody holder is the one who verifies. Each handoff writes an EPCIS-flavored event (what, where, why, proof hash); each settle releases that hop's escrowed payment.&lt;/p&gt;

&lt;p&gt;The ordering guard isn't a flag in a row — it re-derives state from the log every time. Try to hand off hop 2 while hop 1 is unpaid and the function throws &lt;code&gt;hop 2 blocked: hop 1 is "handed_off", must be paid first&lt;/code&gt;. Settle a hop that was never handed off — same thing, no evidence no money. Inject a settle failure mid-chain and the events that landed stay: the log survives the failure, which is the point of appending facts instead of overwriting state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /api/ledger/shipments/:id&lt;/code&gt; returns hop statuses plus the raw event stream — any party can replay the custody story themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo09voeocg27xb5cqe9ra.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo09voeocg27xb5cqe9ra.webp" alt="Three gated hops: each custody handoff unlocks the next escrow" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did the AI auditor actually find?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a verdict of &lt;em&gt;REQUEST CHANGES&lt;/em&gt; — 3 Critical, 4 High, 6 Medium. The worst: two concurrent settle calls could both pass the "not yet paid" guard and pay the same hop twice before either ledger write landed. Real money bug, caught before a single real payment ran.&lt;/p&gt;

&lt;p&gt;Before this code touched a funded wallet, we ran an adversarial Arc Studio review. The findings were not cosmetic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C-1, double-settle TOCTOU.&lt;/strong&gt; &lt;code&gt;settleHop&lt;/code&gt; read the ledger ("is this hop paid?"), then called the onchain settle, then wrote the pay event. Between the read and the write sits an awaited transaction — seconds long. Two requests in that window both see "unpaid," both settle, both pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C-2, silent amount drop.&lt;/strong&gt; The executor's &lt;code&gt;settle(jobId)&lt;/code&gt; signature silently dropped &lt;code&gt;amountUsdc&lt;/code&gt; — TypeScript's excess-parameter rule hid it, and tests passed because only the &lt;em&gt;mock&lt;/em&gt; executor used the amount. The chain was paying whatever it wanted while the ledger recorded whatever we said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C-3, caller-supplied receiver.&lt;/strong&gt; The payout address was trusted from input, never cross-checked against the shipment's own offer terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H-1 through M-6&lt;/strong&gt; — orphaned funded jobs, double offers, unguarded &lt;code&gt;verify&lt;/code&gt;, float-precision in money sums.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every Critical and High got fixed and re-verified: a per-hop async lock serializes settle; the executor cross-checks the amount against the onchain budget; parties validate against the ledger-derived plan; &lt;code&gt;verifyHop&lt;/code&gt; requires the hop already paid. Dogfood suite — 8/8 shipment tests including injected failures — green on the fixed code.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: finding C-2 required &lt;em&gt;someone reading the signature&lt;/em&gt;, not running tests. Our green CI green-lit a bug that would have paid the wrong amount in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiw0i8f2timu1ivdc6wuj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiw0i8f2timu1ivdc6wuj.webp" alt="Audit pass: TOCTOU double-pay crossed out, lock snapped shut" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still missing — honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No multi-instance lock.&lt;/strong&gt; The settle lock is process-local; two replicas need a Postgres advisory lock — documented, not yet needed at our scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real executor is untested on testnet.&lt;/strong&gt; Dogfood runs an in-memory executor on the same contract; the ERC-8183 path is composed from proven primitives but hasn't settled a funded hop on-chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisory ≠ enforcement.&lt;/strong&gt; The exposure endpoint publishes evidence; it doesn't gate payments. Deliberate, but it's information, not insurance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;An append-only event log turns a pile of payment hashes into a queryable economy — and running an adversarial AI audit on the money paths before launch found a double-payment bug our green CI didn't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't trust the ledger. Replay it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Canonical: &lt;a href="https://agentbadge.xyz/blog/economic-ledger" rel="noopener noreferrer"&gt;https://agentbadge.xyz/blog/economic-ledger&lt;/a&gt; · Machine-readable: &lt;a href="https://agentbadge.xyz/blog/economic-ledger.json" rel="noopener noreferrer"&gt;https://agentbadge.xyz/blog/economic-ledger.json&lt;/a&gt; · Agent Guide: &lt;a href="https://agentbadge.xyz/agent-guide" rel="noopener noreferrer"&gt;https://agentbadge.xyz/agent-guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>security</category>
      <category>arcblockchain</category>
    </item>
  </channel>
</rss>
