Build

SHADOW Connect

Let people prove what their private wallets hold on your site, without unlocking there.

A SHADOW account spreads what it owns over private wallets that nothing on chain ties to the main wallet. That is the point, and it means your site cannot find them by asking for a wallet address. SHADOW Connect lets the person show you the ones you need: a game can see the NFTs a player holds privately, a gated page can check a holder, a leaderboard can keep one name per player.

Never ask people to sign the SHADOW unlock message on your site. That signature is the key to their private balance. With Connect they unlock on shadowprivacy.tech, and your site only receives signatures made for it.

What happens

  • Your page calls connect() from a click. A SHADOW window opens (on phones the page goes to SHADOW and comes back).
  • The person connects and unlocks there, sees your site's address, and ticks the private wallets to share.
  • Each shared wallet signs one message naming your site, a fresh nonce and the time. A key made for your site alone signs it too: that is the person's account on your site.
  • You verify the signatures, then read what those wallets hold from the chain.

What your site learns

  • The account: the same every time this person connects to your site, different on every other site. Use it as their id.
  • The wallets they tick, and what those hold. Only the ones that hold what you asked about are offered.
  • Never: the main wallet, the other private wallets, the private balance, or any key.

Code

One module, no dependencies, served from SHADOW (or copy it into your project):

import { connect, finishRedirect, verify } from "https://shadowprivacy.tech/connect.js";

// From a click. nfts: a collection address, "any", or null for the account alone.
button.onclick = async () => {
  const session = await connect({ app: "My game", nfts: "<collection address>" });
  use(session);
};

// On phones the page comes back with the answer in its address: pick it up on load.
const back = finishRedirect();
if (back && back !== "cancelled") use(back);

async function use(session) {
  const r = await verify(session, { holdings: true });
  if (!r.ok) return console.warn(r.error);
  r.account; // this person's id on your site
  r.wallets; // [{ address, nfts: [{ asset, collection, number, name, image }] }]
}

verify checks that the session was made for your origin, is recent (10 minutes by default), and that the account and every wallet signed it. With holdings it re-reads each wallet's NFTs from the chain instead of trusting the list in the session. For anything of value (a prize, access, a payout), send the session to your server and verify there with the same module (Node 20 and later), passing your origin and the nonce you issued: verify(session, { origin: "https://your.site", nonce, holdings: true }).

The signed message

Every key signs exactly this text (ed25519, UTF-8), so any language can check it:

SHADOW Connect v1
Site: https://your.site
Account: <account public key>
Wallets: <address>,<address>   (or: none)
Nonce: <the nonce your page made>
Issued: <ISO time>

Remember the nonce you sent, refuse a session whose nonce or site differ, and refuse it once it is old. What any wallet holds is public at /api/shadow/nft/held/<wallet>, open to browsers.