<?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: Tminus1sec</title>
    <description>The latest articles on DEV Community by Tminus1sec (@tminus1s).</description>
    <link>https://dev.to/tminus1s</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%2F3996717%2Ffb0baa9b-4af0-409d-8246-af52e4597c0e.jpg</url>
      <title>DEV Community: Tminus1sec</title>
      <link>https://dev.to/tminus1s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tminus1s"/>
    <language>en</language>
    <item>
      <title>You Cannot Leak What You Never Collected</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:00:36 +0000</pubDate>
      <link>https://dev.to/tminus1s/you-cannot-leak-what-you-never-collected-2ahc</link>
      <guid>https://dev.to/tminus1s/you-cannot-leak-what-you-never-collected-2ahc</guid>
      <description>&lt;p&gt;On 1 September 2026, KrebsOnSecurity reported that a dark web service was selling scans of more than &lt;strong&gt;153 million driver's licences&lt;/strong&gt; from the US and Canada, alongside identity cards, travel documents and medical cards.&lt;/p&gt;

&lt;p&gt;The reporting traced the likely source to an identity verification vendor that processes over 21 million verifications per month across 20,000+ locations. The operators claimed they had been exfiltrating continuously for over a year. The FBI has opened an investigation.&lt;/p&gt;

&lt;p&gt;I want to skip the outrage and look at the architecture, because the architecture is the story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of the failure
&lt;/h2&gt;

&lt;p&gt;Consider what each verification event actually needed to establish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Car rental&lt;/strong&gt;: this person holds a valid licence, and is the person on it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cannabis dispensary&lt;/strong&gt;: this person is over 21&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alcohol delivery&lt;/strong&gt;: this person is over 21&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotel check-in&lt;/strong&gt;: this person is who they claim to be&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a predicate. A boolean. The answer is one bit.&lt;/p&gt;

&lt;p&gt;What was transmitted and retained instead: name, address, date of birth, document number, portrait, signature, issue and expiry dates, plus &lt;strong&gt;infrared and ultraviolet captures&lt;/strong&gt; of the physical document.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A one-bit question was answered with a multi-megabyte artefact, which was then persisted by a third party.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That persistence is the vulnerability. Not the network perimeter, not the access controls, not whatever the forensic firm eventually finds. Those are proximate causes.&lt;/p&gt;

&lt;p&gt;The root cause is that a system was designed so that &lt;strong&gt;verification implies collection&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this class of data is uniquely bad
&lt;/h2&gt;

&lt;p&gt;Credentials have a rotation story. Passwords get changed, keys get rotated, tokens expire. The blast radius of a leak is bounded by how fast you can rotate.&lt;/p&gt;

&lt;p&gt;Identity documents have no rotation story.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Date of birth is immutable&lt;/li&gt;
&lt;li&gt;Biometric portrait is effectively immutable&lt;/li&gt;
&lt;li&gt;Address history is append-only&lt;/li&gt;
&lt;li&gt;Even reissuing a licence gives you a new number attached to the same underlying person&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the correct mental model for a leaked ID scan is not &lt;em&gt;a compromised credential&lt;/em&gt;. It is &lt;strong&gt;a permanent capability grant&lt;/strong&gt; to anyone who buys it.&lt;/p&gt;

&lt;p&gt;And the reporting notes the data includes people in witness protection and people who have fled domestic violence, for whom the threat model is not financial.&lt;/p&gt;




&lt;h2&gt;
  
  
  The minimisation argument, stated precisely
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A system cannot lose data it does not hold.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds trite until you treat it as a design constraint rather than a platitude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under a collection model&lt;/strong&gt;, your security posture is a function of every control you maintain, forever, across every vendor and subprocessor who touches the data. It degrades over time. It requires perpetual investment. It fails eventually, on a long enough timeline, for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under a proof model&lt;/strong&gt;, there is no data at rest to protect. The verifier's breach exposure for that class of data approaches zero, because the class of data was never created.&lt;/p&gt;

&lt;p&gt;This is not a claim that proof systems are unbreakable. It is a claim that they move the failure mode from &lt;em&gt;inevitable and catastrophic&lt;/em&gt; to &lt;em&gt;bounded&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How verifying without collecting actually works
&lt;/h2&gt;

&lt;p&gt;The primitive is a zero-knowledge proof: prove a statement is true without revealing the data that makes it true.&lt;/p&gt;

&lt;p&gt;The practical flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A trusted issuer (DMV, bank, government service) verifies your identity &lt;strong&gt;once&lt;/strong&gt; and issues a credential you hold on your own device&lt;/li&gt;
&lt;li&gt;A verifier needs to check something specific&lt;/li&gt;
&lt;li&gt;Your device generates a proof of that &lt;strong&gt;predicate only&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The verifier checks the proof against the issuer's signature
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional:
  user ──[ full licence scan ]──&amp;gt; verifier ──&amp;gt; storage ──&amp;gt; breach

Proof-based:
  user ──[ proof: age &amp;gt;= 21 ]──&amp;gt; verifier ──&amp;gt; (nothing to store)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prove age over 21 without revealing date of birth. Prove licence validity without revealing licence number. Prove residency without revealing address.&lt;/p&gt;

&lt;p&gt;The verifier gets a cryptographically sound yes or no. It receives, and therefore stores, nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The privacy property is not policy.&lt;/strong&gt; It is not a promise in a data processing agreement. The verifier cannot leak your date of birth because it never received your date of birth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Midnight fits, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;I build on Midnight, so I'll declare the bias up front.&lt;/p&gt;

&lt;p&gt;Midnight is a blockchain designed around this idea rather than retrofitted for it. It maintains separate public and private state, so sensitive data stays on the user's device and never reaches the chain. Its smart contract language, Compact, enforces the boundary at compile time. Attempting to move a private value into public state is a &lt;strong&gt;compilation error&lt;/strong&gt;, not a lint warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;potential witness-value disclosure must be declared but is not:
  witness value potentially disclosed:
     the return value of witness get_amount at line 8 char 1
  nature of the disclosure:
     ledger operation might disclose the witness value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last detail matters more than it sounds. In most systems, data minimisation is a policy that a tired developer can violate on a Tuesday. Making it a compiler error changes who has to remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I am not claiming:&lt;/strong&gt; that this is deployed at rental counters today. It is not. There is no path right now where you present a ZK proof to a clerk with a document scanner and a vendor contract. The infrastructure gap is real and large.&lt;/p&gt;

&lt;p&gt;The claim is narrower, and I think harder to dispute: the failure here was &lt;strong&gt;architectural, not operational&lt;/strong&gt;. The alternative architecture exists and works. What is missing is the incentive to adopt it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The incentive question
&lt;/h2&gt;

&lt;p&gt;Collection persists because it is currently cheap and proof is currently expensive, in integration effort, not compute.&lt;/p&gt;

&lt;p&gt;Three things change that calculation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Liability&lt;/strong&gt;. If holding identity documents becomes an uninsurable risk, holding them stops being free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulation&lt;/strong&gt;. Data minimisation is already law in several jurisdictions, but enforcement targets breach &lt;em&gt;response&lt;/em&gt; rather than collection &lt;em&gt;design&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards&lt;/strong&gt;. Verification is a solved problem cryptographically and an unsolved problem in plumbing. Whoever makes the integration trivial wins&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineers reading this have more leverage on the third than anyone else does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The next time you specify an identity check, ask what predicate you actually need to satisfy. Then ask what you are about to store, for how long, and who inherits the liability.&lt;/p&gt;

