nact seal — a film clapperboard

nact

The agent proposes. You enact it by signing.

Give an AI agent the ability to act on nostr — it drafts the action, you enact it with a signature, it broadcasts. The agent never holds the key.

The missing safety layer for agentic nostr

Almost every “AI posts to social” integration hands the bot your credentials — which is why nobody sane points one at their real account. nact inverts that: the authorization is your signature, and the key that produces it never leaves you. An agent can compose, schedule, and publish on your behalf, but every outbound action passes a human tap. Human-in-the-loop isn’t a setting you can forget to enable — it’s the architecture.

01 · PERCEIVE

signals in

Your app feeds the agent what it’s allowed to see (pairs with Nvoy).

02 · PROPOSE

agent drafts

An unsigned event — note, reaction, follow, zap, any kind.

03 · ENACT

you sign

You approve on your channel; your key signs. Reject = nothing happens.

04 · BROADCAST

it publishes

The finished, signed event goes to the relays.

Why it’s safe

Sovereignty by construction

Keys never move

Role keys are custodial only by your explicit choice; your sovereign identity signs via NIP-46 and never touches a server.

Only you may enact

Approvals are checked against a single identity — your Telegram ID, or your npub in the nostr-DM adapter.

Discrete & reviewed

You see the exact event and a rationale before signing. No standing “post whenever” authority — ever.

Scoped autonomy

The action-out half of the pair

An agent bounded on both ends — what it may perceive, and what it may do.

governsdirection
Nvoywhat the agent may perceive — scoped, revocable data grantsin
nactwhat the agent may do — signature-gated actionsout

Quickstart

Wire an agent in a dozen lines

// the agent drafts; nact routes it to you; your key signs on approval
import { Nact } from 'nact'
import { telegramApproval } from 'nact/adapters/telegram'

const nact = new Nact({
  identities: { nave: { nsec: NAVE_NSEC }, luke: { nsec: LUKE_NSEC } },
  relays: ['wss://relay.damus.io', 'wss://nos.lol'],
  approval: telegramApproval({ botToken, approverId, webhookSecret }),
})

// your proposer (LLM + signals + voice) asks for an action to be enacted:
await nact.propose({
  identity: 'nave',
  event: { kind: 1, content: 'shipped nact today — an agent that acts only when you sign.' },
  context: 'launch note',   // shown to you, never published
})
// → you get an Approve / Reject prompt. Approve → signed & broadcast.

Ships with a Telegram approval adapter and a custodial signer today; a NIP-59 gift-wrap DM adapter (approve in any nostr client) and a NIP-46 signer (your key stays on your phone) are the nostr-native path. See the design →