Start here

A signed, published event in ten minutes. No X developer app, no API key, nothing anyone can revoke.

Ten minutes, one HTML file, no accounts to create.

1. Get the SDK#

npm install @berm/sdk

Or drop the browser build in and skip the build step entirely:

<script src="https://docs.xonly.ai/dist/berm-sdk.global.min.js"
        integrity="sha256-…" crossorigin="anonymous"></script>

The integrity hash is printed by npm run bundle and published with every release. Use it. A project whose whole claim is "public and verifiable" should not be asking you to load an unchecked script.

2. Connect, sign, publish#

<button id="connect">Connect</button>
<button id="publish">Publish</button>

<script>
  const sdk = Berm.install({ dev: true });      // dev key on localhost only

  connect.onclick = async () => {
    const session = await sdk.connect();        // may prompt, may be declined
    console.log(session.npub, session.tier, session.custody);
  };

  publish.onclick = async () => {
    const event = await sdk.signEvent({
      kind: 1,
      created_at: Math.floor(Date.now() / 1000),
      tags: [],
      content: 'hello from berm',
    });
    const receipt = await sdk.publish(event);   // two relays, or it did not publish
    console.log(receipt.success, receipt.accepted);
  };
</script>

That is the whole integration. There is no registration step, no key to obtain, and no quota, because nothing was issued to you.

3. Check your work#

receipt.success is true only when two or more relays accepted. One relay accepting is not a publish — it is a single point of failure wearing the costume of a success message. The receipt gives you the per-relay breakdown:

{
  "eventId": "3a0f1597db61…",
  "accepted": ["wss://relay.damus.io", "wss://nos.lol"],
  "failed": [],
  "success": true
}

Paste the event id into any Nostr client. It resolves, because you published to the open network rather than to us.

4. Handle the four failures#

Every one of these will happen to a real user. An app that only handles the happy path is a demo.

ErrorMeansDo
UserDeclinedErrorThe user said noReturn to the previous state, quietly. Not a red banner.
SignerUnavailableErrorSigner offline, asleep, or popup-blockedOffer a retry
NoSignerErrorNothing in this browser can signShow onboarding — every new user starts here
DevSignerMisuseErrorDev signer outside localhostYou shipped it. Remove it.
try {
  await sdk.signEvent(template);
} catch (e) {
  if (e instanceof Berm.UserDeclinedError) return;   // not an error, a decision
  showRetry(e.message);
}

5. Turn off the dev signer#

{ dev: true } gives you a raw key in localStorage. It refuses to load anywhere but localhost, a file:// page, or Node — it throws rather than warns, because every dev mode that merely warns eventually ships.

For production, give it a real signer to find:

const sdk = Berm.install({
  signer: { signerOrigin: 'https://signer.xonly.ai' },   // passkey, tier 1
  appName: 'My App',
});

If the user already has a NIP-07 extension, the SDK uses that instead — without being asked. They made a custody decision before they met you, and overriding it would be the behaviour this project exists to be an alternative to.

Next#