&lt;p&gt;If those two answers are wildly different sizes, &lt;strong&gt;you are building a honeypot&lt;/strong&gt;. It may not leak on your watch. On a long enough timeline it leaks on somebody's.&lt;/p&gt;

&lt;p&gt;153 million people answered a yes-or-no question by handing over everything.&lt;/p&gt;

&lt;p&gt;The technology to just say yes already exists.&lt;/p&gt;

</description>
      <category>zkproofs</category>
      <category>security</category>
      <category>architecture</category>
      <category>cryptocurrency</category>
    </item>
    <item>
      <title>Six things I learned moving USDM between Cardano and Midnight</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:37:55 +0000</pubDate>
      <link>https://dev.to/tminus1s/five-things-i-learned-moving-usdm-between-cardano-and-midnight-4l2k</link>
      <guid>https://dev.to/tminus1s/five-things-i-learned-moving-usdm-between-cardano-and-midnight-4l2k</guid>
      <description>&lt;p&gt;Notes from building a cross-chain USDM transfer app and a private invoice contract on Midnight. Enterprise addresses, explorer link rules, a node error code, and why a contract cannot hold USDM today.&lt;/p&gt;




&lt;p&gt;I built two things on VIA's USDM cross-chain transfers over the past two weeks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/tomiin/tender-usdm" rel="noopener noreferrer"&gt;Tender&lt;/a&gt;&lt;/strong&gt; — a browser app that moves native USDM between Cardano Preprod and Midnight Preview, both directions, signed by the user's own wallets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/tomiin/privoice" rel="noopener noreferrer"&gt;Privoice&lt;/a&gt;&lt;/strong&gt; — a private invoice contract in Compact, settling in USDM, deployed on Midnight Preview.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight transfers landed. One contract deployed with four on-chain interactions. Along the way I hit five things that are either not in the docs or are in the docs in a form that does not warn you about the consequence.&lt;/p&gt;

&lt;p&gt;None of this is a criticism of VIA's documentation, which is good. It is the gap between "correctly specified" and "you will not guess this at 2am".&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Your USDM arrives at an address your wallet will not show you
&lt;/h2&gt;

&lt;p&gt;This one cost me a day, and I suspect it has cost other people their submissions.&lt;/p&gt;

&lt;p&gt;Send USDM from Midnight to Cardano and it will not appear in Lace. The balance does not move. The transfer looks like it failed. It did not fail — the money is at an address your wallet does not watch.&lt;/p&gt;

&lt;p&gt;Here is the mechanism, from VIA's own message spec. The VILR payload has fixed-width fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Width&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;source_depositor&lt;/code&gt; — payment key hash of the depositor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;136&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;destination_recipient&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;source_token&lt;/code&gt;, &lt;code&gt;destination_token&lt;/code&gt;, and &lt;code&gt;destination_recipient&lt;/code&gt; are each &lt;strong&gt;exactly 32 bytes. No shorter, no longer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Cardano &lt;strong&gt;base&lt;/strong&gt; address is 57 bytes: a header, a 28-byte payment credential, and a 28-byte stake credential. It does not fit in 32 bytes. What survives is the payment credential — which is exactly what &lt;code&gt;source_depositor&lt;/code&gt; is defined as.&lt;/p&gt;

&lt;p&gt;Reconstruct an address from a payment credential with no stake credential and you get an &lt;strong&gt;enterprise address&lt;/strong&gt;. Every time. By design.&lt;/p&gt;

&lt;p&gt;So the spec is correct and complete, and the consequence — &lt;em&gt;your funds land somewhere Lace does not display, and you will think the bridge ate them&lt;/em&gt; — is nowhere.&lt;/p&gt;

&lt;p&gt;I built the derivation into Tender so the app shows both balances side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cardano Preprod        990.000000 USDM
  977.000000 at your wallet address
   13.000000 at your enterprise address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VIA export &lt;code&gt;getEnterpriseAddress&lt;/code&gt; from the package, so they know. If you are building anything user-facing on this, derive it and show it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. VIA Scan needs &lt;code&gt;0x&lt;/code&gt; on Midnight hashes and refuses it on Cardano ones
&lt;/h2&gt;

&lt;p&gt;Same transfer, two URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scan.vialabs.tech/tx/3c7daa78…9fda6034      →  Not found
scan.vialabs.tech/tx/0x3c7daa78…9fda6034    →  Delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Cardano-source hash must be &lt;strong&gt;bare&lt;/strong&gt;. A Midnight-source hash must be &lt;strong&gt;&lt;code&gt;0x&lt;/code&gt;-prefixed&lt;/strong&gt;. Get it wrong and Scan tells you the transaction does not exist, on a transfer that completed.&lt;/p&gt;

&lt;p&gt;And one more: &lt;strong&gt;VIA Scan opens on Mainnet.&lt;/strong&gt; Every testnet message is invisible until you flip the network toggle.&lt;/p&gt;

&lt;p&gt;That is three separate ways to conclude a working transfer failed. I wrote one helper and stopped guessing:&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;function&lt;/span&gt; &lt;span class="nf"&gt;viaScanHref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromMidnight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fromMidnight&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x&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="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VIA_SCAN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tx/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  3. Testnet attests after one block. Mainnet is the fifty-minute one
&lt;/h2&gt;

&lt;p&gt;The docs say it plainly, but it is easy to read the wrong row:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Validators wait for source-chain confirmations before attesting a message: &lt;strong&gt;1 block on testnet&lt;/strong&gt;, 150 Cardano blocks (≈50 minutes) on mainnet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My fastest Preprod → Preview transfer delivered in &lt;strong&gt;1 minute 52 seconds&lt;/strong&gt;. So if you are on testnet and nothing has arrived after ten minutes, something is wrong — do not wait an hour because you saw "~55 min" somewhere.&lt;/p&gt;

&lt;p&gt;Which is how I noticed the next thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. A Compact contract cannot take custody of USDM on Preview today
&lt;/h2&gt;

&lt;p&gt;Privoice was originally designed to pool USDM inside the contract, so amounts could be hidden outright rather than just unlinked. Before writing it, I tested whether that was possible.&lt;/p&gt;

&lt;p&gt;I put two circuits in &lt;strong&gt;one contract&lt;/strong&gt; — same deploy, same wallet, same DUST state, same block window — differing by a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit bump(): [] {
  bumps = (bumps + (1 as Uint&amp;lt;64&amp;gt;)) as Uint&amp;lt;64&amp;gt;;
}

