No description
  • Rust 96.2%
  • Shell 2.1%
  • Solidity 1.7%
Find a file
2026-10-26 22:46:17 +08:00
contracts feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
decentralized-micro-services-gateway feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
declaw feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
demsg-api feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
demsg-client feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
demsgd feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
dequic feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
examples feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
scripts feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
test_alloy_address feat: fully working dequic service 2026-06-02 17:33:57 +08:00
.gitignore feat: fully working dequic service 2026-06-02 17:33:57 +08:00
.gitmodules chore: drop deployments submodule (single copy lives in LitheumCore) 2026-08-21 17:44:58 +08:00
ARCHITECTURE.md feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
Cargo.toml feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
README.md feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00
REVIEW.md feat: rearchitect to a service based architecture... 2026-10-26 22:46:17 +08:00

DeMSG

DeMSG (Decentralized Micro-Services Gateway) lets anyone sell access to an online service for on-chain payments on Litheum, and lets platforms ("consumer apps") host many sellers' services without touching the chain or wallet cryptography.

Sellers register services in the ServiceMarketPlace contract; buyers pay for units of a service there. One demsgd serves the whole contract: it indexes every service, purchase, usage record, and withdrawal; lets buyers sign in with their wallet and create API keys for the services they buy; and lets consumer apps verify those keys and follow everything as an ordered event stream.

See ARCHITECTURE.md for diagrams of the components, the seller and buyer flows, and the activity lifecycle.

Contents

Concepts

Term Meaning
Seller Any wallet that registers services in the contract. Earnings from all its services accrue in one balance it withdraws.
Service A seller's offering at a network endpoint. Identified by serviceId = keccak256(abi.encode(ip, port)), so no two services share an ip:port. Has a name, a price per unit, a unit name, an optional 33-byte pubkey, an app tag, and a biller. Its ip and port never change.
App tag The consumer app a service is sold through (e.g. declaw), chosen by the seller; empty for standalone sellers.
Biller An address the seller allows to record buyers' usage of the service on-chain with deductBatch, usually the consumer app's wallet; the zero address if none. Usage records are a log: no funds move.
Buyer The address that called buy(serviceId, pubkey).
Purchase One SessionPurchased(serviceId, seller, buyer, pubkey, units) event; units = msg.value / pricePerUnit.
Deduction One UsageDeducted(serviceId, seller, buyer, units) event: the biller recorded units of the buyer's purchased units as used.
Holding What a buyer has of one service: confirmed units bought, units deducted, remaining = bought - deducted, and the pubkeys passed to buy.
Consumer app A platform for one type of service (e.g. an LLM platform). It runs at one known address. Sellers register services tagged with the app and report them to it; buyers send every request to the app, which looks up the API key's service with DeMSG, forwards the request to the service's ip:port, and keeps buyers' balances (what a unit is worth and how much is used is the app's decision).
Standalone seller A seller that runs its service itself (e.g. dequic, a chat relay): registered with an empty app tag, serving buyers directly at its ip:port and verifying keys with DeMSG.
API key A random secret (demsg_…) a buyer creates for one service after signing in with their wallet. Clients send it to the service in cleartext over TLS, like any API key. DeMSG stores only its SHA-256 hash. A key may be bound to one of the pubkeys the buyer bought the service with, for services that identify users by pubkey.
Activity One contract log (service registered/updated, purchase, deduction, withdrawal) with a status: pending, confirmed, dropped, or revoked.
Event One entry in DeMSG's append-only, numbered log: an activity changing status, or an API key created or revoked.
Confirmation depth Blocks required on top of an activity's block before it is confirmed (default 1).

demsgd serves two listeners:

  • Public (public_bind): service listing, holdings, wallet sign-in, and API key management. Reached by dapps through your TLS reverse proxy.
  • Internal (internal_bind): key verification, events, health. No authentication: only consumer app backends may reach it (bind it to 127.0.0.1 or firewall it). All consumer apps are trusted and see all activity.

Every path lives under /demsg/, so DeMSG can share a domain and TLS certificate with other services.

Building a consumer app

A consumer app is a platform for one type of service, at one known address written into its dapp. Its parts:

