Every developer story about smart contracts eventually runs into the same wall.
You've written the contract. It compiles, deploys, runs on chain. Now you want it to do anything useful in the real world. Send a Slack notification. Charge a Stripe card. Email a receipt. Call an LLM. Basically anything that involves the outside internet.
To do that, you need an API key. And to use the API key, you need somewhere to put it.
Every chain punts this problem. The usual answers are Chainlink, some kind of oracle setup, or "run a bot on Vercel that watches the chain and does the API call." None of those are on-chain. All of them mean the developer maintains a separate off-chain thing that could go down, get hacked, or drift out of sync with the contract.
We spent June building an actual on-chain answer. It's called Vault + Workers, and this week it went live.
The core problem
If a smart contract can decrypt a secret to use it, then everyone running the chain can decrypt it too. Contract code is public. State is public. The VM has no secrets. There is nowhere on chain to hide a key that the contract itself needs to read.
This is not a fixable problem with the current shape of smart contracts. So we changed the shape.
What Asentum does instead
Two new primitives, baked into genesis.
The Vault contract at address 0x…08 stores ciphertexts. Not plaintext. It doesn't know how to decrypt anything. It knows how to store an encrypted blob, remember who's allowed to trigger decryption of that blob, and emit an event when authorization is granted.
The WorkerRegistry contract at 0x…09 is a directory of registered off-chain daemons. Each Worker has a hybrid encryption keypair — X25519 (classical) and ML-KEM-768 (post-quantum) — a broadcast URL, and a policy hash committing to which external hosts it's willing to talk to.
The Worker daemon is a small Node.js service. It sits off-chain, tails the chain, and watches for decryption requests. When it sees one that's authorized to it, it decrypts the ciphertext locally using its private key, makes the outbound HTTP call, and submits a signed on-chain receipt.
The chain enforces the rules.
- Who can trigger decryption — the Vault's ACL.
- When it fires — the on-chain cron.
- What call gets made — the contract's own code.
- That it happened — an event log and a fulfilment tx.
- Which Worker did it — the receipt is signed by the Worker's registered pubkey.
The Worker holds the decryption key. Everything else lives on chain.
Hybrid post-quantum encryption
We ship X25519 + ML-KEM-768 + ChaCha20-Poly1305. The plaintext is encrypted to a symmetric key. The symmetric key is derived from both KEMs concurrently. If either one is broken — quantum breaks X25519, some future attack on ML-KEM — the other still protects the key.
Ciphertext overhead is about 1.2 KB. Plaintext capped at 4 KB. That's plenty for an API key, a JWT signing secret, or a database DSN. Not enough for a movie. That's fine.
A concrete example
Imagine a payroll contract. Every 15th of the month at 09:00 UTC, an on-chain cron fires. The contract iterates its employee list, computes each payout, and asks the Vault to decrypt the Stripe API key so the Worker can charge each employee's payroll balance.
The Worker sees the request, decrypts the key using its X25519 + ML-KEM-768 private halves, hits Stripe's API, and posts back a receipt. The receipt is a signed on-chain tx that the payroll contract picks up on the next block. Employees see the payout. The whole thing runs unattended.
Total infra: a smart contract and a Hetzner box running the Worker daemon. No Vercel. No AWS Secrets Manager. No separate cron service.
What went wrong
Plenty. Half of the last month was debugging the failure modes. A short list:
- Non-deterministic scanner initially picked different heights on different nodes. Fixed by pinning scan cursor to committed blocks, not head.
- Ciphertext padding wasn't stable across two Node.js versions. Locked the ChaCha20-Poly1305 nonce derivation to a chain-provided salt.
- The hosted asentum-default Worker got hit by a bad-actor test tx that asked it to decrypt a secret it wasn't authorized for. Rejected correctly at the contract level, but the Worker also needed a defensive local check because the on-chain event alone doesn't fully rule out edge cases during a re-org.
Also several smaller things: worker-daemon nonce desync with the faucet during initial funding, encoder-decoder roundtrip issues for the ML-KEM ciphertext, off-by-one in the retry loop. Real code with real bugs.
What comes next
The hosted asentum-default Worker at worker.asentum.com is running under systemd. Whitelist seeded with resend, openai, slack, telegram, github, httpbin. Anyone building on testnet can use it without running their own Worker daemon.
An SDK (@asentum/vault-sdk) exists for encrypting + storing secrets from a client. A CLI is coming. The IDE at ide.asentum.com has a Vault Explorer tab for browsing your stored secrets and watching decrypt fulfilments live.
If you build something with this that isn't obvious, tell us. We're specifically interested in cases where the on-chain enforceable rules — "this contract can trigger this call, at this schedule, only" — enable something you couldn't otherwise justify hosting off-chain.
The pitch is simple. Your smart contract can now safely call anything on the internet.
That has not been true anywhere else.
