diff --git a/README.md b/README.md
new file mode 100644
index 0000000..677b31e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# nostr-broadcast
+
+Fetch, backup and broadcast your Nostr events.
+
+Live at https://nostr-broadcast.pages.dev
diff --git a/img/nostr-broadcast_og.png b/img/nostr-broadcast_og.png
new file mode 100644
index 0000000..39b5455
Binary files /dev/null and b/img/nostr-broadcast_og.png differ
diff --git a/img/nostr-broadcast_twitter.png b/img/nostr-broadcast_twitter.png
new file mode 100644
index 0000000..a23826b
Binary files /dev/null and b/img/nostr-broadcast_twitter.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..3272f5f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+ NBS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Share all of your events, such as notes &
+ lists of followers/following, with all the major
+ relays and generate a backup filethat can be used
+ to restore your data to any relay of your choice.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/nostr-broadcast.js b/js/nostr-broadcast.js
new file mode 100644
index 0000000..3510761
--- /dev/null
+++ b/js/nostr-broadcast.js
@@ -0,0 +1,74 @@
+// button click handler
+const fetchAndBroadcast = async () => {
+ // reset UI
+ $('#fetching-status').html('')
+ $('#fetching-progress').css('visibility', 'hidden')
+ $('#fetching-progress').val(0)
+ $('#file-download').html('')
+ $('#events-found').text('')
+ $('#broadcasting-status').html('')
+ $('#broadcasting-progress').css('visibility', 'hidden')
+ $('#broadcasting-progress').val(0)
+ // messages to show to user
+ const checkMark = '✓'
+ const txt = {
+ broadcasting: 'Broadcasting to relays... ',
+ fetching: 'Fetching from relays... ',
+ download: `Downloading Backup file... ${checkMark}`,
+ }
+ // parse pubkey ('npub' or hexa)
+ const pubkey = parsePubkey($('#pubkey').val())
+ if (!pubkey) return
+ // disable button (will be re-enable at the end of the process)
+ $('#fetch-and-broadcast').prop('disabled', true)
+ // inform user that app is fetching from relays
+ $('#fetching-status').text(txt.fetching)
+ // show and update fetching progress bar
+ $('#fetching-progress').css('visibility', 'visible')
+ const fetchInterval = setInterval(() => {
+ // update fetching progress bar
+ const currValue = parseInt($('#fetching-progress').val())
+ $('#fetching-progress').val(currValue + 1)
+ }, 1000)
+ // get all events from relays
+ const filter = { authors: [pubkey] }
+ const data = await getEvents(filter)
+ // inform user fetching is done
+ $('#fetching-status').html(txt.fetching + checkMark)
+ clearInterval(fetchInterval)
+ $('#fetching-progress').val(20)
+ // inform user that backup file (js format) is being downloaded
+ $('#file-download').html(txt.download)
+ downloadFile(data, 'nostr-broadcast.js')
+ // inform user that app is broadcasting events to relays
+ $('#broadcasting-status').html(txt.broadcasting)
+ // show and update broadcasting progress bar
+ $('#broadcasting-progress').css('visibility', 'visible')
+ const broadcastInterval = setInterval(() => {
+ // update fetching progress bar
+ const currValue = parseInt($('#broadcasting-progress').val())
+ $('#broadcasting-progress').val(currValue + 1)
+ }, 1000)
+ await broadcastEvents(data)
+ // inform user that broadcasting is done
+ $('#broadcasting-status').html(txt.broadcasting + checkMark)
+ clearInterval(broadcastInterval)
+ $('#broadcasting-progress').val(20)
+ // re-enable broadcast button
+ $('#fetch-and-broadcast').prop('disabled', false)
+}
+
+const getFromExtension = async () => {
+ const pubkey = await window.nostr.getPublicKey()
+ if (pubkey) $('#pubkey').val(pubkey).change()
+}
+
+const pubkeyOnChange = () => {
+ $('#fetch-and-broadcast').css('display', '')
+ $('#get-from-extension').css('display', 'none')
+}
+
+if (window.nostr) {
+ $('#fetch-and-broadcast').css('display', 'none')
+ $('#get-from-extension').css('display', '')
+}
diff --git a/js/relays.js b/js/relays.js
new file mode 100644
index 0000000..195e008
--- /dev/null
+++ b/js/relays.js
@@ -0,0 +1,46 @@
+// list of paid relays from:
+// https://thebitcoinmanual.com/articles/paid-nostr-relay/
+
+const relays = [
+ 'wss://atlas.nostr.land', // paid relay 15000 npub12262qa4uhw7u8gdwlgmntqtv7aye8vdcmvszkqwgs0zchel6mz7s6cgrkj
+ 'wss://bitcoiner.social', // paid relay 1000 npub1dxs2pygtfxsah77yuncsmu3ttqr274qr5g5zva3c7t5s3jtgy2xszsn4st
+ 'wss://brb.io',
+ 'wss://eden.nostr.land', // paid relay 5000 npub16k7j4mwsqm8hakjl8x5ycrqmhx89lxkfwz2xxxcw75eav7sd8ztqy2rwdn
+ 'wss://expensive-relay.fiatjaf.com',
+ 'wss://freedom-relay.herokuapp.com',
+ 'wss://nos.lol',
+ 'wss://nostr-2.zebedee.cloud',
+ 'wss://nostr-pub.wellorder.net',
+ 'wss://nostr-relay.alekberg.net',
+ 'wss://nostr-relay.freeberty.net',
+ 'wss://nostr-relay.wlvs.space',
+ 'wss://nostr.bitcoiner.social',
+ 'wss://nostr.blocs.fr',
+ 'wss://nostr.coollamer.com',
+ 'wss://nostr.decentony.com', // paid relay 7000 npub1pp9csm9564ewzer3f63284mrd9u2zssmreq42x4rtt390zmkrj2st4fzpm
+ 'wss://nostr.fmt.wiz.biz',
+ 'wss://nostr.gives.africa', // paid relay 10000 npub1g8dcep2exsadx9smhdrgwds06pgfc9yyyww6ftdcgnyukcuzk2csqs5jed
+ 'wss://nostr.inosta.cc', // paid relay 5000 npub1r34nhc6nqswancymk452f2frgn3ypvu77h4njr67n6ppyuls4ehs44gv0h
+ 'wss://nostr.milou.lol', // paid relay 1000 npub1rvg76s0gz535txd9ypg2dfqv0x7a80ar6e096j3v343xdxyrt4ksmkxrck
+ 'wss://nostr.onsats.org',
+ 'wss://nostr.orangepill.dev',
+ 'wss://nostr.plebchain.org', // paid relay 6969 npub1u2tehhr3ye4lv4dc8aen2gkxf6zljdpf356sgfjqfun0wxehvquqgvhuec
+ 'wss://nostr.rocks',
+ 'wss://nostr.sandwich.farm',
+ 'wss://nostr.wine', // paid relay 8888 npub18kzz4lkdtc5n729kvfunxuz287uvu9f64ywhjz43ra482t2y5sks0mx5sz
+ 'wss://nostr.zebedee.cloud',
+ 'wss://private.red.gb.net', // paid relay 8888 npub1nctdevxxuvth3sx6r0gutv4tmvhwy9syvpkr3gfd5atz67fl97kqyjkuxk
+ 'wss://puravida.nostr.land', // paid relay 10000 npub16k7j4mwsqm8hakjl8x5ycrqmhx89lxkfwz2xxxcw75eav7sd8ztqy2rwdn
+ 'wss://relay.current.fyi',
+ 'wss://relay.damus.io',
+ 'wss://relay.nostr.bg',
+ 'wss://relay.nostr.com.au', // paid relay 6969 npub1qqqqqrre3jxkuyj3s4m59usdyvm0umgm0lpy6cqjtwpt649sdews5q3hw7
+ 'wss://relay.nostr.info',
+ 'wss://relay.nostrati.com', // paid relay 2000 npub1qqqqqqqut3z3jeuxu70c85slaqq4f87unr3vymukmnhsdzjahntsfmctgs
+ 'wss://relay.nostrich.land', // paid relay 2100 npub1vj0wlergmkcs0sz7hfks2ywj555c2s87f40squ4sqcmqpr7897fqn6mfew
+ 'wss://relay.nostriches.org', // paid relay 421 npub1vnmhd287pvxxk5w9mcycf23av24nscwk0da7rrfaa5wq4l8hsehs90ftlv
+ 'wss://relay.orangepill.dev', // paid relay 4500 npub16jzr7npgp2a684pasnkhjf9j2e7hc9n0teefskulqmf42cqmt4uqwszk52
+ 'wss://relay.snort.social',
+ 'wss://relayer.fiatjaf.com',
+ 'wss://rsslay.fiatjaf.com',
+]
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..6dd78da
--- /dev/null
+++ b/style.css
@@ -0,0 +1,293 @@
+@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;700&display=swap');
+
+:root{
+--color: #333;
+}
+
+body {
+ font-family: 'Red Hat Text', sans-serif;
+}
+
+.container {
+ text-align: center;
+ color: var(--color);
+ width: 100%;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-image: url(https://images.unsplash.com/photo-1519120944692-1a8d8cfc107f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=872&q=80);
+ background-size: cover;
+ background-position: center;
+
+/* this is where the magic happens: */
+ background-attachment: fixed;
+}
+
+h1 {font-family: "Open Sans", sans-serif;
+ line-height: 1.5;
+ font-weight: 700;
+ font-size: 2rem;
+
+}
+.header-space {
+ display: flex;
+ justify-content: center;
+ margin-top: 9px;
+ font-family: "Courier", monospace;
+}
+
+.box {
+ background-color: #f2f2f2;
+ padding: 20px;
+ border-radius: 10px;
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+
+progress {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 10px;
+ background-color: #eee;
+ border-radius: 10px;
+ overflow: hidden;
+}
+progress::-webkit-progress-bar {
+ background-color: #eee;
+ border-radius: 10px;
+}
+progress::-webkit-progress-value {
+ background-color: #007bff;
+ border-radius: 10px;
+ animation: loading 2s ease-out infinite;
+}
+@keyframes loading {
+ 0% {
+ width: 0%;
+ }
+ 100% {
+ width: 100%;
+ }
+}
+
+
+progress {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 10px;
+ background-color: #eee;
+ border-radius: 10px;
+ overflow: hidden;
+}
+progress::-webkit-progress-bar {
+ background-color: #eee;
+ border-radius: 10px;
+}
+progress::-webkit-progress-value {
+ background-color: #007bff;
+ border-radius: 10px;
+}
+progress[value="25"]::-webkit-progress-value {
+ background-color: #ffc107;
+}
+progress[value="50"]::-webkit-progress-value {
+ background-color: #28a745;
+}
+progress[value="75"]::-webkit-progress-value {
+ background-color: #dc3545;
+}
+
+
+p {
+ max-width: 52ch;
+ line-height: 1.5;
+ padding: .3em;
+ margin-top: 5px;
+ margin: 0;
+}
+
+
+
+
+a {
+ display: inline-block;
+ padding: .5em 1.5em;
+ background-color: var(--color);
+ color: white;
+ text-decoration: none;
+ text-transform: uppercase;
+ border-radius: 0.3em;
+ font-weight: 700;
+ letter-spacing: .5px;
+ font-size: .875rem;
+}
+
+.blank,
+.other {
+ width: 100%;
+ min-height: 60vh;
+ background-color: var(--color);
+}
+
+.second {
+ background-image:url(https://images.unsplash.com/photo-1514496959998-c01c40915c5f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1740&q=80);
+ background-attachment: fixed;
+ height: 1200px;
+}
+
+.second {
+ background-image:url(https://images.unsplash.com/photo-1514496959998-c01c40915c5f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1740&q=80);
+ background-attachment: fixed;
+ height: 1200px;
+}
+
+.space-between {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 10px;
+}
+
+h2 {
+ font-family: "Open Sans", sans-serif;
+ font-size: 1em;
+ line-height: 1.5;
+ font-weight: 500; /* Set font-weight to lighter */
+ margin-bottom: 5px;
+ width: 50%; /* set the width of h2 element to 50% of its parent's width */
+ margin: 10 auto; /* set the top and bottom margin to 0, and left and right margin to auto */
+ word-wrap: break-word;
+ padding: 25px;
+}
+
+@media screen and (max-width: 1080px) { /* apply this style only when the screen width is less than or equal to 600px */
+ h2 {
+ width: 70%; /* set the width of h2 element to 100% of its parent's width */
+ margin: 0; /* set all margins to 0 */
+ }
+}
+
+@media screen and (max-width: 800px) { /* apply this style only when the screen width is less than or equal to 600px */
+ h2 {
+ width: 90%; /* set the width of h2 element to 100% of its parent's width */
+ margin: 0; /* set all margins to 0 */
+ }
+}
+
+@media screen and (max-width: 600px) { /* apply this style only when the screen width is less than or equal to 600px */
+ h2 {
+ width: 90%; /* set the width of h2 element to 100% of its parent's width */
+ margin: 0; /* set all margins to 0 */
+ }
+}
+
+#pubkey {
+ width: 700px !important;
+}
+
+@media screen and (max-width: 1080px) {
+ #pubkey {
+ width: 550px !important;
+ }
+}
+@media screen and (max-width: 800px) {
+ #pubkey {
+ width: 450px !important;
+ }
+}
+@media screen and (max-width: 600px) {
+ #pubkey {
+ width: 300px !important;
+ }
+}
+
+
+
+
+
+main div p img {
+ position: relative;
+ top: 6px;
+ height: 1rem;
+ margin-left: 0.5rem;
+}
+
+details {
+ background-color: rgba(242, 241, 238, 0.3);
+ border: 1px solid #ccc;
+ padding: 0.5rem;
+ margin: 1rem 0;
+}
+
+p,
+li {
+ line-height: 1.33;
+ margin-bottom: 1rem;
+}
+
+ul {
+ margin-left: -1rem;
+}
+
+.box {
+ border: 1px solid #ccc;
+ padding: 0 0.5rem;
+ margin: 1rem 0;
+}
+
+.box-content {
+ display: flex;
+ justify-content: center;
+ margin-top: 10px;
+ font-family: "Courier", monospace;
+}
+
+button {
+ appearance: none;
+ background-color: #087bee;
+ border: 1px solid rgba(27, 31, 35, 0.15);
+ border-radius: 6px;
+ box-shadow: rgba(27, 31, 35, 0.04) 0 1px 0, rgba(255, 255, 255, 0.25) 0 1px 0 inset;
+ box-sizing: border-box;
+ color: #f9fafb;
+ cursor: pointer;
+ display: inline-block;
+ font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ list-style: none;
+ padding: 6px 16px;
+ position: relative;
+ transition: background-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: manipulation;
+ vertical-align: middle;
+ white-space: nowrap;
+ word-wrap: break-word;
+}
+
+
+
+.gray {
+ background-color: rgba(183, 177, 168, 0.3);
+}
+
+.red {
+ background-color: rgba(229, 60, 8, 0.3);
+}
+
+.box button {
+ background-color: rgba(0, 0, 0, 0.813);
+ color: rgb(255, 255, 255);
+}
+
+footer p {
+ font-size: 0.8rem;
+ margin: 0;
+ text-align: center;
+}
+
+