Dapp (written for that service type):

  1. Sellers: sign in to the app's backend with the wallet; a form that calls register(pubkey, pricePerUnit, unitName, name, ip, port, app, biller) from the seller's wallet, where ip:port is the seller's own endpoint, app is the app's tag, and biller is the app's wallet if the seller lets it record usage on-chain (else the zero address); then report the service id to the backend. update(...) changes everything but ip/port; withdraw(amount) pays out earnings. The seller's services are listed with GET /demsg/services?seller=0x…&app=<tag>.
  2. Buyers: list the services the app offers (from the backend), call buy(serviceId, pubkey) with payment, then sign in to DeMSG and create an API key for the service (see Sign-in and API keys). Send requests to the app with the key.

Backend (at the app's address):

  1. Sign sellers in and accept their services: the same wallet sign-in DeMSG uses (demsg_api::signin in Rust). When a seller reports a service id, check with GET /demsg/services/{id} that its seller is the signed-in address and its app is the app's tag.
  2. Follow the event stream (/demsg/events/stream?app=<tag>), storing the seq of the last event applied in the same database transaction as the event's effect. On restart, resume from it: every event is delivered once and in order, so each is applied exactly once.
  3. Keep balances per (service, buyer): on purchase.confirmed, credit the buyer by the app's own rule per unit; on purchase.revoked, remove exactly that credit (look it up by the activity id). Track usage.
  4. Route requests: call POST /demsg/verify with the key (no service_id). If valid, check the key's service is one the app accepted and still carries its tag, and that key.buyer has balance; forward the request to service.ip:service.port and bill the usage. Cache valid answers briefly and drop them on key.revoked.
  5. Record usage on-chain (for services whose biller is the app's wallet): periodically send one deductBatch with the whole units used since the last batch. Count them as recorded only on deduction.confirmed; send them again if that does not arrive (the transaction was dropped, or deduction.revoked).

A standalone seller does steps 2–4 for its one service, without routing: follow /demsg/events/stream?service=<id>, verify keys with service_id set to its own, and serve the buyer itself.

JavaScript example: a standalone service (Node 18+)

// npm install ws
import WebSocket from 'ws';
import { createHash } from 'node:crypto';
import { keccak256, AbiCoder } from 'ethers';

const DEMSG = 'http://127.0.0.1:8072'; // internal API
const SERVICE_ID = keccak256(AbiCoder.defaultAbiCoder().encode(['string', 'uint16'], ['203.0.113.7', 8091]));
const CREDITS_PER_UNIT = 1000n;

// The app's own state; persist it (with `lastSeq`) in one transaction per event.
const credits = new Map();   // buyer -> bigint
const applied = new Map();   // purchase activity id -> granted credits
let lastSeq = 0;

function apply(event) {
  const a = event.activity;
  if (event.type === 'purchase.confirmed' && !applied.has(a.id)) {
    const granted = BigInt(a.units) * CREDITS_PER_UNIT;
    applied.set(a.id, granted);
    credits.set(a.buyer, (credits.get(a.buyer) ?? 0n) + granted);
  } else if (event.type === 'purchase.revoked' && applied.has(a.id)) {
    const granted = applied.get(a.id);
    applied.delete(a.id);
    const left = (credits.get(a.buyer) ?? 0n) - granted;
    credits.set(a.buyer, left < 0n ? 0n : left);
  } else if (event.type === 'key.revoked') {
    cache.delete(event.key.key_hash);
  }
  lastSeq = event.seq;
}

function follow() {
  const ws = new WebSocket(`${DEMSG.replace('http', 'ws')}/demsg/events/stream?since_seq=${lastSeq}&service=${SERVICE_ID}`);
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    if (msg.type !== 'replay.end') apply(msg);
  });
  ws.on('close', () => setTimeout(follow, 1000));
}
follow();

const cache = new Map(); // key hash (as DeMSG's key_hash) -> { buyer, until }

