Build on Asentum
Secrets and Workers
Store API keys on chain, schedule outbound HTTP calls, all gated by smart contract. Estimated read time: 10 minutes
TL;DR
Your contract needs to call Stripe, Resend, OpenAI, Slack, or any other API on a schedule. You don't want to paste a credential into contract source. You don't want to trust an off-chain bot to fire the call without anyone being able to audit it. Vault lets you encrypt a secret to a hybrid post-quantum keypair and store the ciphertext on chain. Workers are off-chain processes that watch the chain for authorized requests, decrypt the secret in their own memory, perform the HTTP call, and post a signed receipt back. The whole flow is gated by smart contract code that anyone can read.
Asentum hosts a free default Worker at workerId asentum-default. You install one npm package and write 5 lines of JavaScript.
Why this exists
Every blockchain has the same gap. A smart contract that wants to do anything useful with the real world needs to call an API. Calling an API needs a credential. There's nowhere on chain to put the credential, because everything on chain is public.
Most chains punt this. You write an off-chain bot that watches your contract's events, holds the credential in its env vars, makes the call, hopes nobody asks too many questions. The bot becomes the system of record for who can fire what and when. The chain stops being a chain.
Vault + Workers is the answer. The credential is on chain, encrypted. The rules about who can trigger decryption live in smart contract code. The actual decryption and call happen in a Worker process, but the Worker is constrained by what the contract authorizes. Every fire shows up as an event. Every fulfillment shows up as a signed tx. Audit is built in.
How it works
┌─────────────────────────────────────────────────────────────┐
│ Asentum Chain │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Vault │ │ Your │ │ Worker │ │
│ │ contract │◄──┤ contract │ │ Registry │ │
│ │ │ │ (e.g. HR) │ │ contract │ │
│ │ - secrets │ │ │ │ │ │
│ │ - authz │ │ - cron sched │ │ - worker IDs │ │
│ │ - audit log │ │ - on-tick │ │ - pubkeys │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ cron fires │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ Event: WorkRequested │ │ │
│ │ │ { workerId, jobId, │ │ │
│ │ │ secretRef, payload } │ │ │
│ │ └───────────┬────────────┘ │ │
│ │ │ │ │
└─────────┼─────────────────┼───────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Off-chain Worker │
│ 1. Tail chain for own jobs │
│ 2. Decrypt secret (hybrid X25519 + ML-KEM-768) │
│ 3. Validate URL against allowed-hosts policy │
│ 4. Make HTTP call │
│ 5. Sign + post fulfillment tx back to chain │
└──────────────────────────────────────────────────────────────┘The chain enforces the rules. The Worker just executes. Every step shows up on chain so anyone can audit who fired what, when, against which URL, and what the response hash was.
Install the SDK
npm install @asentum/vault-sdkThat's it. The SDK handles the hybrid X25519 + ML-KEM-768 + ChaCha20-Poly1305 encryption, derives the deterministic sid and jobId identifiers, builds and signs the Vault transactions, and submits them to a chain RPC of your choice.
Put a secret
The minimum viable example. Encrypt a Slack webhook URL to the hosted Worker, authorize one of your contracts to trigger decryption, store the ciphertext on chain.
import { VaultClient } from '@asentum/vault-sdk';
// Your Dilithium3 keypair, loaded however you load it
import { keypair } from './my-wallet.js';
const vault = new VaultClient({
rpc: 'https://testnet.asentum.com',
vaultAddress: '0x0000000000000000000000000000000000000008',
registryAddress: '0x0000000000000000000000000000000000000009',
keypair,
});
const { sid, txHash } = await vault.put('slack-webhook', {
plaintext: process.env.SLACK_WEBHOOK_URL,
workerId: 'asentum-default',
authorizedContracts: ['0xYourHRContract'],
});
console.log('sid:', sid);
console.log('tx:', txHash);The SDK fetches the Worker's hybrid public keypair from the on-chain registry, encrypts your plaintext locally (the secret never leaves your machine in cleartext), and submits the put transaction. The returned sid is the on-chain identifier you reference from your contract.
Trigger decryption from a contract
This is where the magic happens. Your contract calls the Vault's requestDecryption method with a payload that tells the Worker what to do once the secret is in memory.
// In your HR contract, fired by a cron schedule
const VAULT = '0x0000000000000000000000000000000000000008';
function notifyBirthday(employeeName) {
const sid = storage.get('slack-sid');
const jobId = E.deriveJobId(sid, msg.sender, chain.blockNumber, employeeName);
E.call(VAULT, 'requestDecryption', [
sid,
jobId,
JSON.stringify({
url: 'https://hooks.slack.com/services/...',
method: 'POST',
secretAs: { kind: 'body-key', key: 'webhook_token' },
body: { text: 'Happy birthday ' + employeeName + '!' },
resultMode: 'hash',
}),
]);
}The payload is opaque to the Vault, but the contract is the policy. The Worker reads the payload, validates the URL against its allowed-hosts list, injects the decrypted secret where you asked (header, query param, or body key), and makes the call.
The resultMode field controls whether the response body is hashed (default, privacy-preserving) or returned in full (for oracle-style use where the contract needs the data, capped at 1 KB).
Inspect job state
Every job has a public record on chain. Pending, ok, error, with hashes, HTTP status codes, and the fulfilling Worker's identity. From the SDK:
const job = await vault.getJob(jobId);
// {
// jobId: '0x...',
// sid: '0x...',
// requester: '0x...',
// workerId: 'asentum-default',
// status: 'ok', // 'pending' | 'ok' | 'error'
// statusCode: '200',
// hash: '0x...', // sha256 of response body
// fulfilledAt: '20047',
// fulfiller: '0x...',
// }From a contract: same call via E.callView(VAULT, 'getJob', [jobId]). Use it to decide whether to retry, alert, or move on.
Rotate, revoke, delete
// Rotate the underlying plaintext (e.g. API key got cycled)
await vault.rotate('slack-webhook', {
plaintext: 'new-slack-webhook',
workerId: 'asentum-default',
});
// Revoke a contract's authorization
await vault.revoke(sid, '0xContractWeNoLongerTrust');
// Soft-delete the secret (record stays, decryption refused)
await vault.delete(sid);Deletion is soft on purpose. The record stays in storage so the audit trail is preserved. Future calls to requestDecryption refuse the deleted sid.
Self-host a Worker
The hosted Worker is fine for testnet and most production cases. If you want to run your own (you don't want to trust someone else with your decryption keys, or you need to whitelist a custom internal API), the daemon is one npm install:
npm install @asentum/workers
npx asentum-worker init # generates keypair + scaffolds allowed-hosts.json
npx asentum-worker status # shows pubkeys + policy hash
# register the worker on chain with the printed pubkeys
ASENTUM_RPC=https://testnet.asentum.com \
ASENTUM_VAULT_ADDRESS=0xb1557a... \
ASENTUM_REGISTRY_ADDRESS=0xa6902... \
npx asentum-worker startProduction deployments typically run as a systemd unit. The daemon tails the chain, decrypts secrets in its own memory, validates each call against your allowed-hosts.json policy, makes the HTTP call, and posts a signed fulfillment tx. Reachability from the public internet is not required: the daemon polls, it doesn't listen.
Trust model
Be honest about what this gives you.
The Worker operator holds the decryption key for any secret encrypted to their Worker. You trust them the same way you would trust a SaaS vendor with your API keys today. There is no way around this on a transparent chain: if a contract can decrypt a secret autonomously, then whoever runs the decryption process can read the secret.
What you GAIN over "trust Vercel" alone:
- Who can trigger decryption: enforced by authorized-contract list on chain.
- When it fires: enforced by cron schedule on chain.
- What the payload looks like: defined by your smart contract code on chain.
- That it fired: emitted as a public event.
- Which Worker fulfilled: signed tx with registered pubkey on chain.
- Policy the Worker is operating under: hash committed at registration.
The Worker cannot fire a call your contract did not authorize. The contract owner can rotate to a different Worker at any time. A future V2 of the protocol will distribute decryption across multiple Workers via threshold cryptography, eliminating the single point of trust entirely.
Cheatsheet
// Testnet contract addresses
WorkerRegistry 0x0000000000000000000000000000000000000009
Vault 0x0000000000000000000000000000000000000008
// Hosted Worker (free during testnet)
workerId asentum-default
allowed hosts api.resend.com, api.openai.com, hooks.slack.com,
api.telegram.org, api.github.com, httpbin.org
// SDK exports
VaultClient // connected client
encrypt / decrypt // standalone crypto helpers
deriveSid / deriveJobId // deterministic identifiers
generateHybridKeypair // standalone keygen
// Vault contract methods (callable from any contract)
put(sid, workerId, ciphertext, authorized, expiresAt)
rotate(sid, workerId, ciphertext, expiresAt)
authorize(sid, contractAddr)
revoke(sid, contractAddr)
delete(sid)
requestDecryption(sid, jobId, payload)
fulfillWork(jobId, status, statusCode, hash, body, error)
getSecret(sid)
getJob(jobId)
canUse(sid, contractAddr)Full design doc lives in the AsentumChain repo at notes/asentum-workers-vault-design.md. Concept overview is at Concepts: Secrets and Workers.