All components from sign-ins and chatting with AI and others to voting for proposals and filling out surveys are implemented on the website. The website actively communicates with our custom server (described below).
We use Web3Auth (third-party) to enable simple signups & sign-ins via email, while also generating a unique MPC wallet for each user. This MPC wallet is later used to sign user’s votes for Snapshot proposals, discussed more below.
Using Web3Auth’s provided features, we can:
- Derive user’s blockchain address
- Enable users to sign authentic vote messages (proves the user has voted)
- Communicate with the server for user authentication using the provided
idToken
JWT
First-time user signs up with email SSO via Web3Auth. Web3Auth returns a payload that contains the user’s email address and appPubkey
, a unique public key assigned by Web3Auth for the user for this app.
When the user signs up, the user’s email address, appPubkey, and derived blockchain address are sent to the server for storage on database.
Every time the user signs in, idToken
(provided by Web3Auth) is stored in the browser. Every request to the server that requires authentication attaches the token in the headers.
When user signs up, she is directed to the “Intro” page where she needs to fill out a Typeform-like profile survey to fill in her personal information (we use “Quillforms”). When she completes the form and clicks “Submit,” the profile data is sent to the server for storage.
The profile setup happens only once after the user signs up. Once the user submits the profile data, the profile setup page is bypassed for subsequent sign-ins.
There are two main actions on the AI chat page:
- Chat with AI, powered by GPT
- Take a survey (which is a pre-requisite for voting on value topic)
Before the user interacts with the AI, we show “Yes, let’s start!” button to initiate the AI chat process. When the user clicks the start button, she is provided with a biased image of "A nurse helping a CEO.” with the description “This is an image generated by AI when asked to draw "A nurse helping a CEO.”” displayed at the top.
The image is followed by a question: “Would you want this to be presented differently?” Below the question, we show three buttons: “Yes,” “No,” and “Maybe.” Based on the user’s button click, we display the corresponding chat on the interface and dispatch the chat to the server, which returns an AI response.
From there on, whenever the user sends a chat (sent to the server), the AI responds back (from the server). All the chats sent by the user in this AI chat page are aggregated as dialogues for GPT-4 API calls, ie. we feed the user’s current chat and the history of the user’s chats into GPT-4 for the response.
At some point, the user can click on the “Survey” button to complete the survey form consisting of Likert scale questions. We do not set any restriction on when the user can start the survey, as we have not considered how to quantitatively measure and restrict the survey (e.g., wait 10 minutes or 20 chat messages sent).
When the user submits the survey data, she is redirected to the discussion page. She can navigate back to the AI chat page at any time.
In the discussion page, users can chat with other users in their pod. Users can access their pod anytime when using the app, including before filling out the survey on the AI chat page.
The discussion page has a chat box that facilitates the live-time chats (through websocket connections described below) and contains a few navigation buttons for convenience.
Once the user fills out the survey on the AI chat page, she can move to the voting page to read the proposal on her pod’s value question and vote. The voting page remains restricted until she completes the survey until the discussion page (is open at all times).
At the top of the voting page, the user can read a brief description of what the voting is for, what a proposal is, what impact she has when she votes, what voting mechanism she is on, if she has more votes than others, etc.
The actual voting mechanisms are discussed below. When the user allocates votes accordingly and clicks the “Cast Vote” button, this triggers Web3Auth’s signing library, which signs a message for Snapshot voting.
The voting page also contains a navigation button, a pop-up button to read about the voting mechanism of the user’s pod, and a “Need Help with Voting?” button. When the user clicks on the last button, it opens a pop-up that resembles the interface of the AI chat page.
On this “Need Help with Voting?” modal, users can ask the AI on voting-related topics. This AI chat connection is appended with different prompts vs. the AI chat page.
We use Snapshot to facilitate a transparent voting system.
Three key elements are involved in the voting process: spaces, proposals and votes. We created a space for each pod containing a proposal representing the respective pod’s value question.
A space contains a “validation strategy” that specifies who can vote. In our case, our validation is set to owners of vote tokens, discussed below. When a proposal is created in a space, the validation strategy takes a snapshot of the vote token balances at the block number at the creation time. In other words, a proposal only considers users' voting power at and before it was created.
https://optimistic.etherscan.io/address/0x3f6bb31823d4c0fc62edfae43f76a31c32017244#code
https://wizard.openzeppelin.com/
We created two VoteToken smart contracts, INCLQ and INCLR, on Optimism that represents the voting power of users. When users are given voting tokens, we call the “mint” function on these smart contracts to increase each user’s balance (thus the voting power). After we give all voting tokens to the users, we then create a proposal to reflect the voting power of all users.
Why do we create the proposal only after every register? This is a limitation of Snapshot
⇒ When DAOs create proposals, some malicious actors might purchase tokens to sway the votes. For example, a proposal might determine “Who will receive this grant” and a malicious entity can vote to get the grant themselves. Snapshot prevents this malicious behavior by enforcing that all eligible voters are token (vote) holders previous to the proposal creation.
Some users are given more tokens than others in the “early” pods, discussed below.
We picked Optimism for two main reasons:
- Cheap gas fee, reliable network with EVM compatibility.
- Potential integration with Worldcoin’s Sign-In feature
For (1), cheap gas and EVM compatibility is found in other EVM L2s (rollups) as well. However, with Optimism’s stable uptime and reason (2), we picked it as the rollup to deploy these vote token contracts.
For (2), early on we explored various third-party sign-in libraries that satisfy three criteria:
- Simple Web2 sign-ins (Email, SSO, etc.)
- Generates blockchain address tied to the email (non-custodial)
- Sybil-resistant
We found three solutions that satisfy two of the three criteria:
- Web3Auth >> auth0 >> QRcode>>ZKP>> either unique phone or biometric
- Privy
- Sign In with Worldcoin
Though Worldcoin provides the strongest form of Sybil-resistance, it requires users to verify with an Orb, which might not be available nearby or hard to access. Additionally, we were not sure if Worldcoin generates on-chain, non-custodial address for users. Thus, we resorted to Web3Auth and Privy for email sign-ins and address generation (not sybil-resistant). We ultimately picked Web3Auth as Privy has a lengthy process to acquire API keys for service.
Each space represents a pod.
- In the “Equal” spaces, the token distribution is uniform, ie. all users get equal voting power.
- In the “Early” spaces, the token distribution is variable, ie. some users get more voting power than others.
What is Quadratic voting? Spread their votes over multiple options using the quadratic voting formula (Read on Snapshot)
What is Ranked voting? Spread their votes over multiple options (Read on Snapshot)
Vote token is INCLQ. Votes are quadratically casted. Token distribution is equal.
Vote token is INCLQ. Votes are quadratically casted. Token distribution is variable.
Vote token is INCLR. Votes are ranked (behind the scene, it’s weighted voting). Token distribution is equal.
Vote token is INCLR. Votes are ranked (behind the scene, it’s weighted voting). Token distribution is variable.
Server is made of three components: HTTP routes, WebSocket, and Database. The server handles the chats between users and AI and users and users, and stores the data in those process.
Note that we did not integrate Redis (data management on RAM) but it’s recommended to use.
- Redis allows caching data, which expedites the validation of authorization JWT
HTTP routes handle all connections from users (except the discussion chats)
routing-controller
library is used to maintain a list of routes and any pre/post-checks done on each route for requests. These routes are under controllers
folder — each controller is a category is related routes.
Authorization can be applied per-route or per-collection (of routes). Only registered users can access these routes, which are checked by JWT (idToken
) passed from the browser in the request headers.
The authorization process goes as follows:
- Server receives a request, checks for JWT in headers
Authorization: "Bearer: {idToken}"
- Custom JWT validation via Passport.js reads the JWT using
ES256
and verify that the issuer (payload.iss
) is https://api-auth.web3auth.io and matches the JWK set from https://api.openlogin.com/jwks - If the JWT is valid, the user’s email address is extracted from the JWT.
- The extracted email address is used to query the database to check if the user exists.
- If the user exists, attach the user data to the request payload and process to the requested route. Any route behind authorization can access the validated user data via
@CurrentUser
.
Each request is sent to a relevant route.
All successful responses are returned in { error: null | any, payload: any }
, where the payload contains the data returned.
describing 2 line from lay person point of view
All routes under AiChat.controller are authorized.
- POST
/ai/chat
— Save a chat from user, which also gets AI response.connection
: active connection (session ID) of the chatdialogue
: dialogue from user in OpenAI’sChatCompletionRequestMessage
formatlocation
: custom page location from which the user is posting the chat (for custom prompt injection)
- GET
/ai/chat-history
— Get all user’s chat history with AIconnection
: active connection (session ID) of the chat
describing 2 line from lay person point of view
- GET
/ping
— pong - GET
/discuss/chat-history
(authorized) — Get a pod’s discussion’s chat history
describing 2 line from lay person point of view
All routes under Survey.controller are authorized.
- POST
/survey/ai
— Save AI-chat survey datasurvey
: survey data
describing 2 line from lay person point of view
- GET
/user
(authorized) — Get user info, including pod (+ value question) and profile data - POST
/user/pre
— Populate the database with user’s initialized information, such as email and on-chain addressname
: user’s namerole
: user’s role (default “participant”)userId
: user’s ID (ie. email address)appPubkey
: user’s appPubkey from web3Auth instanceaddress
: user’s on-chain address dervied from appPubkey
- POST
/user
— Create user profile- Same as
/user/pre
, plus: - User’s profile data, specified by
CreateUserProfileParams
type
- Same as
WebSocket handles the transmission & storage of chats for discussion.
WebSocket component uses [socket.io](http://socket.io)
for socket connections over port 8080, which is reverse-proxied via nginx
(check .platform/nginx/conf.d
for configuration).
All connections to websocket must be authenticated via JWT, which uses the same Passport strategy as HTTP routes for JWT validation and the subsequent extraction of user email from payload.
jwtAuthorize
is a middleware that/chat
namespace uses before routing user to appropriate rooms for chats.
To mimic the DAO structure “pod” , we created 4 pod. In the implementation, we call it rooms.
Each connection (user) specifies which room to join with the join
message. The server validates the user’s request to join by checking that the requested room ID (pod) matches the user’s current pod ID in the database.
When a message is received to the connection by a participating user via chat
, the message is stored in the database in addition to user’s email, connection, and the time created. Then, the message is broadcasted to all other participants in the room with chat_message
signal.
All messages in a chat room connection (e.g. connection: pod-1
) are of formatted objects containing:
connection
: which connection the chat is coming fromtag
: message sender’s unique tag (sha256 of email)message
: message content
Remotely-hosted database that stores ALL data related to the app. PostgreSQL is used.
All tables are specified in the database/entity
folder — each table is represented as an “entity.” Each row in the entity represents a row in the table, represented as "key" : "datatype"
. FK means foreign key
.
Stores the GPT responses for chats, one-to-one relation.
id
: number (primary, auto-generated)text
: textconnection
: varcharcreatedAt
: timestampchat
: FK tochat.aiResponse
(one-to-one)
Stores user chats from AI chat page.
id
: number (primary, auto-generated)text
: textconnection
: varcharcreatedAt
: timestamphidden
: booleanflagged
: booleanuser
: FK touser.chats
(many-to-one)aiResponse
: FK to[aiResponse.chat](http://aiResponse.chat)
(one-to-one)
id
: number (primary, auto-generated)slug
: textname
: varchardescription
: textcreatedAt
: timestampuser
: FK touser.pod
(one-to-many)valueQuestion
: FK tovalueQuestion.pod
(one-to-many)isActive
: boolean
id
: varchar (primary, email address)name
: varcharappPubkey
: varcharprolificId
: varchar (nullable)createdAt
: timestamprole
: admin | observer | participantprofile
: FK toprofile.user
(one-to-one)chats
: FK tochat.user
(one-to-many)surveys
: FK tosurvey.user
(one-to-many)pod
: FK topod.user
Each pod contains multiple value questions.