diff --git a/src/components/Arbor/ForumPost.ts b/src/components/Arbor/ForumPost.ts
index 280c5b1..3558b53 100644
--- a/src/components/Arbor/ForumPost.ts
+++ b/src/components/Arbor/ForumPost.ts
@@ -1,8 +1,12 @@
+import { wallet } from '@/wallet';
+import { StateController } from '@lit-app/state';
+import type { NDKUser } from '@nostr-dev-kit/ndk';
 import formatDateTime from '@utils/formatDateTime';
 import { LitElement, css, html } from 'lit';
-import { customElement, property } from 'lit/decorators.js';
+import { customElement, property, state } from 'lit/decorators.js';
 import { classMap } from 'lit/directives/class-map.js';
 
+import { ndk } from '@/ndk';
 import '@components/MarkdownContent';
 
 @customElement('arx-forum-post')
@@ -14,6 +18,16 @@ export class ForumPost extends LitElement {
   @property({ type: String }) content = '';
   @property({ type: Boolean }) isHighlighted = false;
 
+  @state() private zapAmountDialogOpen = false;
+  @state() public authorProfile: NDKUser | undefined = undefined;
+
+  override connectedCallback(): void {
+    super.connectedCallback();
+    this.authorProfile = ndk.getUser({ pubkey: this.npub });
+    new StateController(this, wallet);
+    wallet.loadWallet();
+  }
+
   static override styles = [
     css`
       .post {
@@ -171,14 +185,24 @@ export class ForumPost extends LitElement {
   }
 
   private _handleZap() {
-    alert('Zapping is not yet implemented');
-    this.dispatchEvent(
-      new CustomEvent('zap', {
-        detail: { postId: this.id, npub: this.npub },
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    // setting to false and then to true forces the dialog to open, even when it wasn't closed correctly
+    this.zapAmountDialogOpen = false;
+    setTimeout(() => {
+      this.zapAmountDialogOpen = true;
+    }, 0);
+  }
+
+  private async _doZap(e: Event) {
+    if (!(e instanceof CustomEvent)) return;
+    e.preventDefault();
+    const zapAmount = Number.parseInt(e.detail.value);
+    if (Number.isNaN(zapAmount) || zapAmount <= 10) {
+      alert('Zap amount must be greater or equal to 10');
+      return;
+    }
+    await wallet.nutZap(zapAmount, this.authorProfile!, this.id);
+
+    this.zapAmountDialogOpen = false;
   }
 
   private _handleDownzap() {
@@ -206,6 +230,16 @@ export class ForumPost extends LitElement {
     };
 
     return html`
+      <arx-prompt
+        promptText="Zap amount"
+        placeholder="Enter zap amount"
+        @save=${this._doZap}
+        @cancel=${() => {
+          this.zapAmountDialogOpen = false;
+        }}
+        showInput
+        .open=${this.zapAmountDialogOpen}
+      ></arx-prompt>
       <div class=${classMap(postClasses)} id="post-${this.id}">
         <div class="post__sidebar">
           <arx-nostr-profile
@@ -248,7 +282,7 @@ export class ForumPost extends LitElement {
               ></iconify-icon>
             </arx-button>
 
-            <arx-button label="Zap" @click=${this._handleZap} disabled>
+            <arx-button label="Zap" @click=${this._handleZap}>
               <iconify-icon
                 slot="prefix"
                 icon="mdi:lightning-bolt"
diff --git a/src/components/DateTimeSettings.ts b/src/components/DateTimeSettings.ts
index 2bebba0..0910025 100644
--- a/src/components/DateTimeSettings.ts
+++ b/src/components/DateTimeSettings.ts
@@ -1,9 +1,10 @@
 import { StyledToggle } from '@/components/General/Toggle';
+import { ArxInputChangeEvent, type StyledInput } from '@components/General/Input';
 import { LitElement, html } from 'lit';
 import { customElement, state } from 'lit/decorators.js';
 
-import '@components/General/Input';
 import '@components/General/Fieldset';
+import '@components/General/Input';
 import '@components/General/Select';
 
 interface DateTimeFormatOptions {
@@ -71,10 +72,10 @@ export class DateTimeSettings extends LitElement {
   }
 
   private handleChange(key: keyof DateTimeFormatOptions, e: Event) {
-    const target = e.target as HTMLSelectElement | HTMLInputElement | StyledToggle;
-    let value: string | boolean | undefined = target.value;
+    let value = e instanceof ArxInputChangeEvent ? e.detail.value : (e.target as HTMLSelectElement).value;
+    const target = e.target as StyledInput | HTMLSelectElement | HTMLInputElement | StyledToggle;
 
-    if (key === 'hour12' && target instanceof StyledToggle) value = target.checked;
+    if (key === 'hour12' && target instanceof StyledToggle) value = target.checked.toString();
 
     this.options = {
       ...this.options,
@@ -126,7 +127,7 @@ export class DateTimeSettings extends LitElement {
           label="Locale"
           type="text"
           .value=${this.options.locale}
-          @input=${(e: Event) => this.handleChange('locale', e)}
+          @change=${(e: Event) => this.handleChange('locale', e)}
         >
         </arx-input>
 
diff --git a/src/components/General/Button.ts b/src/components/General/Button.ts
index 526ced6..a1a5a64 100644
--- a/src/components/General/Button.ts
+++ b/src/components/General/Button.ts
@@ -186,8 +186,8 @@ export class StyledButton extends LitElement {
   }
 
   private _handleClick(e: MouseEvent) {
+    e.preventDefault();
     if (this.disabled || this.loading) {
-      e.preventDefault();
       return;
     }
 
diff --git a/src/components/General/Input.ts b/src/components/General/Input.ts
index 2936518..3b20e82 100644
--- a/src/components/General/Input.ts
+++ b/src/components/General/Input.ts
@@ -1,17 +1,35 @@
-import { LitElement, css, html } from 'lit';
-import { customElement, property } from 'lit/decorators.js';
+import { LitElement, type PropertyValues, css, html } from 'lit';
+import { customElement, property, query, state } from 'lit/decorators.js';
 import { when } from 'lit/directives/when.js';
 
+export class ArxInputChangeEvent extends CustomEvent<{ value: string }> {
+  constructor(value: string) {
+    super('change', { detail: { value } });
+  }
+}
+
 @customElement('arx-input')
 export class StyledInput extends LitElement {
   @property() placeholder = '';
   @property() value = '';
   @property({ type: Boolean }) disabled = false;
-  @property() type = 'text';
+  @property() type: 'text' | 'number' | 'password' = 'text';
   @property() name = '';
   @property({ type: Boolean }) required = false;
   @property() label = '';
 
+  @query('input') private _input!: HTMLInputElement;
+
+  @state() private _value = '';
+
+  protected override firstUpdated(_changedProperties: PropertyValues): void {
+    this._value = this.value;
+  }
+
+  protected override updated(changedProperties: PropertyValues): void {
+    if (changedProperties.has('value')) this._value = this.value;
+  }
+
   static override styles = css`
     :host {
       display: inline-block;
@@ -98,7 +116,7 @@ export class StyledInput extends LitElement {
     return html`
       ${when(this.label, () => html`<label for="input-${this.name}">${this.label}</label>`)}
       <input
-        .value=${this.value}
+        .value=${this._value}
         ?disabled=${this.disabled}
         ?required=${this.required}
         placeholder=${this.placeholder}
@@ -114,52 +132,24 @@ export class StyledInput extends LitElement {
   }
 
   private _handleKeyDown(e: KeyboardEvent) {
-    this.dispatchEvent(
-      new CustomEvent('keydown', {
-        detail: { key: e.key },
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this.dispatchEvent(new CustomEvent('keydown', { detail: { key: e.key }, composed: true, bubbles: true }));
   }
 
   private _handleKeyUp(e: KeyboardEvent) {
-    this.dispatchEvent(
-      new CustomEvent('keyup', {
-        detail: { key: e.key },
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this.dispatchEvent(new CustomEvent('keyup', { detail: { key: e.key }, composed: true, bubbles: true }));
   }
 
   private _handleFocus() {
-    this.dispatchEvent(
-      new CustomEvent('focus', {
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this.dispatchEvent(new CustomEvent('focus', { composed: true, bubbles: true }));
   }
 
   private _handleBlur() {
-    this.dispatchEvent(
-      new CustomEvent('blur', {
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this.dispatchEvent(new CustomEvent('blur', { composed: true, bubbles: true }));
   }
 
   private _handleInput(e: InputEvent) {
-    const input = e.target as HTMLInputElement;
-    this.value = input.value;
-    this.dispatchEvent(
-      new CustomEvent('input', {
-        detail: { value: this.value },
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this._value = this._input.value;
+    this.dispatchEvent(new ArxInputChangeEvent(this._value));
+    e.preventDefault();
   }
 }
diff --git a/src/components/General/Prompt.ts b/src/components/General/Prompt.ts
index 340bc64..8bb5142 100644
--- a/src/components/General/Prompt.ts
+++ b/src/components/General/Prompt.ts
@@ -1,3 +1,4 @@
+import type { ArxInputChangeEvent } from '@components/General/Input';
 import { LitElement, css, html } from 'lit';
 import { customElement, property, state } from 'lit/decorators.js';
 import { classMap } from 'lit/directives/class-map.js';
@@ -131,9 +132,8 @@ export class EvePrompt extends LitElement {
     if (e.key === 'Enter' && !e.shiftKey) this._handleSave();
   }
 
-  private _handleInputChange(e: Event) {
-    const target = e.target as HTMLInputElement;
-    this._inputValue = target.value;
+  private _handleInputChange(e: ArxInputChangeEvent) {
+    this._inputValue = e.detail.value;
   }
 
   private _handleCancel() {
@@ -166,7 +166,7 @@ export class EvePrompt extends LitElement {
                   type="text"
                   class="input-field"
                   .value=${this._inputValue}
-                  @input=${this._handleInputChange}
+                  @change=${this._handleInputChange}
                   placeholder=${this.placeholder}
                 ></arx-input>
               `
diff --git a/src/components/General/Textarea.ts b/src/components/General/Textarea.ts
index 93e2791..e799a8f 100644
--- a/src/components/General/Textarea.ts
+++ b/src/components/General/Textarea.ts
@@ -1,6 +1,7 @@
 import { LitElement, css, html } from 'lit';
 import { customElement, property } from 'lit/decorators.js';
 import { when } from 'lit/directives/when.js';
+import { ArxInputChangeEvent } from './Input';
 
 @customElement('arx-textarea')
 export class StyledTextarea extends LitElement {
@@ -201,12 +202,6 @@ export class StyledTextarea extends LitElement {
   private _handleInput(e: InputEvent) {
     const textarea = e.target as HTMLTextAreaElement;
     this.value = textarea.value;
-    this.dispatchEvent(
-      new CustomEvent('input', {
-        detail: { value: this.value },
-        bubbles: true,
-        composed: true,
-      }),
-    );
+    this.dispatchEvent(new ArxInputChangeEvent(this.value));
   }
 }
diff --git a/src/components/Header.ts b/src/components/Header.ts
index 4834ab3..e8745c9 100644
--- a/src/components/Header.ts
+++ b/src/components/Header.ts
@@ -1,4 +1,5 @@
 import { ndk } from '@/ndk';
+import type { ArxInputChangeEvent } from '@components/General/Input';
 import type { NDKEvent } from '@nostr-dev-kit/ndk';
 import * as nip19 from '@nostr/tools/nip19';
 import { LitElement, css, html } from 'lit';
@@ -8,8 +9,8 @@ import { keyed } from 'lit/directives/keyed.js';
 import { map } from 'lit/directives/map.js';
 import { when } from 'lit/directives/when.js';
 
-import '@components/HeaderSugestion';
 import '@components/General/Input';
+import '@components/HeaderSugestion';
 
 @customElement('arx-header')
 export class Header extends LitElement {
@@ -49,10 +50,10 @@ export class Header extends LitElement {
     .nav-buttons {
       display: flex;
       gap: var(--space-xs, 0.5rem);
-      padding-right: var(--space-xs, 0.5rem);
+      padding: 0 var(--space-xs, 0.5rem);
     }
 
-    .nav-buttons button {
+    button {
       text-decoration: none;
       color: var(--color-primary-content);
       background: oklch(from var(--color-primary-content) l c h / 0.1);
@@ -66,23 +67,21 @@ export class Header extends LitElement {
       align-items: center;
       justify-content: center;
       transition: all 0.2s ease;
-    }
+      &:hover {
+        background: oklch(from var(--color-primary-content) l c h / 0.2);
+        transform: translateY(-2px);
+        box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
+          calc(var(--depth) * 4px)
+          oklch(from var(--color-base-content) l c h / 0.15);
+      }
+      &:active {
+        transform: translateY(1px);
+      }
 
-    .nav-buttons button:hover {
-      background: oklch(from var(--color-primary-content) l c h / 0.2);
-      transform: translateY(-2px);
-      box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
-        calc(var(--depth) * 4px)
-        oklch(from var(--color-base-content) l c h / 0.15);
-    }
-
-    .nav-buttons button:active {
-      transform: translateY(1px);
-    }
-
-    .nav-buttons button.disabled {
-      opacity: 0.5;
-      pointer-events: none;
+      &.disabled {
+        opacity: 0.5;
+        pointer-events: none;
+      }
     }
 
     .search-container {
@@ -153,7 +152,7 @@ export class Header extends LitElement {
             placeholder=${this.url}
             @keyup=${this._handleSearch}
             @focus=${this._handleFocus}
-            @input=${this._handleInput}
+            @change=${this._handleInput}
           ></arx-input>
           ${when(
             this.showSuggestions,
@@ -175,16 +174,25 @@ export class Header extends LitElement {
             `,
           )}
         </div>
+        <div class="nav-buttons">
+          <button @click=${this._goToWallet}>
+            <iconify-icon icon="material-symbols:wallet"></iconify-icon>
+          </button>
+        </div>
       </header>
     `;
   }
 
+  private _goToWallet() {
+    window.location.hash = 'wallet';
+  }
+
   private _handleFocus() {
     this.showSuggestions = true;
   }
 
-  private _handleInput(e: InputEvent) {
-    this.searchQuery = (e.target as HTMLInputElement).value;
+  private _handleInput(e: ArxInputChangeEvent) {
+    this.searchQuery = e.detail.value;
     if (this._debounceTimeout) {
       clearTimeout(this._debounceTimeout);
     }
@@ -245,8 +253,6 @@ export class Header extends LitElement {
   private _handleSearch(e: KeyboardEvent) {
     if (e.key !== 'Enter') return;
 
-    const target = e.target as HTMLInputElement;
-    this.searchQuery = target.value;
     this.showSuggestions = false;
 
     if (this.searchQuery.startsWith('npub1')) {
diff --git a/src/components/InitialSetup.ts b/src/components/InitialSetup.ts
index a947959..06210c9 100644
--- a/src/components/InitialSetup.ts
+++ b/src/components/InitialSetup.ts
@@ -1,4 +1,5 @@
 import { ndk, setSigner } from '@/ndk';
+import type { ArxInputChangeEvent } from '@components/General/Input';
 import { animate } from '@lit-labs/motion';
 import { randomBytes } from '@noble/ciphers/webcrypto';
 import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
@@ -10,10 +11,10 @@ import { encodeBase64 } from '@std/encoding/base64';
 import { LitElement, css, html } from 'lit';
 import { customElement, state } from 'lit/decorators.js';
 
-import '@components/LoadingView';
 import '@components/General/Button';
-import '@components/General/Input';
 import '@components/General/Fieldset';
+import '@components/General/Input';
+import '@components/LoadingView';
 
 @customElement('arx-initial-setup')
 export class InitialSetup extends LitElement {
@@ -201,8 +202,8 @@ export class InitialSetup extends LitElement {
     }, 300);
   }
 
-  private onSeedPhraseInput(event: Event) {
-    this.seedPhrase = (event.target as HTMLInputElement).value;
+  private onSeedPhraseInput(event: ArxInputChangeEvent) {
+    this.seedPhrase = event.detail.value;
   }
 
   private generateSeedPhrase() {
@@ -300,7 +301,7 @@ export class InitialSetup extends LitElement {
           </p>
           <div class="input-group">
             <arx-input
-              @input=${this.onSeedPhraseInput}
+              @change=${this.onSeedPhraseInput}
               .value=${this.seedPhrase}
               id="seed-input"
               type="text"
@@ -381,16 +382,16 @@ export class InitialSetup extends LitElement {
     `;
   }
 
-  private onUserNameInput(e: Event) {
-    this.userName = (e.target as HTMLInputElement).value;
+  private onUserNameInput(e: CustomEvent<{ value: string }>) {
+    this.userName = e.detail.value;
   }
 
-  private onProfileImageInput(e: Event) {
-    this.profileImage = (e.target as HTMLInputElement).value;
+  private onProfileImageInput(e: CustomEvent<{ value: string }>) {
+    this.profileImage = e.detail.value;
   }
 
-  private onLightningAddressInput(e: Event) {
-    this.lightningAddress = (e.target as HTMLInputElement).value;
+  private onLightningAddressInput(e: CustomEvent<{ value: string }>) {
+    this.lightningAddress = e.detail.value;
   }
 
   private renderPageFour() {
@@ -409,7 +410,7 @@ export class InitialSetup extends LitElement {
                 id="username"
                 type="text"
                 .value=${this.userName}
-                @input=${this.onUserNameInput}
+                @change=${this.onUserNameInput}
                 placeholder="Enter your name"
               ></arx-input>
             </arx-fieldset>
@@ -418,7 +419,7 @@ export class InitialSetup extends LitElement {
                 id="profile-image"
                 type="text"
                 .value=${this.profileImage}
-                @input=${this.onProfileImageInput}
+                @change=${this.onProfileImageInput}
                 placeholder="Enter image URL"
               ></arx-input>
               <small class="note">
@@ -464,7 +465,7 @@ export class InitialSetup extends LitElement {
             id="lightning-address"
             type="text"
             .value=${this.lightningAddress}
-            @input=${this.onLightningAddressInput}
+            @change=${this.onLightningAddressInput}
             placeholder="your@lightning.address"
           /></arx-input>
           <small class="note">
diff --git a/src/components/WalletTransactionLine.ts b/src/components/WalletTransactionLine.ts
new file mode 100644
index 0000000..687ba2f
--- /dev/null
+++ b/src/components/WalletTransactionLine.ts
@@ -0,0 +1,100 @@
+import { StateController } from '@lit-app/state';
+import { LitElement, css, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+
+import { type WalletHistory, wallet } from '@/wallet';
+
+import formatDateTime from '@/utils/formatDateTime';
+import satsComma from '@/utils/satsComma';
+import '@components/General/Card';
+import '@components/General/Input';
+
+@customElement('arx-wallet-transaction-line')
+export class WalletTransactionLine extends LitElement {
+  @property({ type: Object }) transaction!: WalletHistory;
+
+  override connectedCallback(): void {
+    super.connectedCallback();
+    new StateController(this, wallet);
+  }
+
+  static override styles = css`
+    :host {
+      display: block;
+      width: 100%;
+      border-bottom: 1px solid var(--color-base-200);
+      container-type: inline-size;
+    }
+
+    .transaction {
+      display: grid;
+      grid-template-columns: 200px 1fr auto;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px 0;
+    }
+
+    .in {
+      color: var(--color-success);
+    }
+
+    .out {
+      color: var(--color-error);
+    }
+
+    .amount {
+      text-align: right;
+      font-family: var(--font-mono);
+      font-weight: bold;
+      margin-right: 10px;
+    }
+
+    .date {
+      color: var(--color-secondary);
+      font-size: 0.9em;
+    }
+
+    .profiles {
+      display: flex;
+      gap: 10px;
+    }
+
+    .sender {
+      order: 1;
+    }
+
+    .recipient {
+      order: 2;
+    }
+
+    @container (max-width: 400px) {
+      .transaction {
+        grid-template-columns: 1fr;
+      }
+
+      .amount {
+        text-align: left;
+      }
+    }
+  `;
+
+  override render() {
+    return html`<div class="transaction ${this.transaction.direction}">
+      <span class="amount">
+        ${this.transaction.direction === 'in' ? '+' : '-'} ${satsComma(this.transaction.amount)}
+        sats
+      </span>
+      <span class="date">${formatDateTime(this.transaction.created_at * 1000)}</span>
+      <div class="profiles">
+        <arx-nostr-profile
+          pubkey=${this.transaction.senderPubkey}
+          class="sender"
+        ></arx-nostr-profile>
+        <arx-nostr-profile
+          pubkey=${this.transaction.recipientPubkey}
+          class="recipient"
+        ></arx-nostr-profile>
+      </div>
+    </div>`;
+  }
+}
diff --git a/src/components/Widgets/WalletWidget.ts b/src/components/Widgets/WalletWidget.ts
new file mode 100644
index 0000000..492b895
--- /dev/null
+++ b/src/components/Widgets/WalletWidget.ts
@@ -0,0 +1,82 @@
+import satsComma from '@/utils/satsComma';
+import { wallet } from '@/wallet';
+import '@components/General/Fieldset';
+import '@components/LoadingView';
+import '@components/WalletTransactionLine';
+import { StateController } from '@lit-app/state';
+import { LitElement, css, html } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { when } from 'lit/directives/when.js';
+
+@customElement('arx-wallet-widget')
+export class WalletWidget extends LitElement {
+  @state()
+  public loading = false;
+
+  override connectedCallback(): void {
+    super.connectedCallback();
+    new StateController(this, wallet);
+  }
+
+  override async firstUpdated() {
+    this.loading = true;
+    await wallet.loadWallet();
+    this.loading = false;
+  }
+
+  static override styles = [
+    css`
+      :host {
+        display: block;
+      }
+
+      .widget-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+        border-bottom: var(--border) solid var(--color-base-300);
+        padding-bottom: 8px;
+      }
+
+      .widget-title {
+        font-size: 1.2rem;
+        font-weight: 600;
+        margin: 0;
+        color: var(--color-base-content);
+        display: flex;
+        align-items: center;
+        gap: 8px;
+      }
+
+      .bitcoin-icon {
+        color: var(--color-warning);
+        font-size: 1.4rem;
+      }
+    `,
+  ];
+
+  override render() {
+    return html`
+      <div class="widget-header">
+        <h3 class="widget-title">
+          <span class="bitcoin-icon">₿</span> Bitcoin Wallet
+        </h3>
+      </div>
+      <arx-fieldset .legend=${when(
+        this.loading,
+        () => 'Loading...',
+        () => `${satsComma(wallet.balance)} sats`,
+      )}>
+        ${when(
+          wallet.sortedHistory.length > 0,
+          () => html`
+            Latest Transaction:
+            <arx-wallet-transaction-line .transaction=${wallet.sortedHistory[0]}></arx-wallet-transaction-line>
+          `,
+          () => 'No transactions yet',
+        )}
+      </arx-fieldset>
+    `;
+  }
+}
diff --git a/src/routes/Arbor/NewPost.ts b/src/routes/Arbor/NewPost.ts
index 8a3f8b5..ab2a6c3 100644
--- a/src/routes/Arbor/NewPost.ts
+++ b/src/routes/Arbor/NewPost.ts
@@ -1,3 +1,4 @@
+import type { ArxInputChangeEvent } from '@/components/General/Input';
 import { getSigner, ndk } from '@/ndk';
 import { NDKEvent } from '@nostr-dev-kit/ndk';
 import { LitElement, css, html } from 'lit';
@@ -106,8 +107,8 @@ export class ArborPostCreator extends LitElement {
     }
   }
 
-  private handleContentInput(e: InputEvent) {
-    this.postContent = (e.target as HTMLTextAreaElement).value;
+  private handleContentInput(e: ArxInputChangeEvent) {
+    this.postContent = e.detail.value;
     if (this.error && this.postContent.length >= 10) {
       this.error = null;
     }
@@ -121,7 +122,7 @@ export class ArborPostCreator extends LitElement {
         <arx-textarea
           placeholder="Post. You can use Markdown here."
           .value=${this.postContent}
-          @input=${this.handleContentInput}
+          @change=${this.handleContentInput}
           ?disabled=${this.isCreating}
         ></arx-textarea>
 
diff --git a/src/routes/Arbor/NewTopic.ts b/src/routes/Arbor/NewTopic.ts
index 23f3403..7228bee 100644
--- a/src/routes/Arbor/NewTopic.ts
+++ b/src/routes/Arbor/NewTopic.ts
@@ -1,11 +1,12 @@
 import { getSigner, ndk } from '@/ndk';
+import type { ArxInputChangeEvent } from '@components/General/Input';
 import { NDKEvent } from '@nostr-dev-kit/ndk';
 import { LitElement, css, html } from 'lit';
 import { customElement, property, state } from 'lit/decorators.js';
 
+import '@components/General/Button';
 import '@components/General/Input';
 import '@components/General/Textarea';
-import '@components/General/Button';
 
 @customElement('arx-arbor-topic-creator')
 export class ArborTopicCreator extends LitElement {
@@ -86,12 +87,12 @@ export class ArborTopicCreator extends LitElement {
     }
   }
 
-  private handleTopicInput(e: InputEvent) {
-    this.newTopic = (e.target as HTMLInputElement).value;
+  private handleTopicInput(e: ArxInputChangeEvent) {
+    this.newTopic = e.detail.value;
   }
 
-  private handleContentInput(e: InputEvent) {
-    this.topicContent = (e.target as HTMLTextAreaElement).value;
+  private handleContentInput(e: ArxInputChangeEvent) {
+    this.topicContent = e.detail.value;
   }
 
   override render() {
@@ -102,14 +103,14 @@ export class ArborTopicCreator extends LitElement {
         type="text"
         placeholder="New Topic"
         .value=${this.newTopic}
-        @input=${this.handleTopicInput}
+        @change=${this.handleTopicInput}
         ?disabled=${this.isCreating}
       ></arx-input>
 
       <arx-textarea
         placeholder="Topic. You can use Markdown here."
         .value=${this.topicContent}
-        @input=${this.handleContentInput}
+        @change=${this.handleContentInput}
         ?disabled=${this.isCreating}
       ></arx-textarea>
 
diff --git a/src/routes/Home.ts b/src/routes/Home.ts
index dc0736d..3950b8e 100644
--- a/src/routes/Home.ts
+++ b/src/routes/Home.ts
@@ -5,10 +5,11 @@ import { customElement, state } from 'lit/decorators.js';
 import { map } from 'lit/directives/map.js';
 import { html, literal } from 'lit/static-html.js';
 
-import '@widgets/BitcoinBlockWidget';
 import '@components/AppGrid';
-import '@components/NostrAvatar';
 import '@components/General/Card';
+import '@components/NostrAvatar';
+import '@components/Widgets/BitcoinBlockWidget';
+import '@components/Widgets/WalletWidget';
 
 @customElement('arx-eve-home')
 export class Home extends LitElement {
@@ -83,6 +84,10 @@ export class Home extends LitElement {
       title: 'Bitcoin Block',
       content: literal`arx-bitcoin-block-widget`,
     },
+    {
+      title: 'Bitcoin Wallet',
+      content: literal`arx-wallet-widget`,
+    },
   ];
 
   async loadProperties() {
@@ -95,60 +100,71 @@ export class Home extends LitElement {
 
   static override styles = [
     css`
+      ::-webkit-scrollbar {
+        width: 12px;
+        height: 12px;
+      }
+
+      ::-webkit-scrollbar-track {
+        background: var(--color-base-200);
+        border-radius: var(--radius-field);
+      }
+
+      ::-webkit-scrollbar-thumb {
+        background: var(--color-base-300);
+        border-radius: var(--radius-field);
+        border: 2px solid var(--color-base-200);
+        transition: var(--transition);
+      }
+
+      ::-webkit-scrollbar-thumb:hover {
+        background: var(--color-neutral);
+      }
+
       .content-wrapper {
+        margin: auto;
+        height: 90vh;
+        overflow: hidden;
         display: flex;
+        align-items: center;
+        justify-content: center;
         gap: 20px;
         width: 100%;
         max-width: 1200px;
-        margin: auto;
-      }
-
-      .home {
-        min-height: calc(100vh - var(--font-2xl));
-        width: 100%;
-        position: absolute;
-        right: 0;
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        align-items: center;
       }
 
       .home-container {
         flex: 1;
         padding: 0;
+        height: auto;
       }
 
       .widgets-container {
+        height: 75vh;
+        overflow: auto;
         width: 350px;
-        display: flex;
-        flex-direction: column;
-        gap: 20px;
-
-        & > * {
-          flex: 1;
-        }
+        margin: 0 auto;
       }
 
       @media (max-width: 1024px) {
         .content-wrapper {
+          gap: 0;
+          height: auto;
           flex-direction: column;
         }
 
+        .home-container {
+          width: 100%;
+        }
+
         .widgets-container {
+          overflow: unset;
+          height: auto;
           width: calc(100vw - 40px);
-          flex-direction: row;
-          overflow-x: auto;
           padding-bottom: 10px;
         }
       }
 
-      @media (max-width: 768px) {
-        .widgets-container {
-          flex-direction: column;
-        }
-      }
-
       .welcome-section {
         display: flex;
         flex-direction: column;
@@ -173,23 +189,21 @@ export class Home extends LitElement {
 
   override render() {
     return html`
-      <div class="home">
-        <div class="content-wrapper">
-          <arx-card class="home-container">
-            <arx-card class="welcome-section">
-              <arx-nostr-avatar
-                .profile=${this.profile}
-                size="huge"
-              ></arx-nostr-avatar>
-              <div class="welcome-text">
-                <h1>Welcome, ${this.username}</h1>
-              </div>
-            </arx-card>
-            <arx-app-grid .apps=${this.apps}></arx-app-grid>
+      <div class="content-wrapper">
+        <arx-card class="home-container">
+          <arx-card class="welcome-section">
+            <arx-nostr-avatar
+              .profile=${this.profile}
+              size="huge"
+            ></arx-nostr-avatar>
+            <div class="welcome-text">
+              <h1>Welcome, ${this.username}</h1>
+            </div>
           </arx-card>
-          <div class="widgets-container">
-            ${map(this.widgets, (widget) => html`<arx-card><${widget.content}></${widget.content}></arx-card>`)}
-          </div>
+          <arx-app-grid .apps=${this.apps}></arx-app-grid>
+        </arx-card>
+        <div class="widgets-container">
+          ${map(this.widgets, (widget) => html`<arx-card><${widget.content}></${widget.content}></arx-card>`)}
         </div>
       </div>
     `;
diff --git a/src/routes/Settings.ts b/src/routes/Settings.ts
index cdcb210..6e3ddf1 100644
--- a/src/routes/Settings.ts
+++ b/src/routes/Settings.ts
@@ -1,16 +1,17 @@
 import defaultAvatar from '@/default-avatar.png';
 import { getSigner, getUserProfile, ndk } from '@/ndk';
+import type { ArxInputChangeEvent } from '@components/General/Input';
 import { NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk';
 import { LitElement, css, html } from 'lit';
 import { customElement, state } from 'lit/decorators.js';
 import { when } from 'lit/directives/when.js';
 
-import '@components/DateTimeSettings';
-import '@components/General/Input';
-import '@components/General/Button';
-import '@components/General/Fieldset';
-import '@components/General/Card';
 import '@components/Breadcrumbs';
+import '@components/DateTimeSettings';
+import '@components/General/Button';
+import '@components/General/Card';
+import '@components/General/Fieldset';
+import '@components/General/Input';
 
 @customElement('arx-settings')
 export class EveSettings extends LitElement {
@@ -73,11 +74,10 @@ export class EveSettings extends LitElement {
     }
   }
 
-  private handleInputChange(e: Event) {
-    const target = e.target as HTMLInputElement;
+  private handleInputChange(field: string, e: ArxInputChangeEvent) {
     this.profile = {
       ...this.profile,
-      [target.name]: target.value,
+      [field]: e.detail.value,
     };
   }
 
@@ -144,7 +144,7 @@ export class EveSettings extends LitElement {
             type="text"
             name="name"
             .value=${this.profile.name}
-            @input=${this.handleInputChange}
+            @change=${(e: ArxInputChangeEvent) => this.handleInputChange('name', e)}
             placeholder="Your display name"
           ></arx-input>
 
@@ -153,7 +153,7 @@ export class EveSettings extends LitElement {
             type="text"
             name="image"
             .value=${this.profile.picture}
-            @input=${this.handleInputChange}
+            @change=${(e: ArxInputChangeEvent) => this.handleInputChange('picture', e)}
             placeholder="https://example.com/your-image.jpg"
           ></arx-input>
 
@@ -162,7 +162,7 @@ export class EveSettings extends LitElement {
             type="text"
             name="banner"
             .value=${this.profile.banner}
-            @input=${this.handleInputChange}
+            @change=${(e: ArxInputChangeEvent) => this.handleInputChange('banner', e)}
             placeholder="https://example.com/your-image.jpg"
           ></arx-input>
         </arx-fieldset>
diff --git a/src/routes/Wallet.ts b/src/routes/Wallet.ts
new file mode 100644
index 0000000..8d3d54d
--- /dev/null
+++ b/src/routes/Wallet.ts
@@ -0,0 +1,78 @@
+import { wallet } from '@/wallet';
+import type { ArxInputChangeEvent } from '@components/General/Input';
+import { StateController } from '@lit-app/state';
+import { LitElement, css, html } from 'lit';
+import { customElement } from 'lit/decorators.js';
+
+import '@components/General/Card';
+import '@components/General/Input';
+import '@components/WalletTransactionLine';
+
+@customElement('arx-wallet-route')
+export class WalletRoute extends LitElement {
+  static override styles = css`
+    h1 {
+      text-align: center;
+    }
+
+    .transaction-list {
+      background-color: var(--color-base-100);
+      border-radius: var(--radius-card);
+      padding: 20px;
+      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    }
+
+    .transaction-list h3 {
+      color: var(--color-base-content);
+      margin-bottom: 20px;
+    }
+
+    arx-wallet-transaction-line:last-child {
+      border-bottom: none;
+    }
+  `;
+
+  private inputToken = '';
+
+  override async firstUpdated() {
+    await wallet.loadWallet();
+  }
+
+  async doReceiveToken() {
+    console.log('clicked', Date.now());
+    await wallet.receiveToken(this.inputToken);
+    this.inputToken = '';
+  }
+
+  override connectedCallback(): void {
+    super.connectedCallback();
+    new StateController(this, wallet);
+  }
+
+  override render() {
+    return html`
+      <arx-card>
+        <h1>Wallet</h1>
+        <hr />
+        <h2>You have ${wallet.balance} sats</h2>
+        <arx-input
+          label="Token"
+          .value=${this.inputToken}
+          @change=${(e: ArxInputChangeEvent) => {
+            if (!e.detail.value) return;
+            this.inputToken = e.detail.value;
+          }}
+          type="text"
+          id="token"
+        ></arx-input>
+        <arx-button label="Receive" @click=${this.doReceiveToken}></arx-button>
+        <div class="transaction-list">
+          <h3>Transaction History</h3>
+          ${wallet.sortedHistory.map(
+            (h) => html`<arx-wallet-transaction-line .transaction=${h}></arx-wallet-transaction-line>`,
+          )}
+        </div>
+      </arx-card>
+    `;
+  }
+}
diff --git a/src/routes/router.ts b/src/routes/router.ts
index 235a129..ab7a796 100644
--- a/src/routes/router.ts
+++ b/src/routes/router.ts
@@ -1,12 +1,13 @@
+import '@components/InitialSetup';
 import '@routes/404Page';
+import '@routes/Arbor/Home';
+import '@routes/Arbor/NewPost';
+import '@routes/Arbor/NewTopic';
+import '@routes/Arbor/TopicView';
 import '@routes/Home';
 import '@routes/Profile';
 import '@routes/Settings';
-import '@routes/Arbor/Home';
-import '@routes/Arbor/NewTopic';
-import '@routes/Arbor/TopicView';
-import '@routes/Arbor/NewPost';
-import '@components/InitialSetup';
+import '@routes/Wallet';
 
 import { spread } from '@open-wc/lit-helpers';
 import { LitElement, css } from 'lit';
@@ -65,6 +66,11 @@ export default class EveRouter extends LitElement {
       params: {},
       component: literal`arx-settings`,
     },
+    {
+      pattern: 'wallet',
+      params: {},
+      component: literal`arx-wallet-route`,
+    },
     {
       pattern: '404',
       params: {},
@@ -92,6 +98,27 @@ export default class EveRouter extends LitElement {
       overflow: hidden;
     }
 
+    ::-webkit-scrollbar {
+      width: 12px;
+      height: 12px;
+    }
+
+    ::-webkit-scrollbar-track {
+      background: var(--color-base-200);
+      border-radius: var(--radius-field);
+    }
+
+    ::-webkit-scrollbar-thumb {
+      background: var(--color-base-300);
+      border-radius: var(--radius-field);
+      border: 2px solid var(--color-base-200);
+      transition: var(--transition);
+    }
+
+    ::-webkit-scrollbar-thumb:hover {
+      background: var(--color-neutral);
+    }
+
     .window {
       overflow: auto;
     }
@@ -103,6 +130,10 @@ export default class EveRouter extends LitElement {
       margin: 0 auto;
       padding: 1rem;
     }
+
+    .hide-overflow {
+      overflow: hidden;
+    }
   `;
 
   constructor() {
@@ -268,7 +299,7 @@ export default class EveRouter extends LitElement {
         @go-forward=${this.goForward}
         title="Eve"
       ></arx-header>
-      <div class="window">
+      <div class="window ${this.currentRoute.pattern === 'home' ? 'hide-overflow' : ''}">
         <div class="window-content">
           ${keyed(
             this.currentRoute.params,
diff --git a/src/style.css b/src/style.css
index a6a8efb..bb02569 100644
--- a/src/style.css
+++ b/src/style.css
@@ -43,6 +43,8 @@
   --border: 2px;
   --depth: 1;
   --noise: 1;
+
+  --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
 }
 
 body.dark {
diff --git a/src/wallet.ts b/src/wallet.ts
new file mode 100644
index 0000000..3d1fc09
--- /dev/null
+++ b/src/wallet.ts
@@ -0,0 +1,306 @@
+import { CashuMint, CashuWallet, type Proof, type SendOptions } from '@cashu/cashu-ts';
+import { bytesToHex } from '@noble/ciphers/utils';
+import { secp256k1 } from '@noble/curves/secp256k1';
+import { NDKEvent, type NDKEventId, NDKKind, type NDKTag, type NDKUser } from '@nostr-dev-kit/ndk';
+import { getSigner, ndk } from './ndk';
+
+import { State, property } from '@lit-app/state';
+
+export type WalletHistory = {
+  id: string;
+  amount: number;
+  created_at: number;
+  direction: 'in' | 'out';
+  tokenEventId: NDKEventId;
+  nutZapEventId?: NDKEventId;
+  senderPubkey?: string;
+  recipientPubkey: string;
+};
+
+class Wallet extends State {
+  static mintUrl = 'https://testnut.cashu.space'; // TODO: this should be user controlled
+
+  private walletEvent: NDKEvent | null = null;
+  private cashuWallet: CashuWallet = new CashuWallet(new CashuMint(Wallet.mintUrl));
+
+  private proofs: Proof[] = [];
+  private eventIdToProofIdMap: Map<NDKEventId, [string, string][]> = new Map();
+
+  private isHandlingZapEvent = false;
+  private zapEventsQueue: NDKEvent[] = [];
+  private latestHistoryTimestamp = 0;
+
+  @property() public balance = 0;
+  @property() public history: WalletHistory[] = [];
+  @property() public sortedHistory: WalletHistory[] = [];
+
+  async loadWallet() {
+    if (this.walletEvent) return;
+    await getSigner();
+    const user = await ndk.signer!.user()!;
+    const privateWalletEvent = await ndk.fetchEvent({
+      kinds: [17375 as NDKKind],
+      authors: [user.pubkey],
+    });
+    if (!privateWalletEvent) {
+      const walletPrivateKey = secp256k1.utils.randomPrivateKey();
+      const walletPublicKey = secp256k1.getPublicKey(walletPrivateKey);
+      const newPrivateWalletEvent = new NDKEvent(ndk);
+      newPrivateWalletEvent.kind = NDKKind.CashuWallet;
+      newPrivateWalletEvent.content = await ndk.signer!.encrypt(
+        user,
+        JSON.stringify([
+          ['privkey', bytesToHex(walletPrivateKey)],
+          ['mint', Wallet.mintUrl],
+        ]),
+        'nip44',
+      );
+      const publicWalletEvent = new NDKEvent(ndk);
+      publicWalletEvent.kind = NDKKind.CashuMintList;
+      publicWalletEvent.content = '';
+      publicWalletEvent.tags = [
+        ['mint', Wallet.mintUrl, 'sat'],
+        ['pubkey', bytesToHex(walletPublicKey)],
+      ];
+      await newPrivateWalletEvent.sign();
+      await newPrivateWalletEvent.publish();
+      await publicWalletEvent.sign();
+      await publicWalletEvent.publish();
+      this.walletEvent = newPrivateWalletEvent;
+    } else this.walletEvent = privateWalletEvent;
+
+    const walletHistory = await ndk.fetchEvents({
+      kinds: [NDKKind.CashuWalletTx],
+      authors: [user.pubkey],
+      since: this.walletEvent?.created_at,
+    });
+
+    for (const event of walletHistory) {
+      await this.processHistoryEvent(event);
+    }
+
+    const proofsEvents = await ndk.fetchEvents({
+      kinds: [NDKKind.CashuToken],
+      authors: [user.pubkey],
+    });
+
+    for (const proofsEvent of proofsEvents) {
+      const {
+        mint,
+        proofs = [],
+        del = [],
+      } = JSON.parse(await ndk.signer!.decrypt(user, proofsEvent.content, 'nip44')) as {
+        mint: string;
+        proofs: Proof[];
+        del?: NDKEventId[];
+      };
+
+      if (mint !== this.cashuWallet.mint.mintUrl) continue;
+
+      for (const eventId of del) {
+        const toDeletes = this.eventIdToProofIdMap.get(eventId);
+        if (toDeletes)
+          for (const proof of toDeletes)
+            this.proofs = this.proofs.filter((p) => p.secret !== proof[0] || p.C !== proof[1]);
+        this.eventIdToProofIdMap.delete(eventId);
+      }
+
+      for (const proof of proofs) this.proofs.push(proof);
+
+      this.eventIdToProofIdMap.set(
+        proofsEvent.id,
+        proofs.map((p) => [p.secret, p.C]),
+      );
+    }
+
+    ndk
+      .subscribe({
+        kinds: [NDKKind.Nutzap],
+        '#p': [user.pubkey],
+        since: this.latestHistoryTimestamp,
+      })
+      .on('event', async (event) => {
+        this.zapEventsQueue.push(event);
+        this.handleZapEvent();
+      });
+
+    ndk
+      .subscribe({
+        kinds: [NDKKind.CashuWalletTx],
+        authors: [user.pubkey],
+        since: this.latestHistoryTimestamp,
+      })
+      .on('event', async (event) => {
+        await this.processHistoryEvent(event);
+      });
+
+    this.recalculateBalance();
+  }
+
+  private async processHistoryEvent(event: NDKEvent) {
+    const user = await ndk.signer!.user()!;
+    const parsedContent = JSON.parse(await ndk.signer!.decrypt(user, event.content, 'nip44'));
+    const direction = parsedContent.find((p: [string, string]) => p[0] === 'direction')?.[1];
+    const amount = parsedContent.find((p: [string, string]) => p[0] === 'amount')?.[1];
+    const tokenEventId = parsedContent.find((p: [string, string]) => p[0] === 'e')?.[1];
+    const nutZapEventId = event.tags.find((t: NDKTag) => t[0] === 'e')?.[1];
+    const senderPubkey = event.tags.find((t: NDKTag) => t[0] === 'p')?.[1];
+    if (!nutZapEventId || !senderPubkey) return;
+    this.history.push({
+      id: event.id,
+      amount: Number.parseInt(amount),
+      created_at: event.created_at ?? 0,
+      direction: direction as 'in' | 'out',
+      tokenEventId,
+      nutZapEventId: nutZapEventId as NDKEventId,
+      senderPubkey,
+      recipientPubkey: event.pubkey,
+    });
+    this.latestHistoryTimestamp = Math.max(this.latestHistoryTimestamp, event.created_at ?? 0);
+    this.sortedHistory = [...this.history.sort((a, b) => b.created_at - a.created_at)];
+  }
+
+  private async handleZapEvent() {
+    if (this.isHandlingZapEvent) return;
+    const event = this.zapEventsQueue.shift();
+    if (!event) return;
+    this.isHandlingZapEvent = true;
+    const proofs = JSON.parse(event.tags.find((t) => t[0] === 'proof')?.[1] ?? '[]') as Proof[];
+    this.proofs.push(...proofs);
+
+    const newProofsEvent = await this.createProofsEvent();
+    await this.createHistoryEvent(
+      newProofsEvent,
+      this.proofs.reduce((acc, curr) => acc + curr.amount, 0),
+      'in',
+    );
+    this.recalculateBalance();
+    this.isHandlingZapEvent = false;
+  }
+
+  private async createProofsEvent(): Promise<NDKEvent> {
+    const newProofsEvent = new NDKEvent(ndk);
+    newProofsEvent.kind = NDKKind.CashuToken;
+    newProofsEvent.content = await ndk.signer?.encrypt(
+      await ndk.signer!.user()!,
+      JSON.stringify({
+        mint: this.cashuWallet.mint.mintUrl,
+        proofs: this.proofs,
+        del: Array.from(this.eventIdToProofIdMap.keys()),
+      }),
+      'nip44',
+    )!;
+    await newProofsEvent.sign();
+    await newProofsEvent.publish();
+    return newProofsEvent;
+  }
+
+  recalculateBalance() {
+    this.proofs = this.proofs.filter(
+      (p, index, self) => index === self.findIndex((t) => t.secret === p.secret && t.C === p.C),
+    );
+    this.balance = this.proofs.reduce((acc, curr) => acc + curr.amount, 0);
+  }
+
+  private async createHistoryEvent(newProofsEvent: NDKEvent, amount: number, direction: 'in' | 'out') {
+    const historyEvent = new NDKEvent(ndk);
+    historyEvent.kind = NDKKind.CashuWalletTx;
+    historyEvent.content = await ndk.signer?.encrypt(
+      await ndk.signer!.user()!,
+      JSON.stringify([
+        ['direction', direction],
+        ['amount', amount.toString()],
+        ['e', newProofsEvent.id],
+      ]),
+      'nip44',
+    )!;
+    historyEvent.tags = [
+      ['e', newProofsEvent.id],
+      ['p', newProofsEvent.pubkey],
+    ];
+    await historyEvent.sign();
+    await historyEvent.publish();
+  }
+
+  /** @throws Error if token is invalid */
+  async receiveToken(token: string) {
+    const proofs = await this.cashuWallet.receive(token);
+    if (proofs.length === 0) throw new Error('Invalid token');
+    for (const proof of proofs) this.proofs.push(proof);
+    const newProofsEvent = await this.createProofsEvent();
+    const amount = proofs.reduce((acc, curr) => acc + curr.amount, 0);
+    await this.createHistoryEvent(newProofsEvent, amount, 'in');
+    this.recalculateBalance();
+  }
+
+  private async createToken(amount: number, recipient?: NDKUser) {
+    if (amount < 10) throw new Error('Amount must be greater than 10');
+    const sendOptions: SendOptions = {
+      includeFees: true,
+    };
+    if (recipient) {
+      const mintListEvent = await ndk.fetchEvent({
+        kinds: [NDKKind.CashuMintList],
+        authors: [recipient.pubkey],
+      });
+      if (!mintListEvent) throw new Error('Recipient has no mint list');
+      const pubkey = mintListEvent.tags.find((t) => t[0] === 'pubkey')?.[1];
+      if (!pubkey) throw new Error('Recipient has no mint list');
+      sendOptions.pubkey = pubkey;
+    }
+    await this.cashuWallet.getKeySets();
+    const sendResult = await this.cashuWallet.send(amount, this.proofs, sendOptions);
+    this.proofs = sendResult.keep;
+    const proofsEvent = new NDKEvent(ndk);
+    proofsEvent.kind = NDKKind.CashuToken;
+    proofsEvent.content = await ndk.signer?.encrypt(
+      await ndk.signer!.user()!,
+      JSON.stringify({
+        mint: this.cashuWallet.mint.mintUrl,
+        proofs: this.proofs,
+        del: Array.from(this.eventIdToProofIdMap.keys()),
+      }),
+      'nip44',
+    )!;
+    await proofsEvent.sign();
+    await proofsEvent.publish();
+    this.recalculateBalance();
+    return sendResult.send;
+  }
+
+  async nutZap(amount: number, recipient: NDKUser, eventId?: NDKEventId, message?: string) {
+    const token = await this.createToken(amount, recipient);
+    const zapEvent = new NDKEvent(ndk);
+    zapEvent.kind = NDKKind.Nutzap;
+    zapEvent.content = message ?? '';
+    zapEvent.tags = [
+      ['proof', JSON.stringify(token)],
+      ['u', Wallet.mintUrl],
+      ['p', recipient.pubkey],
+    ];
+    if (eventId) zapEvent.tags.push(['e', eventId]);
+    await zapEvent.sign();
+    await zapEvent.publish();
+
+    const spendingHistoryEvent = new NDKEvent(ndk);
+    spendingHistoryEvent.kind = NDKKind.CashuWalletTx;
+    const historyContent = [
+      ['direction', 'out'],
+      ['amount', amount.toString()],
+    ];
+    if (eventId) historyContent.push(['e', eventId]);
+    spendingHistoryEvent.content = await ndk.signer?.encrypt(
+      await ndk.signer!.user()!,
+      JSON.stringify(historyContent),
+      'nip44',
+    )!;
+    spendingHistoryEvent.tags = [
+      ['e', zapEvent.id],
+      ['p', recipient.pubkey],
+    ];
+    await spendingHistoryEvent.sign();
+    await spendingHistoryEvent.publish();
+  }
+}
+
+export const wallet = new Wallet();