async function verify(key) {
  const hash = '0x' + createHash('sha256').update(key.trim()).digest('hex');
  const hit = cache.get(hash);
  if (hit && hit.until > Date.now()) return hit.buyer;
  const resp = await fetch(`${DEMSG}/demsg/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key, service_id: SERVICE_ID }),
  });
  const v = await resp.json();
  if (!v.valid) return null;
  cache.set(hash, { buyer: v.key.buyer, until: Date.now() + 30_000 });
  return v.key.buyer;
}

// In a request handler:
//   const buyer = await verify(req.headers.authorization?.replace(/^Bearer /, ''));
//   if (!buyer) return res.status(401).end();
//   if ((credits.get(buyer) ?? 0n) === 0n) return res.status(402).end();
//   ...serve, then deduct usage from credits.get(buyer).

Rust

demsg-client provides DemsgClient (verify with cache, services, holdings, events, a reconnecting subscribe, spawn_cache_invalidator), signin (EIP-4361 challenge messages and personal_sign recovery), and ledger::Ledger, which implements steps 2 and 3 in a local redb file: balances per (service, buyer) for one service or one app tag, time or quantity metering, idempotent usage, and tracking of usage sent to deductBatch until it is confirmed. declaw (a platform) and dequic (a standalone seller) are complete examples.

The contract

contracts/ServiceMarketPlace.sol; deploy with scripts/deploy-servicemarketplace.sh (writes the address to the deployments directory for LITHEUM_NETWORK). Register a dequic relay with scripts/register-dequic.sh --name <name> --price <IONS per minute> (ip and port from the dequic config, or --ip/--port); it signs with lcli 1, whose wallet becomes the seller.

Function Who Effect
register(bytes pubkey, uint256 pricePerUnit, string unitName, string name, string ip, uint16 port, string app, address biller) returns (bytes32) seller New service; reverts if ip:port is taken, the price is 0, ip is empty, or pubkey is neither empty nor 33 bytes
update(bytes32 serviceId, bytes pubkey, uint256 pricePerUnit, string unitName, string name, string app, address biller) the service's seller Changes everything but ip/port
buy(bytes32 serviceId, bytes pubkey) payable buyer value must be a positive multiple of the price; pubkey must be 33 bytes; credits the seller's earnings
deductBatch(bytes32[] serviceIds, address[] buyers, uint256[] units) each service's biller Records usage: adds each entry, capped at the buyer's remaining units, to deducted. Reverts entirely if any service is unregistered or has another biller. No funds move
withdraw(uint256 amount) seller Pays out earnings
services(bytes32), serviceIdOf(string, uint16), bought(bytes32, address), deducted(bytes32, address), earnings(address), serviceCount(), getServicesPage(start, limit) anyone Reads

Events: ServiceRegistered/ServiceUpdated(bytes32 indexed serviceId, address indexed seller, Service service) (the whole record), SessionPurchased(bytes32 indexed serviceId, address indexed seller, address indexed buyer, bytes pubkey, uint256 units), UsageDeducted(bytes32 indexed serviceId, address indexed seller, address indexed buyer, uint256 units) (not emitted for 0 units), ServiceWithdrew(address indexed seller, uint256 amount).

The id hashes the exact ip string: 203.0.113.7 and a DNS name for the same host are different services.

Running demsgd

cargo build --release -p demsgd
./target/release/demsgd --config ~/.litheum/demsg/config.toml

The first run creates the config interactively (--non-interactive fails instead). demsgd holds no wallet. Flags: --config, --non-interactive, --bootstrap.

Config

public_bind = "0.0.0.0:8070"
internal_bind = "127.0.0.1:8072"
public_url = "https://example.com"      # as buyers reach it; shown in the sign-in message
db_path = "$HOME/.litheum/demsg/demsg.redb"
peers_storage = "$HOME/.litheum/demsg/peers.bin"
demsg_contract_address = "0x…"          # ServiceMarketPlace
confirmation_depth = 1                  # optional, default 1
start_block_id = 0                      # optional: first block scanned on a new database

[[bootstrap_peers]]
ip = "127.0.0.1"
port = 8001
address = "0101010101010101010101010101010101010101"
roles_bitmask = 7

Set start_block_id to the block the contract was deployed in to skip older blocks.

The bundled consumer apps and their default ports:

Process Ports
demsgd public 8070, internal 127.0.0.1:8072
dequic HTTP 8081 (/ws, /service-info, /balance/{address}; register this port with scripts/register-dequic.sh), QUIC 4443, WebTransport 4444
declaw 8091: the platform's one address (sellers register their own endpoints' ports)

TLS

demsgd speaks plain HTTP. Put a TLS reverse proxy in front of the public listener (never the internal one), e.g. with Caddy:

example.com {
    handle /demsg/* {
        reverse_proxy 127.0.0.1:8070
    }
}

Set public_url to the URL buyers use, so wallets show the right site in the sign-in message.

API reference

All bodies are JSON. Addresses, pubkeys, ids, and hashes are 0x-prefixed lowercase hex; addresses are accepted in any case. Times are unix seconds; uint256 amounts and prices are decimal strings. Errors are {"error": "<message>"} with a 4xx/5xx status. Paths carry no version: changes only add fields and endpoints, so ignore unknown fields.

Public listener

Method Path Auth Purpose
GET /demsg/info – Contract, chain id, confirmation depth, public URL
GET /demsg/services?seller=&app=&after=&limit= – Registered services
GET /demsg/services/{service_id} – One service (404 if not registered)
GET /demsg/holdings/{service_id}/{buyer} – A buyer's holding for a service
POST /demsg/auth/challenge – Get a one-time sign-in message
POST /demsg/auth/login – Exchange the signed message for a management session
POST /demsg/keys session Create an API key for a service
GET /demsg/keys session List the buyer's keys
DELETE /demsg/keys/{key_id} session Revoke a key

Internal listener

The read-only public endpoints, plus:

Method Path Purpose
POST /demsg/verify Look up an API key presented to a service
GET /demsg/events?since_seq=&limit=&service=&seller=&buyer=&app= Page through events
GET (WebSocket) /demsg/events/stream?since_seq=&service=&seller=&buyer=&app= Replay, then live events
GET /demsg/health Sync status

Services and holdings

GET /demsg/services/{service_id} returns the service's current record (its latest confirmed registration or update):

{
  "service_id": "0x6c1f…",
  "seller": "0x5a0b…",
  "pubkey": "0x",
  "price_per_unit": "1000000000000000",
  "unit_name": "kilotokens",
  "name": "Alice's LLM",
  "ip": "203.0.113.7",
  "port": 11434,
  "app": "declaw",
  "biller": "0x9e4d…",
  "registered_block_id": 18300,
  "updated_block_id": 18452
}

GET /demsg/services returns {"services": [...], "next_after"}, ordered by service_id, up to limit (default 100, max 1000), optionally only one seller's and/or only those with one app tag. Pass next_after as after for the next page; it is absent on the last page.

GET /demsg/holdings/{service_id}/{buyer}:

{ "service_id": "0x6c1f…", "buyer": "0x1c2b…", "bought": 12, "deducted": 5, "remaining": 7, "pubkeys": ["0x02ab…"] }

bought counts confirmed purchases and deducted confirmed deductions.

Sign-in and API keys

  1. POST /demsg/auth/challenge with {"address": "0x…"} returns {"nonce", "message", "expires_at"}. The message is in the Sign-In with Ethereum (EIP-4361) format and expires after 5 minutes:

    example.com wants you to sign in with your Ethereum account:
    0x1c2BD067b0f155d5034e9fb623A97e1F3670a89F
    
    Manage your DeMSG API keys.
    
    URI: https://example.com
    Version: 1
    Chain ID: 1174
    Nonce: 3f9c…
    Issued At: 2026-10-22T23:10:00Z
    Expiration Time: 2026-10-22T23:15:00Z
    
  2. The wallet signs the message with personal_sign (EIP-191). Both the Litheum wallet and MetaMask support this.

  3. POST /demsg/auth/login with {"nonce", "signature"} returns {"session", "buyer", "expires_at"}. Each challenge can be used once, successful or not. Sessions last 15 minutes.

  4. With Authorization: Bearer <session>:

    • POST /demsg/keys with {"service_id": "0x…", "name": "opencode", "bound_pubkey": "0x02…" (optional), "replace": true (optional)} returns 201 with the key — the only time it is shown — and its info:

      {
        "key": "demsg_9b1e…",
        "key_id": "key_4f2a…",
        "service_id": "0x6c1f…",
        "buyer": "0x1c2b…",
        "name": "opencode",
        "bound_pubkey": null,
        "key_hash": "0x77d0…",
        "created_at": 1793142600,
        "revoked_at": null
      }
      

      The service must be registered. bound_pubkey must be a pubkey from one of the buyer's confirmed purchases of that service. replace revokes the buyer's other active keys for that service with the same name. A buyer can have at most 100 active keys; names are 1–64 characters.

    • GET /demsg/keys returns {"keys": [<key info>, …]} (never the keys).

    • DELETE /demsg/keys/{key_id} revokes the key and returns its info.

Verify

POST /demsg/verify with {"key": "demsg_…", "service_id": "0x…"} (service_id optional):

{
  "valid": true,
  "reason": null,
  "key": { "key_id": "key_4f2a…", "service_id": "0x6c1f…", "buyer": "0x1c2b…", "name": "opencode", "bound_pubkey": null, "key_hash": "0x77d0…", "created_at": 1793142600, "revoked_at": null },
  "service": { "service_id": "0x6c1f…", "seller": "0x5a0b…", "…": "…" },
}

A key is valid when it exists, is not revoked, and, if service_id is given, is for that service. Otherwise reason is unknown_key, revoked, or wrong_service. A standalone service passes its own id; a platform omits it and routes by key.service_id and service. Validity does not depend on units: whether the buyer may use the service is the app's decision, from its own balances (or from GET /demsg/holdings). key is present unless the key is unknown; service only if valid.

Health

GET /demsg/health returns {"chain_tip", "cursor", "pending_activities", "last_seq"}: the latest block the chain sync knows, the latest block applied (a block is applied only once confirmation_depth blocks sit on top of it), and the latest event number. pending_activities is always 0. chain_tip - cursor includes blocks still waiting for confirmation.

Events

Every event has a gap-free seq (from 1), a time, a type, and app: the app tag of its service when the event was recorded (absent for withdrawals and untagged services).

Type Also carries
purchase.confirmed, purchase.revoked activity
deduction.confirmed, deduction.revoked activity
service.confirmed, service.revoked activity
withdrawal.confirmed, withdrawal.revoked activity
key.created, key.revoked key

activity:

{
  "id": "0x…",
  "status": "confirmed",
  "block_id": 18452,
  "block_hash": "0x…",
  "block_time": 1793142600,
  "tx_hash": "0x…",
  "tx_index": 3,
  "log_index": 0,
  "kind": "purchase",
  "service_id": "0x6c1f…",
  "seller": "0x5a0b…",
  "buyer": "0x1c2b…",
  "pubkey": "0x02ab…",
  "units": 3
}

Service activities (kind: "service") carry action (registered or updated), service_id, seller, pubkey, price_per_unit, unit_name, name, ip, port, app, biller. Deductions (kind: "deduction") carry service_id, seller, buyer, units. Withdrawals (kind: "withdrawal") carry seller and amount.

GET /demsg/events?since_seq=N&limit=L&service=&seller=&buyer=&app= returns {"events": [...], "last_seq"}: events after N (default 0) matching every given filter, oldest first, up to L (default 100, max 1000). service matches purchase, deduction, service, and key events of that service; seller matches purchase, deduction, service, and withdrawal events of that seller; buyer matches purchase, deduction, and key events of that buyer; app matches events whose app is that tag.

The WebSocket /demsg/events/stream (same filters) sends each event as a JSON text message: first every matching event after since_seq, then {"type": "replay.end", "last_seq": M}, then new events as they happen. Every event is sent once and in order. To resume, reconnect with since_seq set to the last seq you processed.

Confirmations and rollbacks

demsgd scans blocks of the longest chain in order and compares the hashes it has scanned against the chain on every pass, so restarts, missed blocks, and reorganizations all converge:

  • An activity is confirmed once its block has confirmation_depth blocks on top (0 confirms immediately). Activities in one block are applied in transaction and log order.
  • If a block is rolled back before its activities are confirmed, they become dropped.
  • If a block is rolled back after confirmation, its activities become revoked and their effect is undone: a purchase's units leave the holding and a deduction's units return to it; a service registration or update is removed, restoring the previous record (or removing the service). Apps must undo their own effects on *.revoked.
  • If a rolled-back block returns to the longest chain, its activities are pending again under the same id and confirmed once more — never twice.
  • Blocks missed while demsgd was down are scanned on startup. If a transaction receipt is not available yet, the block is retried rather than skipped.

Persistence and backups

All state is in one redb file (db_path): activities, services, holdings, keys, sessions, and events. Writes are transactional, so a crash never leaves a half-applied block. Back it up by copying the file while demsgd is stopped. The file carries a schema version; demsgd refuses to open a file from a different version (start with a new db_path; it rebuilds from the chain, but API keys are not on-chain and are lost).

Repository layout and tests

Crate Purpose
decentralized-micro-services-gateway Library behind demsgd: chain scanning, store, sign-in, HTTP API
demsgd The demsgd binary
demsg-api JSON types of the API and service_id(ip, port)
demsg-client Rust client for the internal API and the balance ledger
dequic QUIC / WebTransport / WebSocket chat relay, a standalone seller
declaw LLM platform: routes OpenAI-compatible requests to sellers' endpoints, meters tokens, records usage on-chain; a consumer app
CARGO_TARGET_DIR=target.noindex cargo test --workspace

The contract tests (decentralized-micro-services-gateway/tests/contract.rs) compile the contract with solc and run it in revm. The gateway crate's test-util feature exposes FakeChain and TestServer (both listeners over a temporary database and a fake chain), which the client, dequic, and declaw tests use.

Known limitations

  • API keys are bearer credentials: whoever holds a key can use it with its service until it is revoked. Clients must send keys over TLS.
  • Services cannot be unregistered; a seller pauses one with a very high price.
  • The event log is never trimmed.
  • A platform forwards requests to the seller's public ip:port, so a buyer who knows a service's key can also call that endpoint directly, unmetered. Sellers must allow only the platform to reach their endpoint (firewall it, or check a shared secret).
  • The app tag is not verified on-chain: any seller can tag a service with any app, which is why platforms accept only services their sellers reported to them.
  • The chat dapp lists standalone services sold in minutes; the declaw dapp lists the services declaw offers.

Complete dev evn deployment

export LITHEUM_NETWORK=local

  1. Contract cd DeMSG scripts/deploy-servicemarketplace.sh lcli 1 eth-call --to "$(cat ../LitheumCore/deployments/local/servicemarketplace)"
    --data "$(lcli 1 function-selector 'serviceCount()' | sed -n 's/^Selector: //p')" That should print a zero word, not bare 0x.
  2. demsgd Copy examples/demsg.toml to ~/.litheum/demsg/config.toml. Set demsg_contract_address to the new address and start_block_id to the current block (lcli 0 block tip). Delete ~/.litheum/demsg/demsg.redb if it exists. Then: cargo run -p demsgd -- --config ~/.litheum/demsg/config.toml curl http://127.0.0.1:8072/demsg/health
  3. dequic cargo run -p dequic scripts/register-dequic.sh --name dequic --price 0.001 --ip 127.0.0.1 --port 8081 service_ip and service_port in ~/.litheum/dequic/config.toml must be 127.0.0.1 and 8081. Check with curl http://127.0.0.1:8081/service-info.
  4. declaw Copy examples/declaw.toml to ~/.litheum/declaw/config.toml. For a local-only test, delete the [biller] section so it does not need a wallet or chain sync. cargo run -p declaw curl http://127.0.0.1:8091/info Sellers register their own ollama ip:port from the declaw dapp. declaw does not register itself.
  5. Dapps cd ../Dapps scripts/deploy-js-libs.sh scripts/set-demsg-url.sh http://127.0.0.1:8070 scripts/set-declaw-url.sh http://127.0.0.1:8091 scripts/deploy-chat-js.sh && scripts/deploy-chat-html.sh scripts/deploy-declaw-js.sh && scripts/deploy-declaw-html.sh