Agent-native content marketplace. Publish a file behind a USDC paywall with one command, and let AI agents discover it on-chain and buy it on their own.
3rd place, The Synthesis 2026 hackathon (Best Use of Locus / Agent Services on Base).
AI agents are getting good at finding information, but they still can't pay for it. Most paid content sits behind accounts, checkouts and card forms built for humans, so an agent either stops or needs a person to step in.
nustuf removes the human from both sides of the sale. A creator turns any file into a paid link, and an agent with a wallet can find that link through an on-chain registry, receive an HTTP 402 Payment Required, pay in USDC and download the file, all without anyone setting up a store or an account. It is built on the x402 payment protocol, Locus agent wallets and Base.
| Promo page a buyer lands on | Live feed of on-chain drops |
|---|---|
![]() |
![]() |
A full publish, discover and buy run, with terminal output, is in demo/terminal-output.md.
| Need | For | Notes |
|---|---|---|
| Node.js 18+ and pnpm | everything | nustuf is not on the npm registry yet, so install from source (below). Running the tests needs Node 21+. |
| Tailscale | publish --public (selling) |
Free Personal plan is enough. Install it, run tailscale up, then in the admin console enable MagicDNS and HTTPS certificates, and allow Funnel (the first publish --public run prints an enable link if it is still off). This gives your drop a stable https://<machine>.<tailnet>.ts.net URL. No Tailscale? Use --tunnel cloudflared (install cloudflared) for a temporary URL that changes on restart. |
| A wallet with USDC (nustuf-generated, or Locus) | buy |
Run nustuf wallet create --role buyer and fund it (see Wallets), or use a Locus wallet via skills/nustuf-buy/SKILL.md. Buying and discovering need no Tailscale. |
| CDP API keys | mainnet payments | See Configuration. Skip by selling on Base Sepolia. |
nustuf is not on the npm registry yet, so install it from source:
git clone https://github.com/eucalyptus-viminalis/nustuf.git
cd nustuf
pnpm install
pnpm link --global # makes `nustuf` available globallyπ€ Agents: Check the
skills/directory for step-by-step guides:
skills/nustuf-buy/SKILL.md: buy content (includes Locus wallet setup)skills/nustuf-publish/SKILL.md: publish content behind a paywallskills/nustuf-discover/SKILL.md: discover live drops on-chain
publish --public uses Tailscale Funnel to give your drop a stable https://<machine>.<tailnet>.ts.net URL. Setting it up takes a few minutes:
- Install Tailscale and run
tailscale upto log in. The free Personal plan is enough. - In the admin console, open DNS and enable MagicDNS, then enable HTTPS Certificates.
- Allow Funnel for your device. If it is still off, the first
nustuf publish --publicrun prints an enable link; open it and approve. - Pick a machine name you are happy to publish. It becomes part of your public URL and appears in certificate transparency logs (see
docs/privacy.md).
Buying and discovering do not need Tailscale. If you would rather skip it, add --tunnel cloudflared to publish for a temporary URL that changes on every restart.
nustuf publish --file ./track.mp3 --price 0.50 --pay-to 0xYOUR_ADDRESS --publicThis starts a server with x402 payment gating, exposes it at a stable public URL through Tailscale Funnel (https://<machine>.<tailnet>.ts.net), and prints a shareable promo link. The URL survives restarts, so on-chain listings stay valid. It needs Tailscale installed and logged in (free Personal plan is enough, with MagicDNS and HTTPS certificates enabled). No Tailscale account? Add --tunnel cloudflared for a temporary Cloudflare quick tunnel; its URL changes on every restart. Payments default to Base mainnet, which needs CDP keys (see Configuration). To try it without them, sell on Base Sepolia with --network eip155:84532.
Add --announce to list the drop in the on-chain registry as soon as its public URL is live, so agents can find it:
nustuf publish --file ./track.mp3 --price 0.50 --pay-to 0xYOUR_ADDRESS --public --announce --title "My Track"A full testnet run, with payments and the listing both on Base Sepolia:
nustuf publish --file ./track.mp3 --price 0.50 --pay-to 0xYOUR_ADDRESS --public --network eip155:84532 --announce --title "My Track"The listing uses the sale price, expires when the sale window ends, and includes a SHA-256 hash of the file. It is signed by the first key found among --private-key-file, DEPLOYER_PRIVATE_KEY (shell or .env), the seller wallet and .locus.json; the publish summary prints the signer address and its balance. It is written to the same chain as the payments (--network), so a Base Sepolia drop is listed on Base Sepolia; a --testnet or NUSTUF_REGISTRY_CHAIN that disagrees with --network is an error. Listing on mainnet costs a little real gas. If the signer has no ETH on that chain, publish exits before going live. The announce result (tx hash or error) shows up in nustuf status --json. If the announce fails, the drop stays live and nustuf prints the nustuf announce command to retry by hand.
Note: in a terminal,
nustuf publishopens an interactive wizard prefilled from your flags and saved profile (nustuf config). Without a terminal (agents, CI, scripts) it runs straight from the flags, and--publicalso needs--public-confirm I_UNDERSTAND_PUBLIC_EXPOSURE.
nustuf discover --activeQueries the registry for live releases. It reads Base mainnet by default, so testnet drops only show up with --testnet (Base Sepolia). Filter with --creator, --max-price and --limit, and add --json for agent-friendly output. In --json, price is rounded to 2 decimals; use priceRaw (USDC base units) for sub-cent prices.
nustuf buy https://some-nustuf-url.com/ # pays from your nustuf buyer wallet
nustuf buy https://some-nustuf-url.com/ --locus # or pay with a Locus walletPays and downloads the file. With no flags it uses the wallet from nustuf wallet create --role buyer.
Files are saved to ~/Downloads/nustuf/ by default (set NUSTUF_DOWNLOAD_DIR to change it, or pass --out <file>). nustuf never overwrites an existing file; the new one is saved as name (1).ext, and the full path is printed.
nustuf feed-uiOpens a local web UI showing live announcements from the on-chain registry. No wallet or config needed.
--testnetreads Base Sepolia instead of Base mainnet.--port <port>changes the local port (default 3000, or thePORTenv var).--publicalso exposes the feed at a public URL, using Tailscale Funnel by default (stable URL) or--tunnel cloudflared(temporary URL). Without it, the feed stays on localhost.
nustuf status # what this machine is serving right now
nustuf status --json # same, machine-readable
nustuf stop <id> # stop one item (id or unique prefix), or: nustuf stop allEvery nustuf publish records itself under ~/.nustuf/runs/, so status works from any terminal. Runs started before this was added show as unknown in status --all and are not listed as serving.
For a menu bar indicator, install SwiftBar (brew install --cask swiftbar) and copy or symlink integrations/swiftbar/nustuf.10s.sh into its plugin folder. SwiftBar runs plugins with a minimal PATH, so the plugin adds the usual Node locations (Homebrew, nvm, Volta, fnm); if it still can't find nustuf, set NUSTUF_CLI to its full path in the plugin's environment settings. It shows a green dot and count while anything is live, with a per-item menu to open, copy the link, or stop.
nustuf wallet create --role buyer --testnet # prints an address; send USDC there
nustuf wallet create --role seller --testnet # prints an address; send a little ETH there
nustuf wallet show # addresses and USDC/ETH balancesnustuf generates the wallet, so you only send funds to its public address from a wallet you already use. Keys are stored in ~/.nustuf/wallets/<role>.key (mode 600). These are hot wallets: keep balances small. create never overwrites an existing wallet, and export prints the key only in an interactive terminal.
- buyer:
nustuf buypays from it when you pass no--locusor--buyer-private-key-file. It needs USDC on the seller's network and no ETH. - seller:
--announcesigns with it when there is no--private-key-fileorDEPLOYER_PRIVATE_KEY. It needs a little ETH for gas, and its address works as--pay-to. - Network:
--network <caip2>or--testnet(default: your configured chain, else Base mainnet). It only affects the funding hint and the balances shown.
Set these as environment variables or in a .env file.
| Variable | Needed for | Notes |
|---|---|---|
LOCUS_API_KEY |
buy --locus |
Locus agent wallet. A .locus.json file with { "apiKey": "..." } also works. No wallet yet? See skills/nustuf-buy/SKILL.md. |
NUSTUF_REGISTRY_ADDRESS |
publish --announce, announce, discover |
Optional. Defaults to the deployed registry, 0x134597d9Cc6270571C2b8245c4235f7838C0d65D. |
NUSTUF_REGISTRY_CHAIN |
publish --announce, announce, discover |
Optional. base (default) or base-sepolia for announce and discover. publish --announce lists on the --network chain, and exits with an error if this disagrees. |
DEPLOYER_PRIVATE_KEY |
publish --announce, announce, identity |
Optional. Key that signs on-chain writes; without it nustuf uses the seller wallet. Use a dedicated low-value key. |
SELLER_PAY_TO |
publish |
Optional. Payout address when --pay-to is not given (after that, the saved profile). |
NUSTUF_DOWNLOAD_DIR |
buy |
Optional. Download folder (default ~/Downloads/nustuf/). |
CDP_API_KEY_ID, CDP_API_KEY_SECRET, FACILITATOR_MODE=cdp_mainnet |
mainnet payments | Coinbase Developer Platform keys for settling USDC on Base mainnet. |
Creator Blockchain Buyer Agent
β β β
ββ nustuf publish βββββββββββββββββΊβ announce (on-chain registry) β
β (server + Tailscale Funnel) β β
β ββββββββββββββ nustuf discover βββββ€
β β (query active releases) β
ββββββββββββββββββββββββββββββββββββΌββββββββββββ nustuf buy βββββββββββ€
β 402 Payment Required β β
ββββββββββββββββββββββββββββββββββββΌββββββββ USDC payment βββββββββββββ€
β β
Content delivered β β
- Publish: the creator runs
nustuf publish. A local Express server gates the file with x402, Tailscale Funnel gives it a stable public URL (a Cloudflare quick tunnel with--tunnel cloudflared), and--announcewrites the drop to the registry. - Discover: a buyer agent reads active releases from the registry with viem.
- Buy: the agent requests the URL, receives a
402 Payment Requiredwith the price and payee, pays in USDC from its wallet (a nustuf buyer wallet or Locus), and gets the file.
The buyer never needs to know the file format or create an account; the HTTP response tells it everything it needs to pay.
nustuf ships 3 agent skills in the skills/ directory. Each has a SKILL.md with full instructions an agent can follow autonomously:
| Skill | Description |
|---|---|
nustuf-publish |
Publish content behind a USDC paywall |
nustuf-buy |
Purchase content with a nustuf buyer wallet or Locus |
nustuf-discover |
Discover live drops from the on-chain registry |
Running nustuf servers also expose skill metadata at /.well-known/skills/index.json for automated agent discovery.
nustuf publish Publish content behind payment gate
nustuf buy Purchase content
nustuf discover Find live releases
nustuf feed-ui Browse live releases in the browser
nustuf announce Register a drop on-chain
nustuf status Show what this machine is serving
nustuf stop Stop a published item (or all)
nustuf host Multi-host server
nustuf config Manage configuration
nustuf wallet Generate and check a buyer or seller wallet
nustuf identity ERC-8004 agent identity (testnet, draft)
nustuf version Show the version
Run nustuf --help or nustuf <command> --help for details.
- Node.js and Express: CLI and payment-gated content server
- x402: HTTP 402 payment protocol
- Locus: agent wallet for autonomous USDC payments
- Base: L2 for USDC payments and the on-chain registry
- Solidity and Foundry: the
NustufRegistrycontract (contracts/) - viem: chain reads and writes
- Tailscale Funnel: stable public URLs for a machine you own (Cloudflare quick tunnels remain as
--tunnel cloudflared)
What Tailscale's relays can and cannot see when you sell with --public, and what ends up public: see docs/privacy.md.
NustufRegistry is deployed at the same address on both networks:
- Base Mainnet:
0x134597d9Cc6270571C2b8245c4235f7838C0d65D - Base Sepolia:
0x134597d9Cc6270571C2b8245c4235f7838C0d65D
pnpm test # unit tests, plus integration tests that start the real server
pnpm test:contracts # Foundry tests for NustufRegistry
pnpm check # version sync, no machine-specific paths, skill command scope, terminology, no commentsThe integration tests run the server against a fake x402 facilitator on localhost, so they need no wallet, network access or credentials. They check that an unpaid download gets 402 Payment Required with the right payment requirements, and that download codes are enforced. pnpm test needs Node 21 or later.
The commands that spend money or write to the registry have their own tests. test/cli_inputs.test.js runs nustuf buy and checks that bad URLs, --out paths, download-code and key inputs fail before any request or payment. test/announce.test.js checks the nustuf announce plan (price to USDC base units, expiry, URL and content-hash validation), the signer-key order and the gas check. test/help.test.js checks that every --help exits cleanly and only suggests commands that exist. CI runs all of it, plus pnpm check, on every push and pull request.
nustuf was built during a hackathon and works end to end, but it is not production software:
- The package is not published to npm yet; install from source as above.
- Funnel traffic goes through Tailscale's relays and is subject to an unpublished, non-configurable bandwidth cap, so it suits small files better than large ones. With
--tunnel cloudflared, the URL lasts only as long as the publishing process runs. nustuf hoststill uses Cloudflare quick tunnels.- A drop is announced once, on its first public URL. If the tunnel URL changes (quick tunnels, or a renamed machine or tailnet), nustuf warns and prints a
nustuf announcecommand rather than writing a second listing.
MIT


