If you're familiar with JavaScript it is relatively straightforward to install new primitives in the Lil interpreter by modifying an HTML build of Decker. This thread includes an example of exposing browser localStorage to Lil.
Async communication is somewhat awkward; if you were to expose an API based on callbacks, like the underlying JS APIs, you could easily end up with Lil functions attached via closure to parts of a deck that have been modified or destroyed since the original call was performed. A polling-based system exposed on an Interface might be less error-prone.
Here's a sketch (untested) of a polling-based wrapper for XMLHttpRequests:
const pending_messages=[] interface_messagebox=lmi((self,i,x)=>{ if(ikey(i,'send'))return lmnat(([url,verb,text,id])=>{ const x=new XMLHttpRequest() x.onreadystatechange=_=>{ if(x.readyState!=XMLHttpRequest.DONE||x.status!=200)return pending_messages.push(lmd(['text','id'].map(lms),[lms(x.responseText),id?id:NONE])) } x.open(verb?ls(verb):'POST',url?ls(url):'') x.send(text?ls(text):'') return NONE }) if(ikey(i,'poll'))return lmnat(_=>pending_messages.length?pending_messages.shift():NONE) return x?x:NONE })
WebSockets add some additional complexity because the connections need to be persistent, but a similar approach might work.