export circuit deposit(color: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;128&amp;gt;): [] {
  receiveUnshielded(disclose(color), disclose(amount));
  deposits = (deposits + (1 as Uint&amp;lt;64&amp;gt;)) as Uint&amp;lt;64&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contract &lt;code&gt;c755041b16edcde0fb504e8967317d8de0cb2ef6a8593585877cd2a7e9c6e928&lt;/code&gt; on Preview:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;circuit&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bump()&lt;/code&gt; — ledger write only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ACCEPTED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;deposit()&lt;/code&gt; — ledger write + &lt;code&gt;receiveUnshielded&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;REJECTED, &lt;code&gt;Custom(192)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six attempts, each &lt;strong&gt;rebuilt and re-proved from scratch&lt;/strong&gt; rather than resubmitted, all rejected identically, about forty seconds after the control was accepted from the same wallet.&lt;/p&gt;

&lt;p&gt;Decoding &lt;code&gt;1010: Invalid Transaction: Custom error: 192&lt;/code&gt; matters here, so: &lt;strong&gt;&lt;code&gt;1010&lt;/code&gt; is upstream Substrate and carries no Midnight meaning.&lt;/strong&gt; The cause is the inner &lt;code&gt;u8&lt;/code&gt;. Look it up in &lt;code&gt;impl From&amp;lt;LedgerApiError&amp;gt; for u8&lt;/code&gt; in &lt;code&gt;midnight-node/ledger/src/versions/common/types.rs&lt;/code&gt; — not in a blog post, because the mapping changes across ledger versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;MalformedError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InputsSignaturesLengthMismatch&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;192&lt;/code&gt; sits in a run of four checks that are all specifically about unshielded inputs and outputs: &lt;code&gt;InputsNotSorted&lt;/code&gt; (189), &lt;code&gt;OutputsNotSorted&lt;/code&gt; (190), &lt;code&gt;DuplicateInputs&lt;/code&gt; (191), &lt;code&gt;InputsSignaturesLengthMismatch&lt;/code&gt; (192).&lt;/p&gt;

&lt;p&gt;So Privoice settles at the &lt;strong&gt;application layer&lt;/strong&gt; — the contract records that settlement happened and never touches the token. That is a measured constraint, not a design preference, and the README says so with both contract addresses.&lt;/p&gt;

&lt;p&gt;Worth knowing if you are designing anything that wants a contract to hold USDM.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Unshielded inputs each need a signature, and forgetting it lands you on 192 too
&lt;/h2&gt;

&lt;p&gt;Related, and the practical version of the above. When you move USDM wallet-to-wallet with the SDK, the recipe must be &lt;strong&gt;signed before it is finalised&lt;/strong&gt;:&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;const&lt;/span&gt; &lt;span class="nx"&gt;recipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;facade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transferTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unshielded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USDM_TOKEN_COLOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receiverAddress&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shieldedSecretKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dustSecretKey&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payFees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signed&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;facade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&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;keystore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;facade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalizeRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txId&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;facade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip &lt;code&gt;signRecipe&lt;/code&gt; and the transaction reaches the node with fewer signatures than inputs — and you get &lt;code&gt;Custom(192)&lt;/code&gt; again, from a completely different cause than the contract case above. Same code, same error, unrelated bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. When the gateway stops, there is no other way to get USDM
&lt;/h2&gt;

&lt;p&gt;USDM is minted on Midnight &lt;strong&gt;solely&lt;/strong&gt; by VIA's cross-chain gateway delivering a&lt;br&gt;
message from Cardano. No faucet, no other issuance path. Which means if that&lt;br&gt;
gateway stops delivering, you cannot get USDM on Preview at all — and I found&lt;br&gt;
that out with a submission deadline in front of me.&lt;/p&gt;

&lt;p&gt;On 2 September I sent 10 USDM from my own bridge UI at 12:15 MDT. Nothing&lt;br&gt;
arrived. An hour later I sent 1 USDM using &lt;strong&gt;VIA's own CLI&lt;/strong&gt; as a control, to&lt;br&gt;
rule out my code. Both sat at &lt;em&gt;Awaiting Attestation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The evidence said it was not me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both messages carried routing values matching VIA's published testnet table — chain ids &lt;code&gt;2273266 → 64364450&lt;/code&gt;, gateway &lt;code&gt;471dfe55…e73e485c&lt;/code&gt;, token colour &lt;code&gt;003bacd9…947d73&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both &lt;code&gt;destinationRecipient&lt;/code&gt; fields decoded to my own address&lt;/li&gt;
&lt;li&gt;Cardano Preprod was healthy — the tip was ~260 blocks past my lock&lt;/li&gt;
&lt;li&gt;The docs say testnet attests after &lt;strong&gt;1 block&lt;/strong&gt;, and my 29 August transfer on the same route delivered in &lt;strong&gt;1m 52s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On VIA's global message feed, the last delivered message on that route was ~24 hours old. Mine were the only two since.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two independent clients, two package versions, ninety minutes apart, identical&lt;br&gt;
failure. That is about as clean a "not my code" result as you get.&lt;/p&gt;

&lt;p&gt;So I shipped anyway, and documented it. The payment leg is a parameter — one&lt;br&gt;
32-byte token colour — so I ran the settlement with NIGHT instead, and the&lt;br&gt;
README said exactly that: which token moved, which token the code defaults to,&lt;br&gt;
why the substitution was necessary, and both stuck message ids so a reviewer&lt;br&gt;
could check the claim themselves in thirty seconds.&lt;/p&gt;

&lt;p&gt;The next morning both messages delivered — &lt;strong&gt;16h 32m&lt;/strong&gt; and &lt;strong&gt;15h 4m&lt;/strong&gt;. VIA's&lt;br&gt;
scanner still labels those "normal". I re-ran the lifecycle with real USDM,&lt;br&gt;
paying a second address rather than my own, and removed the substitution note&lt;br&gt;
from the README, because it had stopped being true.&lt;/p&gt;

&lt;p&gt;Two things I would take from that. &lt;strong&gt;Delivery time on Preprod → Preview is not&lt;br&gt;
something to plan around&lt;/strong&gt; — the documented rule is one block and I saw sixteen&lt;br&gt;
hours. And when you are blocked by someone else's infrastructure, the move is&lt;br&gt;
to make the blocker verifiable rather than quietly omit it. Naming it cost&lt;br&gt;
nothing and would have survived review either way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two smaller ones, for whoever hits them next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DUST is not automatic.&lt;/strong&gt; Holding NIGHT does not generate DUST. It must be &lt;strong&gt;registered&lt;/strong&gt;. My wallet and my probe both read zero until I registered, and nothing tells you that is why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DUST wallet syncs separately, and far more slowly.&lt;/strong&gt; Most sync helpers wait on &lt;code&gt;state.unshielded.progress&lt;/code&gt;, which catches up in about a second — then declare success while dust sits at 42 of 183,000 events. A zero DUST balance in that state is correct, not a fault. Wait on &lt;code&gt;state.dust.progress&lt;/code&gt; explicitly, and save a checkpoint afterwards or you pay the cold sync every run.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tomiin/tender-usdm" rel="noopener noreferrer"&gt;Tender&lt;/a&gt;&lt;/strong&gt; — eight transfers, both directions, three of them signed in Lace rather than by the service. Enterprise-address derivation and correct explorer links built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tomiin/privoice" rel="noopener noreferrer"&gt;Privoice&lt;/a&gt;&lt;/strong&gt; — a Compact contract where only &lt;code&gt;persistentHash(amount, payer, memo, salt)&lt;/code&gt; goes on chain. The payer proves an invoice names them by reconstructing a commitment that matches, so identity is demonstrated rather than asserted. Deployed on Preview with &lt;code&gt;issue → acknowledge → pay → settle&lt;/code&gt;, all four accepted.&lt;/p&gt;

&lt;p&gt;Both repos have the full working code, the failed experiments, and the addresses to check any of this yourself.&lt;/p&gt;

&lt;p&gt;If you are starting on this today: derive the enterprise address, put &lt;code&gt;0x&lt;/code&gt; on your Midnight hashes, register your NIGHT, and read error codes from &lt;code&gt;types.rs&lt;/code&gt;. That is most of the day I lost, in one line.&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>compact</category>
      <category>midnightfordevs</category>
      <category>zk</category>
    </item>
    <item>
      <title>Building a .night domain profile viewer on Midnight with the Midnames SD</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Fri, 24 Jul 2026 01:59:21 +0000</pubDate>
      <link>https://dev.to/tminus1s/building-a-night-domain-profile-viewer-on-midnight-with-the-midnames-sd-4kph</link>
      <guid>https://dev.to/tminus1s/building-a-night-domain-profile-viewer-on-midnight-with-the-midnames-sd-4kph</guid>
      <description>&lt;p&gt;I'm a self-taught builder, and this week I built a small thing that made a concept finally click for me: a .night domain profile viewer. You type a name like tomin.night, it resolves on-chain, and it shows you the profile. This is the walkthrough — what a .night name actually is, the one SDK call that resolves it, the two gotchas that cost me time, and how I wrapped it into a tiny dApp.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/tomiin/midnames-profile-viewer" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnames-profile-viewer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What a .night name actually is&lt;/p&gt;

&lt;p&gt;On Midnight, .night domains are managed by Midnames. The important part for a developer: the name registry is a Compact smart contract deployed on-chain. The @midnames/sdk package literally ships the compiled contract (it exports Contract, ledger, witnesses, and a MANAGED_DIR of Compact artifacts).&lt;/p&gt;

&lt;p&gt;So "look up a domain" doesn't mean "call a company's REST API." It means read contract state. The domain, its owner, where it points, and its profile fields all live on the chain. That's the whole appeal: a .night name is a portable, on-chain identity you own.&lt;/p&gt;

&lt;p&gt;The one call that does the work&lt;/p&gt;

&lt;p&gt;Install the SDK (Node ≥ 22, ESM):&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
npm install @midnames/sdk&lt;br&gt;
npm pkg set type=module&lt;/p&gt;

&lt;p&gt;Resolving a domain is two lines:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
import { createDefaultProvider, getDomainProfile } from "@midnames/sdk";&lt;/p&gt;

&lt;p&gt;const provider = createDefaultProvider({ networkId: "mainnet" });&lt;br&gt;
const result = await getDomainProfile("tomin.night", { provider });&lt;/p&gt;

&lt;p&gt;if (result.success) {&lt;br&gt;
  console.dir(result.data, { depth: null });&lt;br&gt;
} else {&lt;br&gt;
  console.error(result.error); // e.g. DomainNotFoundError&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The SDK returns a result object with a success flag — no try/catch gymnastics for the "domain doesn't exist" case, which is nice.&lt;/p&gt;

&lt;p&gt;What comes back&lt;/p&gt;

&lt;p&gt;Here's the real shape of result.data for my domain (trimmed):&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
{&lt;br&gt;
  fullDomain: 'tomin.night',&lt;br&gt;
  info: {&lt;br&gt;
    id: 142n,&lt;br&gt;
    owner: '0e3240f66410...a9f36a6694',&lt;br&gt;
    ownerAddress: 'mn_addr1vglt...w63s8cp7sd',&lt;br&gt;
    target: {&lt;br&gt;
      type: 'shielded',&lt;br&gt;
      address: 'mn_shield-cpk1...nvxdys3gwdtn'&lt;br&gt;
    },&lt;br&gt;
    targetLocked: false&lt;br&gt;
  },&lt;br&gt;
  fields: Map(4) {&lt;br&gt;
    'epk' =&amp;gt; 'mn_shield-epk1...',&lt;br&gt;
    'profile_type' =&amp;gt; 'personal',&lt;br&gt;
    'discord' =&amp;gt; 'Uetto',&lt;br&gt;
    'Zealy' =&amp;gt; 'tminus1sec'&lt;br&gt;
  },&lt;br&gt;
  settings: { coinColor: Uint8Array(32) [...], costs: {...}, buyEnabled: false }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Two things jump out:&lt;/p&gt;

&lt;p&gt;target — the address the name points to, like a DNS record but for wallets. Mine resolves to a shielded address. So a wallet could let you "pay tomin.night" and quietly turn it into that address, instead of anyone copying 60 hex characters.&lt;br&gt;
fields — arbitrary key/value profile data (discord, socials, a profile_type, whatever you set). This is the identity layer.&lt;br&gt;
Gotcha #1: which network is your domain on?&lt;/p&gt;

&lt;p&gt;This one cost me real time. The Midnames SDK supports two networks, mainnet and preprod. I first ran a script against preprod and got:&lt;/p&gt;

&lt;p&gt;DomainNotFoundError: Domain not found: tomin.night&lt;/p&gt;

&lt;p&gt;The domain wasn't missing — it was registered on mainnet, and I was querying preprod. The tell is in the addresses: a mainnet owner address looks like mn_addr1..., while preprod is mn_addr_preprod1.... So point the provider at the network your domain actually lives on:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
const provider = createDefaultProvider({ networkId: "mainnet" }); // not "preprod"&lt;br&gt;
Gotcha #2: the SDK returns real JS types, not JSON&lt;/p&gt;

&lt;p&gt;Look again at that result: id is a BigInt (142n), coinColor is a Uint8Array, and fields is a Map. If you JSON.stringify that to send it to a browser, it throws on the BigInt and mangles the Map. So I wrote a small serializer:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
function serialize(v) {&lt;br&gt;
  if (typeof v === 'bigint') return v.toString();&lt;br&gt;
  if (v instanceof Uint8Array) return Buffer.from(v).toString('hex');&lt;br&gt;
  if (v instanceof Map) return Object.fromEntries([...v].map(([k, val]) =&amp;gt; [k, serialize(val)]));&lt;br&gt;
  if (Array.isArray(v)) return v.map(serialize);&lt;br&gt;
  if (v &amp;amp;&amp;amp; typeof v === 'object') {&lt;br&gt;
    const o = {};&lt;br&gt;
    for (const [k, val] of Object.entries(v)) o[k] = serialize(val);&lt;br&gt;
    return o;&lt;br&gt;
  }&lt;br&gt;
  return v;&lt;br&gt;
}&lt;br&gt;
Wrapping it in a dApp&lt;/p&gt;

&lt;p&gt;I kept the architecture boring on purpose: the SDK runs in a tiny Node/Express backend (where it's happiest — no browser polyfills), and a plain HTML/JS page calls it and renders the card.&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
import express from 'express';&lt;br&gt;
import { createDefaultProvider, getDomainProfile } from '@midnames/sdk';&lt;/p&gt;

&lt;p&gt;const app = express();&lt;br&gt;
app.use(express.static('public'));&lt;/p&gt;

&lt;p&gt;const providers = {};&lt;br&gt;
const providerFor = (net) =&amp;gt; (providers[net] ??= createDefaultProvider({ networkId: net }));&lt;/p&gt;

&lt;p&gt;app.get('/api/resolve', async (req, res) =&amp;gt; {&lt;br&gt;
  const domain = String(req.query.domain || '').toLowerCase();&lt;br&gt;
  const network = String(req.query.network || 'mainnet').toLowerCase();&lt;br&gt;
  const result = await getDomainProfile(domain, { provider: providerFor(network) });&lt;br&gt;
  if (!result.success) return res.status(404).json({ error: result.error?.message });&lt;br&gt;
  res.json({ domain, network, data: serialize(result.data) });&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;app.listen(5173, () =&amp;gt; console.log('&lt;a href="http://localhost:5173')" rel="noopener noreferrer"&gt;http://localhost:5173')&lt;/a&gt;);&lt;/p&gt;

&lt;p&gt;The front end is just an input, a network toggle, and a fetch to /api/resolve that renders the owner, the resolved target, and the profile fields. No wallet connect, no login — resolving a name is a public read.&lt;/p&gt;

&lt;p&gt;Run it&lt;br&gt;
bash&lt;br&gt;
git clone &lt;a href="https://github.com/tomiin/midnames-profile-viewer" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnames-profile-viewer&lt;/a&gt;&lt;br&gt;
cd midnames-profile-viewer&lt;br&gt;
npm install&lt;br&gt;
npm start&lt;/p&gt;

&lt;h1&gt;
  
  
  open &lt;a href="http://localhost:5173" rel="noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;Takeaways&lt;br&gt;
A .night name is an entry in an on-chain Compact contract you own — resolvable to an address plus a bag of profile fields. It's a genuinely nice identity primitive.&lt;br&gt;
Reading it is one SDK call; the friction is all around it: pick the right network, and serialize the rich JS types (BigInt / Uint8Array / Map) before they hit a browser.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/tomiin/midnames-profile-viewer" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnames-profile-viewer&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  MidnightForDevs
&lt;/h1&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Midnight sprint: a sealed-bid auction, a stubborn error 170, and everything it taught me"</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Sun, 19 Jul 2026 01:46:51 +0000</pubDate>
      <link>https://dev.to/tminus1s/midnight-sprint-a-sealed-bid-auction-a-stubborn-error-170-and-everything-it-taught-me-24kd</link>
      <guid>https://dev.to/tminus1s/midnight-sprint-a-sealed-bid-auction-a-stubborn-error-170-and-everything-it-taught-me-24kd</guid>
      <description>&lt;p&gt;I'm a self-taught builder, not a career dev. I lean on AI to help me a lot when my syntax breaks and explanations are rather verbose, I read a lot of&lt;br&gt;
docs, and I break things constantly. So this is a build story from that seat — the&lt;br&gt;
Midnight Expert sprint, everything I shipped, and every wall I hit getting there.&lt;br&gt;
If you're also learning this stuff, the messy parts below are the useful parts.&lt;/p&gt;

&lt;p&gt;The whole sprint ran through the &lt;strong&gt;Midnight Expert&lt;/strong&gt; plugins inside &lt;strong&gt;Claude Code&lt;/strong&gt;&lt;br&gt;
— a marketplace of AI agent plugins for the Midnight blockchain (Compact contracts,&lt;br&gt;
DApp frontends, toolchain, an error-code lookup, and more). I installed it there,&lt;br&gt;
ran the diagnostics, and did the contract + tooling work from that setup. The&lt;br&gt;
finishing and debugging happened in a mix of Claude Code and Claude's desktop&lt;br&gt;
agent, but the Midnight-specific muscle came from those plugins.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I shipped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Compact smart contract&lt;/strong&gt; — a sealed-bid auction with a real nullifier:
&lt;a href="https://github.com/tomiin/midnight-sealed-bid-commit-reveal" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnight-sealed-bid-commit-reveal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployed it to Midnight Preprod&lt;/strong&gt; with a live on-chain interaction (contract
address is in that repo's README).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A PR back to the Midnight Expert repo&lt;/strong&gt; improving an error-code entry I'd just
spent hours living inside.&lt;/li&gt;
&lt;li&gt;(Bonus, same journey: a from-scratch &lt;strong&gt;CLI wallet&lt;/strong&gt; —
&lt;a href="https://github.com/tomiin/midnight-cli-wallet" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnight-cli-wallet&lt;/a&gt; — which is where I accidentally
learned the fix that unblocked everything else.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Getting set up (the easy 10-XP wins)
&lt;/h2&gt;

&lt;p&gt;First three quests were the warm-up: explore the marketplace, star the repo, and&lt;br&gt;
install + run diagnostics. In Claude Code that last one is a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/midnight-expert:doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks your compiler version, tooling, and MCP servers and tells you what's&lt;br&gt;
green. Mine flagged that my Compact compiler had upgraded (0.30 → 0.31.1) and a&lt;br&gt;
couple of harmless warnings. Good baseline before touching anything real.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quest 1 — Writing a Compact contract
&lt;/h2&gt;

&lt;p&gt;The idea: a &lt;strong&gt;sealed-bid auction&lt;/strong&gt;. While bidding is open, nobody — not other&lt;br&gt;
bidders, not even the auctioneer — can see what anyone bid. When bidding closes,&lt;br&gt;
people reveal their numbers, the contract checks each against what was locked in&lt;br&gt;
earlier, and the highest honest bid wins. Think sealed envelopes: shut while you're&lt;br&gt;
bidding, opened at reveal time.&lt;/p&gt;

&lt;p&gt;The part I actually wanted to get right was the &lt;strong&gt;nullifier&lt;/strong&gt;. Compact has no&lt;br&gt;
&lt;code&gt;msg.sender&lt;/code&gt; — there's no built-in "who's calling." So a caller proves who they are&lt;br&gt;
by knowing a secret key that never leaves their machine, and the contract derives a&lt;br&gt;
one-way fingerprint (a nullifier) from it. Two details make it real: it's&lt;br&gt;
&lt;strong&gt;domain-separated&lt;/strong&gt; (tagged &lt;code&gt;"sbid:v1:nullifier"&lt;/code&gt; so it can't be confused with any&lt;br&gt;
other hash from the same key), and it's the &lt;strong&gt;double-bid guard&lt;/strong&gt; (your commitment is&lt;br&gt;
filed under your nullifier, so a second bid collides and bounces). One identity, one&lt;br&gt;
bid, and your identity never hits the chain.&lt;/p&gt;

&lt;p&gt;I wrote it with the &lt;code&gt;compact-core&lt;/code&gt; plugin, compiled it, and got a passing test suite&lt;br&gt;
before going anywhere near a network. The honest boundary, which a lot of "private"&lt;br&gt;
demos skip: the bid &lt;em&gt;amounts&lt;/em&gt; are private while bidding is open, but the&lt;br&gt;
nullifiers and commitments are public — that's what makes the anti-double-bid check&lt;br&gt;
work.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quest 2 — Ship and deploy it (a.k.a. the error 170 saga)
&lt;/h2&gt;

&lt;p&gt;This is where I lost a night, so buckle up. The quest wants the contract deployed to&lt;br&gt;
&lt;strong&gt;Preprod&lt;/strong&gt; (a public test network) with at least one real on-chain interaction.&lt;/p&gt;

&lt;p&gt;My deploy CLI would build the transaction, prove it, submit it — and the node would&lt;br&gt;
spit back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1010: Invalid Transaction: Custom error: 170
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single time. Error 170 is &lt;code&gt;InvalidDustSpendProof&lt;/code&gt;. Here's the thing I didn't&lt;br&gt;
understand at first: on Midnight you don't pay fees with the main token (NIGHT). You&lt;br&gt;
register NIGHT to &lt;em&gt;generate&lt;/em&gt; a fee resource called &lt;strong&gt;DUST&lt;/strong&gt;, and every transaction&lt;br&gt;
proves a little DUST spend to cover its fee. &lt;strong&gt;170 is the fee leg getting rejected&lt;/strong&gt;&lt;br&gt;
— nothing to do with my contract.&lt;/p&gt;

&lt;p&gt;I assumed it was the test network being slow (a Midnight dev even confirmed that&lt;br&gt;
version of it happens when the public indexer lags behind the node). So I tried the&lt;br&gt;
other public network, Preview. Its faucet's human-check spun forever and then locked&lt;br&gt;
me out for 24 hours. Two public networks, two different dead ends, same night.&lt;/p&gt;

&lt;p&gt;So I did the thing that actually cracked it: I stood up a &lt;strong&gt;local devnet&lt;/strong&gt; with the&lt;br&gt;
&lt;code&gt;midnight-tooling&lt;/code&gt; plugin — a node and indexer in Docker, right on my machine, with&lt;br&gt;
zero lag between them. And it &lt;em&gt;still&lt;/em&gt; threw 170. That was the lightbulb. If a chain&lt;br&gt;
that's perfectly in sync with itself rejects my transaction too, the problem isn't&lt;br&gt;
the network — it's my code.&lt;/p&gt;

&lt;p&gt;Three things had to be fixed, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Rebuild the transaction on every retry — don't resubmit the same one.&lt;/strong&gt; My retry&lt;br&gt;
loop was resubmitting the &lt;em&gt;exact same&lt;/em&gt; proven transaction each time. But a 170's&lt;br&gt;
stale DUST proof is baked into that transaction, so re-sending it just re-presents&lt;br&gt;
the same dead proof. On a chain minting blocks every few seconds, it's stale the&lt;br&gt;
instant it's built. The fix was to rebuild and re-balance the whole thing on each&lt;br&gt;
attempt so every try carries a fresh proof:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Attempt 1: building (fresh dust balance), proving, submitting…
  attempt 1 failed: Transaction submission error   (that's the 170)
Attempt 2: building (fresh dust balance), proving, submitting…
Transfer submitted. Tx: 0087338e7833176e...c008fe3d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempt two, fresh rebuild, straight through. (I found this on the wallet first, then&lt;br&gt;
carried it to the deploy.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Keep the wallet's DUST synced to the current tip.&lt;/strong&gt; Even with the rebuild loop,&lt;br&gt;
if the wallet's DUST state is hours behind the chain it can't even balance the fee —&lt;br&gt;
you get instant "could not balance dust" failures instead of 170. My deploy wallet&lt;br&gt;
was restoring from a checkpoint saved earlier in the day. I re-synced it to the&lt;br&gt;
current tip first, then deployed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use a strong local password.&lt;/strong&gt; Once the DUST was sorted, the deploy got all the&lt;br&gt;
way to storing private state and died with &lt;code&gt;PasswordValidationError: Password must&lt;br&gt;
contain at least 3 of: uppercase, lowercase, digits, special. Found: 2&lt;/code&gt;. The SDK now&lt;br&gt;
enforces that on the local private-state store. Bumped my dev password to four&lt;br&gt;
classes and moved on.&lt;/p&gt;

&lt;p&gt;And then, finally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;✅ DEPLOYED. Contract address: ad08e233a172874748b05ab40a30c9217699650115aa5650c3c671accfee4244
Placing one sealed bid (on-chain interaction)…
✅ placeSealedBid submitted.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live on Preprod, with a real interaction. After all that, it went through on the&lt;br&gt;
second attempt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quest 3 — Extend Midnight Expert
&lt;/h2&gt;

&lt;p&gt;The Extend quest wants a real PR back to the &lt;a href="https://github.com/devrelaicom/midnight-expert" rel="noopener noreferrer"&gt;midnight-expert&lt;br&gt;
repo&lt;/a&gt;. I had the perfect thing,&lt;br&gt;
because I'd just been &lt;em&gt;inside&lt;/em&gt; the exact gap.&lt;/p&gt;

&lt;p&gt;The repo has a &lt;code&gt;midnight-status-codes&lt;/code&gt; plugin — a searchable catalog of every&lt;br&gt;
Midnight error code. I looked up 170, and its only suggested fix was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Regenerate the dust spend proof using the proof server"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is exactly the trap I'd fallen into. Re-proving or resubmitting the &lt;em&gt;same&lt;/em&gt;&lt;br&gt;
transaction can never clear a 170 — that's the whole lesson I'd just paid for in&lt;br&gt;
hours. So I expanded the entry with the remediation that actually works: sync the&lt;br&gt;
DUST to the tip, rebuild-don't-resubmit on 170, and the practical tell (instant&lt;br&gt;
"could not balance dust" = too stale to balance, resync; a 170 &lt;em&gt;after&lt;/em&gt; proving = the&lt;br&gt;
block-advance race, which rebuilding rides through).&lt;/p&gt;

&lt;p&gt;Their repo has a schema check and a test suite, and both pass with the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schema check PASSED
Results: 14 passed, 0 failed
All checks passed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small, genuine, and something I could only have written by living through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A retry only helps if the next attempt is different from the one that failed.&lt;/strong&gt;
For a dropped connection, resend. For a rejected proof, rebuild. I'd copied a retry
that resent, and it couldn't have worked no matter how many times it ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a public network keeps telling you "it's infra," reproduce it somewhere you
fully control.&lt;/strong&gt; The local devnet turning "the chain is broken" into "my code is
wrong" was the single most useful move of the whole sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the real state, not the tutorial's idea of it.&lt;/strong&gt; The SDK moves fast; half my
walls were things that had quietly changed shape since the docs were written.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sealed-bid auction (contract + deploy CLI + the full roadblocks writeup):
&lt;a href="https://github.com/tomiin/midnight-sealed-bid-commit-reveal" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnight-sealed-bid-commit-reveal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CLI wallet: &lt;a href="https://github.com/tomiin/midnight-cli-wallet" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnight-cli-wallet&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Midnight Expert marketplace: &lt;a href="https://github.com/devrelaicom/midnight-expert" rel="noopener noreferrer"&gt;https://github.com/devrelaicom/midnight-expert&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're learning Midnight too: the concepts are genuinely different (DUST fees,&lt;br&gt;
nullifiers, no &lt;code&gt;msg.sender&lt;/code&gt;), but none of the walls I hit were exotic. They were&lt;br&gt;
stale state, flaky endpoints, and a fast-moving SDK. Reproduce, rebuild, and don't&lt;br&gt;
trust "it's infra" until you've seen it fail somewhere you own.&lt;/p&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>zeroknowledge</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Privacy isn't encryption: what building on Midnight actually taught me</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:11:39 +0000</pubDate>
      <link>https://dev.to/tminus1s/privacy-isnt-encryption-what-building-on-midnight-actually-taught-me-27j6</link>
      <guid>https://dev.to/tminus1s/privacy-isnt-encryption-what-building-on-midnight-actually-taught-me-27j6</guid>
      <description>&lt;p&gt;When I started building on Midnight, I had the wrong mental model, and I suspect&lt;br&gt;
most people arriving from "normal" blockchains have it too. I assumed a &lt;em&gt;privacy&lt;/em&gt;&lt;br&gt;
chain meant my data would sit on-chain, encrypted, and only the right people could&lt;br&gt;
decrypt it. A lockbox on a public ledger.&lt;/p&gt;

&lt;p&gt;That model is wrong. And if you design around it, you'll build the wrong thing.&lt;br&gt;
Here's what actually clicked for me after building a few contracts, including a&lt;br&gt;
privacy-preserving health-records system.&lt;/p&gt;

&lt;p&gt;Encryption and zero-knowledge are different tools&lt;/p&gt;

&lt;p&gt;They get lumped together, but they solve different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption hides bytes.&lt;/strong&gt; You scramble data so only someone with the key can&lt;br&gt;
read it. The catch: whoever has the key sees &lt;em&gt;everything&lt;/em&gt;. And "encrypted, on a&lt;br&gt;
public ledger, forever" is a liability, not a feature. Keys leak. Keys get lost.&lt;br&gt;
And a ciphertext that sits on-chain for eternity is a standing bet that today's&lt;br&gt;
encryption survives tomorrow's computers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-knowledge proves statements.&lt;/strong&gt; It lets you show that something is true —&lt;br&gt;
"I'm over 18," "this vote is valid," "I'm the assigned doctor" — &lt;em&gt;without&lt;br&gt;
revealing the underlying data at all&lt;/em&gt;. No key handed over, nothing decrypted. The&lt;br&gt;
sensitive part never has to be shown to anyone.&lt;/p&gt;

&lt;p&gt;So privacy on Midnight is not "encrypt the data and put it on-chain." It's almost&lt;br&gt;
the opposite: keep the sensitive data &lt;em&gt;off&lt;/em&gt; the ledger entirely, and put only&lt;br&gt;
&lt;strong&gt;proofs&lt;/strong&gt; and &lt;strong&gt;commitments&lt;/strong&gt; on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real model: selective disclosure
&lt;/h2&gt;

&lt;p&gt;The idea that runs through everything on Midnight is &lt;em&gt;selective disclosure&lt;/em&gt;. You&lt;br&gt;
choose exactly which facts become public, and everything else simply never touches&lt;br&gt;
the chain. A couple of examples from things I built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tip jar where the running total is public — so it's trustworthy — but each
donor is recorded only as a one-way hash. The chain never learns who gave.&lt;/li&gt;
&lt;li&gt;A vote where the tally is public but the voter is a nullifier. Reveal the count,
hide the identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; happening: nothing is "encrypted and then decrypted later." The&lt;br&gt;
private part is never on-chain in any form. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the misconception really bites: health records
&lt;/h2&gt;

&lt;p&gt;I built an access-control contract for medical results, and it's the perfect place&lt;br&gt;
to see the wrong model fail.&lt;/p&gt;

&lt;p&gt;If you believe "privacy = encrypt on-chain," you'd try to store encrypted lab&lt;br&gt;
results on the chain and build a key-management scheme around them. You'd spend all&lt;br&gt;
your time on the vault. And you'd have put the most sensitive data humans produce&lt;br&gt;
onto a public ledger forever.&lt;/p&gt;

&lt;p&gt;Here's what I built instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The result stays encrypted &lt;strong&gt;off-chain&lt;/strong&gt;. The chain holds only a &lt;strong&gt;commitment&lt;/strong&gt;
— a one-way hash of it. Tamper-proof, but it reveals nothing.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// the lab posts only the HASH of the (off-chain, encrypted) result
resultHash.insert(disclose(recordId), disclose(resultCommitment));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Identity never goes on-chain. A caller proves they hold a secret key — a private
&lt;em&gt;witness&lt;/em&gt; — and all the chain ever stores is a hash of it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness localSecretKey(): Bytes&amp;lt;32&amp;gt;;              // private, never disclosed

export pure circuit accountId(sk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  return persistentHash&amp;lt;Vector&amp;lt;1, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([sk]);   // one-way
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The chain enforces the &lt;strong&gt;rules&lt;/strong&gt;: only the assigned doctor with the patient's&lt;br&gt;
consent gets routine access; an emergency responder can break the glass, but&lt;br&gt;
every override is logged for audit. None of that needs the data — it needs&lt;br&gt;
proofs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And you can still prove a result is the genuine one, without the chain ever&lt;br&gt;
seeing it:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit verifyResult(recordId: Uint&amp;lt;64&amp;gt;, candidate: Bytes&amp;lt;32&amp;gt;): Boolean {
  return resultHash.lookup(disclose(recordId)) == disclose(candidate);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain never sees your data. It enforces the rules &lt;em&gt;about&lt;/em&gt; your data, and keeps&lt;br&gt;
an honest audit trail. The data itself stays where sensitive data belongs — off&lt;br&gt;
the public ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design lesson
&lt;/h2&gt;

&lt;p&gt;Once I stopped thinking "hide the bytes" and started thinking "prove the facts,"&lt;br&gt;
the designs got simpler and better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't put sensitive data on-chain, even encrypted. Put &lt;strong&gt;proofs and commitments&lt;/strong&gt;
on-chain; keep &lt;strong&gt;data&lt;/strong&gt; off-chain.&lt;/li&gt;
&lt;li&gt;Let the chain be the &lt;strong&gt;referee&lt;/strong&gt; — rules, consent, audit — not the &lt;strong&gt;vault&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Compact makes this explicit whether you like it or not: the &lt;code&gt;disclose()&lt;/code&gt; keyword
forces you to opt in every time you make something public. You can't accidentally
leak private data; you have to choose to reveal it. That constraint is a gift.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;Midnight isn't magic, and pretending otherwise is how you ship a demo that&lt;br&gt;
overpromises. The encryption and key management for the actual data still happen&lt;br&gt;
off-chain with ordinary cryptography. What Midnight adds is the layer on top: the&lt;br&gt;
proofs, the rules, and the audit. Being clear about that line is the difference&lt;br&gt;
between a real design and a pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Privacy on Midnight isn't a lockbox on the chain. It's proving the truth while&lt;br&gt;
keeping the data to yourself. Once that clicked, I stopped trying to hide bytes and&lt;br&gt;
started asking a better question: &lt;em&gt;which facts actually need to be public?&lt;/em&gt; Answer&lt;br&gt;
that, put only those on-chain as proofs, and let everything else stay private by&lt;br&gt;
never being there in the first place.&lt;/p&gt;

&lt;h1&gt;
  
  
  midnight #zero-knowledge #privacy #blockchain #selective-disclosure
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>Why your Midnight contract "can't be found on chain" (or: Midnight, stop playing with my emotions)</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Fri, 26 Jun 2026 01:48:18 +0000</pubDate>
      <link>https://dev.to/tminus1s/why-your-midnight-contract-cant-be-found-on-chain-or-midnight-stop-playing-with-my-emotions-18l5</link>
      <guid>https://dev.to/tminus1s/why-your-midnight-contract-cant-be-found-on-chain-or-midnight-stop-playing-with-my-emotions-18l5</guid>
      <description>&lt;p&gt;I'm a self-taught learner grinding through the Midnight quests, and this week the chain and I had a little falling out. I deployed a contract, got an address back, submitted it like a proud parent at a recital, and got served four of the saddest words in Web3: "cannot find contract on chain."&lt;/p&gt;

&lt;p&gt;My code was fine. I just didn't understand Midnight's networks and tokens yet, and a tutorial I trusted shipped me off to a network that doesn't exist anymore.&lt;/p&gt;

&lt;p&gt;So before we get into it: Midnight team, I love you, you're literally building the future of privacy, but quit teasing those of us out here learning. As the song goes - my feelings, don't play with them.&lt;/p&gt;

&lt;p&gt;Okay. Map time. This is the thing I wish someone had handed me on day one.&lt;/p&gt;

&lt;p&gt;Midnight is not one network (plot twist)&lt;br&gt;
The first thing that got me: "Midnight" isn't a single place. There are a few networks, and they do not talk to each other:&lt;/p&gt;

&lt;p&gt;A local one. You run a node, an indexer, and a proof server on your own machine with Docker (people call it "standalone" or "undeployed"). Everything here lives only on your computer.&lt;br&gt;
Preview. A public test network.&lt;br&gt;
Preprod. Another public test network, and the one a lot of the current quests and tutorials point you at.&lt;br&gt;
Mainnet. The real one.&lt;br&gt;
"It works on my machine" is the oldest excuse in software, and on Midnight it's sometimes literally true. My contract was absolutely on chain. The chain was just my laptop.&lt;/p&gt;

&lt;p&gt;Oh, and there used to be a network called testnet-02. It has been retired. It is gone. But a lot of older tutorials and example repos were written for it and still point right at its ghost.&lt;/p&gt;

&lt;p&gt;The trap: old tutorials point at a network that left the chat&lt;br&gt;
testnet-02 walked so the newer networks could run. Then it kept walking, straight out of existence. Following an older tutorial that targets it is like pulling up to the function at last year's address: lights off, cups gone, nobody's coming.&lt;/p&gt;

&lt;p&gt;Here's what actually bit me. I forked an older example dApp, followed its instructions to the letter, deployed, and my contract was nowhere. When I finally opened the project's config, every address pointed at the dead network:&lt;/p&gt;

&lt;p&gt;indexer = '&lt;a href="https://indexer.testnet-02.midnight.network/api/v1/graphql" rel="noopener noreferrer"&gt;https://indexer.testnet-02.midnight.network/api/v1/graphql&lt;/a&gt;'&lt;br&gt;
node    = '&lt;a href="https://rpc.testnet-02.midnight.network" rel="noopener noreferrer"&gt;https://rpc.testnet-02.midnight.network&lt;/a&gt;'&lt;br&gt;
Those URLs are tombstones. So before you run any older Midnight project, open its config and check which network it targets. A quick way to sniff it out:&lt;/p&gt;

&lt;p&gt;grep -rn "midnight.network" --include=*.ts .&lt;br&gt;
If you see "testnet-02" or an old path like "/api/v1," back away slowly. The live networks today are reached through "indexer.preview.midnight.network" and "indexer.preprod.midnight.network" on a newer API version. If a repo hasn't been touched since testnet-02 was alive, it probably won't land anywhere you can verify, no matter how clean your code is.&lt;/p&gt;

&lt;p&gt;"Cannot find contract on chain": the two usual heartbreaks&lt;br&gt;
Once the networks clicked, the error finally made sense. When a quest checker (or another human) can't find your contract, it's almost always one of these:&lt;/p&gt;

&lt;p&gt;You deployed to your local network. The address is real, but it lives on your machine and nowhere else. (See: "works on my machine.")&lt;br&gt;
You deployed against a dead or wrong network, so it never landed anywhere a live explorer can see.&lt;br&gt;
The fix for both is the same: deploy to a live public network (Preview or Preprod), then confirm it in a public explorer before you submit the address anywhere. If you can't find it in an explorer, the checker won't either. Trust, but verify - and in Web3, verify twice.&lt;/p&gt;

&lt;p&gt;Tokens: NIGHT, DUST, and their test-network twins (please stop trying to buy anything)&lt;br&gt;
Confession: I went hunting for the NIGHT token on an exchange so I could deploy a contract. Like a clown. You do not buy anything to build and test. But the token setup confused me for a minute, because there are four names floating around, not two. Here's the simple version:&lt;/p&gt;

&lt;p&gt;NIGHT is the real, mainnet token - staking and governance, and it's what generates the fee token.&lt;br&gt;
DUST is what actually pays transaction fees. You don't buy it; you generate it by holding and registering NIGHT.&lt;br&gt;
On a test network, both have free stand-ins: test NIGHT (tNIGHT) and tDUST. They mirror the real pair exactly.&lt;br&gt;
So the real flow for testing is: get free test NIGHT from the faucet, register it for DUST generation, and that produces the tDUST that pays your fees. You hold the NIGHT, the DUST drips out of it over time, and none of it costs you a cent.&lt;/p&gt;

&lt;p&gt;Here's the part that humbled me: I had 1000 test NIGHT sitting in my wallet, staring at "you have no DUST for gas," wondering who hurt me. The answer was me. I hadn't registered the NIGHT yet, so no DUST was being generated. Rookie move. NIGHT in the wallet is potential energy; you have to register it before the fees actually flow.&lt;/p&gt;

&lt;p&gt;The other catch that gets everyone: set your wallet to the Preprod network BEFORE you copy your address. Hand the faucet a mainnet address and it'll look at you like "I don't know her," even though it's the same wallet.&lt;/p&gt;

&lt;p&gt;The proof server (somebody's gotta do the math)&lt;br&gt;
Every Midnight transaction needs a zero-knowledge proof, and something has to actually do that homework. That something is a "proof server." You either run one yourself locally in Docker, or you use a wallet that does the proving for you (the 1AM wallet does this "dust-free," so you can skip the Docker dance). If your deploys or DUST registration just hang there spinning, the proof server and your network config are the first suspects.&lt;/p&gt;

&lt;p&gt;Big shoutout to Gutopro, who already wrote the Preprod survival guide for the exact faucet-and-sync headaches that come next - linked at the bottom. Standing on the shoulders of devs who suffered first.&lt;/p&gt;

&lt;p&gt;A short checklist before you deploy (so the chain stops ghosting you)&lt;br&gt;
Pick your network: local for solo testing, Preview or Preprod for anything others (and quests) need to see.&lt;br&gt;
Open the project config and confirm it targets that live network, not testnet-02 or an old API path.&lt;br&gt;
Fund your wallet with free test NIGHT from the faucet (wallet set to that network first), then register it so DUST starts generating.&lt;br&gt;
Make sure a proof server is available, local in Docker or via a dust-free wallet.&lt;br&gt;
After deploying, find your contract in a public explorer. There? You're golden. Not there? You're on local-only or a dead network.&lt;br&gt;
Wrapping up&lt;br&gt;
Honestly, this is still the most fun I've had while being completely confused. Privacy tech on the frontier comes with a few battle scars, and that's part of the charm. Midnight team: I'm not mad, I'm just... also a little mad. But mostly grateful, and mostly having a blast. Just, you know - don't play with my tDUST.&lt;/p&gt;

&lt;p&gt;If you're just starting out: the networks are separate, old repos may point at a dead one, and you fund testing with free test tokens, not your wallet. Get those three straight and you'll save yourself the afternoon I lost.&lt;/p&gt;

&lt;p&gt;Built on, and learning with, the Midnight community.&lt;/p&gt;

&lt;p&gt;Further reading: "Troubleshooting Midnight Pre-Prod: A Week in the Trenches" by Gutopro for the Midnight Aliit Fellowship - &lt;a href="https://dev.to/midnight-aliit/troubleshooting-midnight-pre-prod-a-week-in-the-trenches-2g4d"&gt;https://dev.to/midnight-aliit/troubleshooting-midnight-pre-prod-a-week-in-the-trenches-2g4d&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>Learning Midnight: nothing goes public by accident</title>
      <dc:creator>Tminus1sec</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:26:58 +0000</pubDate>
      <link>https://dev.to/tminus1s/learning-midnight-nothing-goes-public-by-accident-2odm</link>
      <guid>https://dev.to/tminus1s/learning-midnight-nothing-goes-public-by-accident-2odm</guid>
      <description>&lt;p&gt;I'm a self-taught enthusiast poking around the Midnight Network, and I wanted to write down one thing that finally clicked for me this week. Maybe it helps someone else who's just starting out.&lt;br&gt;
Midnight is a privacy blockchain, and you write its smart contracts in a language called Compact. Coming in, I assumed "privacy blockchain" meant everything is hidden by default and you flip a switch when you want to share something.&lt;br&gt;
It's actually the other way around, and that surprised me.&lt;br&gt;
In Compact, anything you want to put into the public, on-chain state you have to mark on purpose, with a keyword called disclose(). If you forget, the compiler stops you. It won't let you publish a value unless you've clearly said "yes, I mean to make this public."&lt;br&gt;
The first time I hit that, it felt annoying, like extra typing for no reason. Then it clicked: that IS the privacy model. Nothing leaks by accident. You go line by line and decide exactly what crosses from the private side to the public side. Everything you don't disclose just stays private.&lt;br&gt;
That made Midnight's whole "choose what you share, nothing more" idea real for me in a way the tagline never did.&lt;br&gt;
To play with it, I built a tiny app: a proof-of-age gate. You prove you're over 18 without ever showing your birth year. The birth year stays private the whole time; the only thing that becomes public is a simple "yes, this person qualifies." Same idea, share the one fact that matters and keep the rest to yourself.&lt;br&gt;
Still very early in my journey, but this one stuck with me. Most of what I picked up came straight from the docs at docs.midnight.network.&lt;br&gt;
Repo, if you want to peek: &lt;a href="https://github.com/tomiin/midnight-proof-of-age" rel="noopener noreferrer"&gt;https://github.com/tomiin/midnight-proof-of-age&lt;/a&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
