A realtime rundown and remote event timer in the browser, where the timekeeper changes the rundown and runs the clock from a laptop, the presenters see the time left on a screen in front of them, and the crew and the participants waiting their turn follow the same rundown on their phones, seeing who is on now, how long is left, and when their turn comes. It is for anyone who wants an event to run on time, smoothly, and clearly. Running late is one problem, but not knowing what changed is a bigger one, so even when a session runs over or the rundown shifts, the live rundown is the one schedule everyone trusts.
- Kind
- app
- Field
- Product design, UX design, system design, software architecture, real-time systems, web development, backend engineering, serverless infrastructure, DevOps, product management, market research, pricing
- Stack
- TypeScript, SvelteKit, Bun, Cloud Run, Cloudflare, Firebase Realtime Database, Firebase Auth
- Language
- Bahasa
- Status
- Live at waktumu.com





















Two apps for an apartment tower under construction in Indonesia, one on iOS for residents to enter the building, run the lights, curtains and air conditioning in their unit, order services and pay, and one on the web for the staff who run the building. Underneath sits a smart system platform that knows the building's spaces and devices but never its people, so the apartment keeps its residents to itself and sends over only what each of them may do.
- Kind
- app
- Field
- Product design, UI/UX design, system design, software architecture, mobile development, iOS development, web development, platform development, DevOps, IoT, smart home, access control, integrated building management system, property management
- Apps
- Resident app on iOS, IBMS dashboard on the web
- Stack
- SwiftUI, SvelteKit, Svelte 5, TypeScript, Tailwind CSS, Bun, Go, Tuya Cloud, Cloud Run, Cloudflare, Neon, Postgres, Firebase, Xendit
- Status
- Building is still under construction
client, err := tuya.New(accessID, accessSecret, "https://openapi-sg.iotbing.com")
store, err := postgres.NewAppAccountStore(ctx, pool)
app := appaccount.NewService(client, store)
devices, err := app.Devices(ctx, "ardian")
if err != nil {
return err
}
err = app.SendCommands(ctx, devices[0].ID, []tuya.DataPoint{
{Code: "switch_led", Value: true},
}) 01 Owner resolved store.Get
- query
- SELECT owner, tuya_uid, created_at, updated_at FROM tuya_app_accounts WHERE owner = $1
- $1
- ardian
- tuya_uid
- ay1762310745982Kq7Ts
02 Devices listed client.UserDevices
- GET
- https://openapi-sg.iotbing.com/v1.0/users/ay1762310745982Kq7Ts/devices
- stringToSign
- "GET\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\n/v1.0/users/ay1762310745982Kq7Ts/devices"
- client_id
- kx7d3w9qpm4nt8vr2yhe
- sign
- 9FC6FF9642D50662BF74CA300AB1743791E7B6320F91F666F4072C45D878FD2D
- t
- 1793822268636
- sign_method
- HMAC-SHA256
- access_token
- 3f9c1e7a5b2d8064e1c7a9f3b5d20e48
- nonce
- 90d95b10f5eb1c919b5ea34fcb5ddd72
- response
- {"success":true,"t":1793822268720,"result":[{"id":"bf3a91c4e27d58f0a6kxqz","name":"Bedroom lights","category":"dj","online":true,"status":[{"code":"switch_led","value":false}]},{"id":"bf8e2d7a19c3054b6fm4tw","name":"Desk lamp","category":"dj","online":true,"status":[{"code":"switch_led","value":true},{"code":"bright_value","value":300}]},{"id":"bf51c7e9a2d6830f4bnr8y","name":"Kitchen switch","category":"kg","online":true,"status":[{"code":"switch_1","value":true},{"code":"switch_2","value":false}]}]}
03 Command sent client.SendCommands
- POST
- https://openapi-sg.iotbing.com/v1.0/iot-03/devices/bf3a91c4e27d58f0a6kxqz/commands
- stringToSign
- "POST\n8479c9c60cd5d531054c49333c7b361a9ce41b9b313ab8eb6bc9df4141f658ef\n\n/v1.0/iot-03/devices/bf3a91c4e27d58f0a6kxqz/commands"
- client_id
- kx7d3w9qpm4nt8vr2yhe
- sign
- 92F4BB405AC7CFBAE8D8AC2501B24825D06A16754316FA6FD9B11C367A062617
- t
- 1793822268775
- sign_method
- HMAC-SHA256
- access_token
- 3f9c1e7a5b2d8064e1c7a9f3b5d20e48
- nonce
- 0686b06cfa2b4aaad09ee2b21fa7178c
- Content-Type
- application/json
- body
- {"commands":[{"code":"switch_led","value":true}]}
- response
- {"success":true,"t":1793822268859,"result":true}
04 Returned return
- err
- nil
A Go library for the Tuya Cloud OpenAPI, the service that lets your own software read and switch the lights, plugs and other devices in a Tuya smart home, with a second package that remembers which Tuya account each of your own users connected. It is for Go developers with a Tuya cloud project, whether they script a few switches of their own or build an app whose users bring their homes along, and it keeps that link in PostgreSQL or Firestore. It is judged by whether Tuya accepts every request it signs and whether a call survives the access token expiring, because Tuya refuses any request whose signature is off and turns down every call made with a dead token.
- Kind
- library
- Field
- Internet of Things, software engineering, backend engineering, API integration, applied cryptography, databases
- Language
- Go 1.25
- Module
- go.trueardian.com/tuya
- Packages
- tuya, appaccount, postgres, firestore
- Version
- v0.10.1
Findings
- 01
The client never trusts a clock. Tuya sends an expire_time with every token, but the token is reused until Tuya itself answers 1010, then a new one is fetched and the call retried once, and a second 1010 comes back as the error instead of a loop.
- 02
Tuya checks a signature against the query it decoded, so a comma escaped as %2C between IDs fails as 1004 sign invalid. The client leaves those commas bare.
- 03
Owning a device and being allowed to use it are different things in Tuya. A device someone shared with you isn't in your own list, so HasDevice only answers and SendCommands never consults it.
- 04
An app account's homes and rooms live in Tuya's older asset family, and every /v2.0/cloud/space endpoint rejects their IDs, so appaccount has no spaces at all.
- Human: music!
- Ava: sure, something calm, or anything goes?
- Human: anything
- Ava: @yori play what the human's been into lately
- Yori: on it! lately the human keeps listening to Never Is Enough by Iann Dior, it's playing now
- Human: @rafal any important email today? any events? I don't think so
- Rafal: right, nothing, just emails that don't matter, and no events today either
- Human: ok, a slow day then. please open the curtains a little
- Ava: @zee
- Zee: opened them a quarter!!
- Human: oh, I'm picking up Nay at 5 later
- Ava: got it, I'll remind you at 4:30 so you have time to get ready
- 16:30
- Ava: it's 4:30, half an hour until you pick up Nay (woken by Postera)
- 16:45
- Ava: 15 minutes left, time to head out for Nay (woken by Postera)
- 17:00
- Ava: it's 5, Nay is waiting. are you on your way? (woken by Postera)
- Human: yes, I'm already on the way
- Ava: great, I've cancelled your 5:10 reminder. drive safe!
A group chat for people in Indonesia whose other members are AI agents, where Ava listens and hands work to Zee for the Tuya smart home, Yori for Spotify, and Rafal for Gmail, Calendar, and Contacts, on web and iOS. Every agent reads and writes one shared thread per person instead of passing summaries along, and remembers what was said, what it has learned about the person, and what it promised to do later. Since every turn rereads a long thread that agent mostly did not write, the model was judged by how well it keeps using long context.
- Kind
- app
- Field
- Agentic AI, multi-agent systems, AI engineering, knowledge graphs, system design, software architecture, backend engineering, web development, iOS development, API integration, serverless infrastructure, DevOps, smart home, double-entry accounting
- Agents
- Ava, Zee, Yori, Rafal
- Memory
- Episodic and semantic in Zep, prospective in Postera
- Model
- Gemini 3.5 Flash
- Stack
- Go, Google ADK, Zep, SvelteKit, Bun, SwiftUI, Cloud Run, Cloud Tasks, Neon, Postgres, Firestore, Firebase
- Status
- Paused
Scripted, following her real instruction
The thread on the screen
Her instruction
Balasan specialist sudah jelas = balasan finalku KOSONG. Turn kosong itu normal dan benar untuk orkestrator; aku bukan pengeras suara yang menirukan ucapan teman. Once the specialist's reply is clear, my final reply is EMPTY. An empty turn is normal and right for an orchestrator, I am not a loudspeaker repeating what a friend said.
01 Woken by the human POST /ava
- message
- turn off the bedroom light
- channel
- text
02 Hands it to Zee zee
- message
- "@zee"
03 Zee answers zee
- response
- "done, the bedroom light is off"
04 Final reply 200
- response
- ""
Ava is the lead agent of Avagenc Chat, a language model given a written instruction and a handful of tools, who answers every message that names no one else. When the work belongs to a specialist she calls Zee for the smart home, Yori for Spotify, or Rafal for Gmail, Calendar, and Contacts, and when it belongs to later she writes a note to her future self through Postera. Who she is lives in an instruction written in Indonesian in her own first person, so she is judged by whether she behaves the way it says, never repeating a specialist in the chat, never going quiet on a call, and never letting the human see the machinery behind her.
- Kind
- agent
- Field
- Agentic AI, multi-agent systems, AI engineering, prompt engineering, conversation design, voice interfaces, software engineering
- Model
- Gemini 3.5 Flash
- Tools
- zee, yori, rafal, wake_future_self, list_upcoming_wakes, cancel_upcoming_wake
- Instruction
- Indonesian, in her own first person
- Built with
- Go, Google ADK, agentkit, Postera
- Module
- go.avagenc.com/ava
Scripted, following Zee's real instruction and tools
Thread
The home
- Bedroom curtain
- percent_control 0 fully open, closed since last night
- Living room switch
- Ceiling, switch_1 on Lamp, switch_2 off
01 Mentioned by the human runner.Run
- speaker
- human
- message
- @zee open the bedroom curtains
- run instruction
- ran by the human
02 Checks the link get_account
- args
- {}
- result
- {"owner":"ardian","tuya_uid":"ay1762310745982Kq7Ts"}
03 Reads the home list_devices
- args
- {}
- result
- {"devices":[{"id":"bf7e21c94d0a3c5f8e1kqx","category":"cl","name":"Bedroom curtain","status":[{"code":"control","value":"stop"},{"code":"percent_control","value":100}],"code_name_mapping":[]},{"id":"bf2d84a61c7e9b03f5wnrj","category":"kg","name":"Living room switch","status":[{"code":"switch_1","value":true},{"code":"switch_2","value":false}],"code_name_mapping":[{"identifier":"switch_1","name":"Ceiling"},{"identifier":"switch_2","name":"Lamp"}]}]}
04 Moves the curtain send_commands
- args
- {"device_id":"bf7e21c94d0a3c5f8e1kqx","commands":[{"code":"percent_control","value":0}]}
- result
- {"ok":true}
05 Answers once final response
- text
- opened them all the way
An AI agent for people whose lights, switches, and curtains run on Tuya, a cloud that many smart home brands build on. It turns a message like "open the bedroom curtains" into the exact value the curtain understands, sends it, and tells the person what happened. It lives in Avagenc Chat, where Ava hands it the smart home or the person mentions it by name, and it ships as a Go module that any other product can import. It is judged by whether the device ends up the way the person asked and the reply tells the truth about it, because Zee never sees the room, and a confident "done" about a curtain that never moved is worse than no answer.
- Kind
- agent
- Field
- Agentic AI, AI engineering, prompt engineering, Internet of Things, software engineering, API integration
- Model
- Gemini 3.5 Flash in Avagenc Chat
- Built with
- Go, Google ADK, agentkit, tuya
- Module
- go.avagenc.com/zee
- Status
- Paused with Avagenc Chat
Scripted, in English, following Yori's instruction
Yori calls a tool, unseen in the chat
- tool
- my_playlists
- arguments
- {}
- returned
- {"playlists":[…, {"id":"…","name":"🌌","url":"https://open.spotify.com/playlist/…"}, …]}
Yori calls a tool, unseen in the chat
- tool
- playlist_tracks
- arguments
- {"playlist_id":"…"}
- returned
- {"tracks":[…, {"id":"…","name":"Kacamata","uri":"spotify:track:…"}, …]}
Yori calls a tool, unseen in the chat
- tool
- play
- arguments
- {"context_uri":"spotify:playlist:…","uri":"spotify:track:…"}
- returned
- {"ok":true}
Yori is the member of Avagenc Chat who handles music. Someone writes "@yori put on Bohemian Rhapsody", or Ava passes the request on, and Yori, a Gemini model with eleven Spotify tools and a written instruction, decides which tools to call, reads what each one returns, and answers in the chat once the music is actually playing. It is for people who would rather ask for a song than open the app, and it is judged by whether it acts instead of answering from memory and tells the truth when Spotify refuses, because an agent that says it can't when it can is as wrong as one that pretends it did.
- Kind
- agent
- Field
- Agentic AI, multi-agent systems, AI engineering, prompt engineering, software engineering, API integration
- Lives in
- Avagenc Chat
- Model
- Gemini 3.5 Flash
- Tools
- Spotify search, playlists, devices, and playback
- Built with
- Go, Google ADK, agentkit, spotify
- Status
- Paused
Scripted from Rafal's instruction and tools, not a recording
01 Handed on by Ava runner.Run
- session
- chat-ardian
- speaker
- ava
- message
- @rafal
- run instruction
- ran by Ava
02 Model calls a tool get_contacts
- args
- {}
03 Tool returns get_contacts
- contacts[0].name
- Bob Tan
- contacts[0].emails
- ["bob@example.com"]
- contacts[1].name
- Sari Wulandari
- contacts[1].emails
- ["sari@example.com", "sari.w@work.example"]
04 Model calls a tool get_events
- time_min
- 2026-11-06T12:00:00+07:00
- time_max
- 2026-11-06T13:00:00+07:00
05 Tool returns get_events
- events
- []
06 Model calls a tool add_event
- summary
- Lunch with Bob
- start
- 2026-11-06T12:00:00+07:00
- end
- 2026-11-06T13:00:00+07:00
- guests
- ["bob@example.com"]
07 Tool returns add_event
- summary
- Lunch with Bob
- start
- 2026-11-06T12:00:00+07:00
- attendees
- ["bob@example.com"]
- html_link
- https://www.google.com/calendar/event?eid=…
08 Model answers final response
- function_call
- none, so the run ends
Rafal is the AI agent for a person's Google account in Avagenc Chat. Someone types "@rafal" with a request, or Ava, the lead agent, passes one on, and Rafal reads and sends mail, checks and adds calendar events, and finds and saves contacts by calling Google itself. It treats the three as one surface, so it finds a person's address in Contacts before it invites or emails them. It is judged by whether every answer comes from a fresh call to Google and every recipient from the person's own contacts, because an address guessed from a name can belong to a stranger.
- Kind
- agent
- Field
- Agentic AI, AI engineering, prompt engineering, multi-agent systems, software engineering, API integration, OAuth and identity
- Model
- Gemini 3.5 Flash
- Tools
- read_messages, send_email, get_events, add_event, get_contacts, add_contact
- Framework
- Google ADK for Go
- Module
- go.avagenc.com/rafal
- Lives in
- Avagenc Chat
- Status
- Paused with Avagenc Chat
Without agentkit764 lines
You have to manually implement ADK's session and memory services on top of Zep. Then you map every session to a Zep thread, put the speaker and the local time in front of every history message, write the instruction block that explains them to the model, check who owns each thread, and search the human's knowledge graph for memories.
zep/adk/memory.go, 64 lines
package adk
import (
"context"
"fmt"
"github.com/getzep/zep-go/v3"
"github.com/getzep/zep-go/v3/client"
"github.com/getzep/zep-go/v3/option"
"google.golang.org/adk/memory"
adksession "google.golang.org/adk/session"
"google.golang.org/genai"
)
type graphClient interface {
Search(ctx context.Context, request *zep.GraphSearchQuery, opts ...option.RequestOption) (*zep.GraphSearchResults, error)
}
// MemoryService implements memory.Service over a user-scoped Zep knowledge
// graph. It is independent of any session: SearchMemory is keyed by UserID, not
// by thread.
type MemoryService struct {
graphClient graphClient
}
var _ memory.Service = (*MemoryService)(nil)
func NewMemoryService(c *client.Client) *MemoryService {
return &MemoryService{graphClient: c.Graph}
}
// SearchMemory runs a user-scoped graph search and returns Zep's materialized
// context block as a single memory entry. The response is empty when nothing
// matches.
func (s *MemoryService) SearchMemory(ctx context.Context, req *memory.SearchRequest) (*memory.SearchResponse, error) {
scope := zep.GraphSearchScopeAuto
res, err := s.graphClient.Search(ctx, &zep.GraphSearchQuery{
Query: req.Query,
UserID: &req.UserID,
Scope: &scope,
})
if err != nil {
return nil, fmt.Errorf("zep: search graph of user %q: %w", req.UserID, err)
}
text := res.GetContext()
if text == nil || *text == "" {
return &memory.SearchResponse{}, nil
}
return &memory.SearchResponse{
Memories: []memory.Entry{{
Content: genai.NewContentFromText(*text, genai.RoleModel),
}},
}, nil
}
// AddSessionToMemory is a no-op: SessionService.AppendEvent already writes every
// turn to the Zep thread, which Zep ingests into the user graph automatically.
// It exists only to satisfy memory.Service.
func (s *MemoryService) AddSessionToMemory(ctx context.Context, sess adksession.Session) error {
return nil
}zep/adk/session.go, 700 lines
package adk
import (
"context"
"errors"
"fmt"
"iter"
"strings"
"sync"
"time"
"unicode"
"github.com/getzep/zep-go/v3"
"github.com/getzep/zep-go/v3/client"
"github.com/getzep/zep-go/v3/option"
threadclient "github.com/getzep/zep-go/v3/thread/client"
userclient "github.com/getzep/zep-go/v3/user"
"google.golang.org/adk/model"
adksession "google.golang.org/adk/session"
"google.golang.org/genai"
)
// TZResolver resolves the user's timezone for the time harness.
// Use StaticTZ or TZFromContext to create one.
type TZResolver struct {
resolve func(context.Context) (*time.Location, error)
}
type timeHarnessConfig struct {
zoneResolver *TZResolver
}
// Speaker identifies the actor behind an inbound user-role turn: a display name
// and the Zep role under which the message is persisted. An empty Role means
// the ADK-derived role (user) is kept unchanged.
type Speaker struct {
Name string
Role zep.RoleType
}
// SpeakerResolver resolves the Speaker attributed to inbound user-role turns.
// Use StaticSpeaker or SpeakerFromContext to create one.
type SpeakerResolver struct {
resolve func(context.Context) (Speaker, error)
}
type userClient interface {
Get(ctx context.Context, userID string, opts ...option.RequestOption) (*zep.User, error)
Add(ctx context.Context, request *zep.CreateUserRequest, opts ...option.RequestOption) (*zep.User, error)
}
var _ userClient = (*userclient.Client)(nil)
type threadClient interface {
Create(ctx context.Context, request *zep.CreateThreadRequest, opts ...option.RequestOption) (*zep.Thread, error)
AddMessages(ctx context.Context, threadID string, request *zep.AddThreadMessagesRequest, opts ...option.RequestOption) (*zep.AddThreadMessagesResponse, error)
Get(ctx context.Context, threadID string, request *zep.ThreadGetRequest, opts ...option.RequestOption) (*zep.MessageListResponse, error)
}
var _ threadClient = (*threadclient.Client)(nil)
type SessionService struct {
threadClient threadClient
userClient userClient
speakerResolver *SpeakerResolver
msgHistoryLength int
instructionKey string
timeHarness *timeHarnessConfig
}
var _ adksession.Service = (*SessionService)(nil)
type Option func(*SessionService)
func NewSessionService(c *client.Client, opts ...Option) *SessionService {
s := &SessionService{
threadClient: c.Thread,
userClient: c.User,
}
for _, opt := range opts {
opt(s)
}
return s
}
func WithMessageHistoryLength(n int) Option {
return func(s *SessionService) {
s.msgHistoryLength = n
}
}
// StaticSpeaker returns a speaker resolver backed by a fixed Speaker value.
// An empty Name means inbound user turns fall back to the session's UserID.
// An empty Role keeps the ADK-derived role unchanged.
func StaticSpeaker(sp Speaker) *SpeakerResolver {
return &SpeakerResolver{
resolve: func(context.Context) (Speaker, error) {
return sp, nil
},
}
}
type speakerContextKey struct{}
// WithSpeaker returns a child context carrying the Speaker for SpeakerFromContext.
func WithSpeaker(ctx context.Context, sp Speaker) context.Context {
return context.WithValue(ctx, speakerContextKey{}, sp)
}
// SpeakerFromContext returns a speaker resolver that reads the inbound user-turn
// Speaker from the request context. Use WithSpeaker to provide the Speaker per
// request. AppendEvent returns an error when the Speaker is absent or has an empty
// Name, so a forgotten context does not silently mislabel the turn.
func SpeakerFromContext() *SpeakerResolver {
return &SpeakerResolver{
resolve: func(ctx context.Context) (Speaker, error) {
sp, ok := ctx.Value(speakerContextKey{}).(Speaker)
if !ok || sp.Name == "" {
return Speaker{}, errors.New("zep: SpeakerFromContext active but speaker absent or name empty")
}
return sp, nil
},
}
}
// WithSpeakerResolver sets how inbound user-role turns are attributed in the Zep
// thread, using the provided speaker source. Without this option, user turns are
// attributed to the session's UserID.
func WithSpeakerResolver(r *SpeakerResolver) Option {
if r == nil || r.resolve == nil {
panic("zep: WithSpeakerResolver: resolver must be created by StaticSpeaker or SpeakerFromContext")
}
return func(s *SessionService) {
s.speakerResolver = r
}
}
// WithInstruction registers the state key under which a combined session
// instruction block is written during Get. The block includes a message format
// section whenever history is present, and a current-time section when the time
// harness is enabled. The consumer includes {key?} (or {key}) in their agent
// instruction for the placeholder to be resolved by the ADK runner. When not
// set, no instruction is written to state.
func WithInstruction(key string) Option {
return func(s *SessionService) {
s.instructionKey = key
}
}
// StaticTZ returns a zone backed by a fixed IANA timezone.
// Empty string means UTC. Invalid timezone names panic because they are
// programmer configuration errors.
func StaticTZ(tz string) *TZResolver {
loc, err := time.LoadLocation(tz)
if err != nil {
panic(fmt.Sprintf("zep: StaticTZ: %v", err))
}
return &TZResolver{
resolve: func(context.Context) (*time.Location, error) {
return loc, nil
},
}
}
type timezoneContextKey struct{}
// WithTimezone returns a child context carrying an IANA timezone string
// for TZFromContext.
func WithTimezone(ctx context.Context, tz string) context.Context {
return context.WithValue(ctx, timezoneContextKey{}, tz)
}
// TZFromContext configures the time harness to resolve the timezone from the
// request context. Use WithTimezone to provide the timezone per request.
func TZFromContext() *TZResolver {
return &TZResolver{
resolve: func(ctx context.Context) (*time.Location, error) {
tz, _ := ctx.Value(timezoneContextKey{}).(string)
if tz == "" {
return nil, errors.New("zep: TZFromContext active but timezone absent or empty")
}
loc, err := time.LoadLocation(tz)
if err != nil {
return nil, fmt.Errorf("zep: TZFromContext: %w", err)
}
return loc, nil
},
}
}
// WithTimeHarness enables time-awareness using the provided zone source.
// A nil zone enables time-awareness without timezone conversion; timestamps are
// formatted in their parsed timezone, and the current-time anchor uses UTC.
//
// When enabled:
// - History message prefix becomes [YYYY-MM-DD HH:MM Name] instead of [Name]
// - Any message with unparseable CreatedAt causes Get to return an error
func WithTimeHarness(zoneResolver *TZResolver) Option {
if zoneResolver != nil && zoneResolver.resolve == nil {
panic("zep: WithTimeHarness: zone must be created by StaticTZ or TZFromContext")
}
return func(s *SessionService) {
s.timeHarness = &timeHarnessConfig{zoneResolver: zoneResolver}
}
}
func (s *SessionService) ensureUser(ctx context.Context, userID string) error {
_, err := s.userClient.Get(ctx, userID)
if err == nil {
return nil
}
var notFound *zep.NotFoundError
if !errors.As(err, ¬Found) {
return err
}
_, err = s.userClient.Add(ctx, &zep.CreateUserRequest{UserID: userID})
return err
}
// ErrSessionOwnerMismatch is returned by Get when the requesting user is not
// the owner of the requested thread. Callers (e.g. an HTTP handler) should map
// it to 403 Forbidden.
var ErrSessionOwnerMismatch = errors.New("zep: session does not belong to user")
func derefOrEmpty(s *string) string {
if s == nil {
return ""
}
return *s
}
// verifyThreadOwner enforces that an existing thread belongs to the requesting
// user. It runs only when expectedUserID is non-empty (an authenticated
// request carries a user identity); requests without an identity — internal
// callers, tests — are not gated.
//
// It is fail-closed: when a user identity is present but Zep does not report a
// thread owner, ownership cannot be confirmed and access is denied. This is the
// security path, so an unconfirmable owner is treated as a mismatch rather than
// waved through.
func verifyThreadOwner(resp *zep.MessageListResponse, expectedUserID string) error {
if expectedUserID == "" {
return nil
}
if derefOrEmpty(resp.GetUserID()) != expectedUserID {
return ErrSessionOwnerMismatch
}
return nil
}
type state struct {
mu sync.RWMutex
m map[string]any
}
var _ adksession.State = (*state)(nil)
func (s *state) Get(key string) (any, error) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.m[key]
if !ok {
return nil, adksession.ErrStateKeyNotExist
}
return v, nil
}
func (s *state) Set(key string, val any) error {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = val
return nil
}
func (s *state) All() iter.Seq2[string, any] {
s.mu.RLock()
cp := make(map[string]any, len(s.m))
for k, v := range s.m {
cp[k] = v
}
s.mu.RUnlock()
return func(yield func(string, any) bool) {
for k, v := range cp {
if !yield(k, v) {
return
}
}
}
}
type events []*adksession.Event
func (e events) All() iter.Seq[*adksession.Event] {
return func(yield func(*adksession.Event) bool) {
for _, evt := range e {
if !yield(evt) {
return
}
}
}
}
func (e events) Len() int { return len(e) }
func (e events) At(i int) *adksession.Event {
if i < 0 || i >= len(e) {
return nil
}
return e[i]
}
type session struct {
id string
userID string
app string
events []*adksession.Event
state *state
lastUpdate time.Time // zero if no timestamped messages were fetched
}
func (s *session) ID() string { return s.id }
func (s *session) AppName() string { return s.app }
func (s *session) UserID() string { return s.userID }
// LastUpdateTime returns the timestamp of the most recent message fetched from
// Zep. Returns zero time.Time{} when no messages carry a parseable CreatedAt
// (e.g. a brand-new or empty thread).
func (s *session) LastUpdateTime() time.Time { return s.lastUpdate }
func (s *session) State() adksession.State { return s.state }
func (s *session) Events() adksession.Events { return events(s.events) }
func (s *SessionService) timeHarnessEnabled() bool {
return s.timeHarness != nil
}
// resolveLocation returns the timezone for header formatting. A nil location
// means timestamps should be formatted in their parsed timezone.
func (s *SessionService) resolveLocation(ctx context.Context) (*time.Location, error) {
if !s.timeHarnessEnabled() || s.timeHarness.zoneResolver == nil {
return nil, nil
}
return s.timeHarness.zoneResolver.resolve(ctx)
}
func (s *SessionService) buildMessageFormatInstruction() string {
if s.timeHarnessEnabled() {
return `[MESSAGES_HISTORY_FORMAT]
Incoming message history will be shown as: [YYYY-MM-DD HH:MM Name] {raw message content}
IMPORTANT AND CRITICAL:
- The bracketed prefix is metadata the system adds to identify the speaker and the message's local time, and is NOT part of the message and NOT something a speaker typed.
- I as an agent should never write a prefix ([YYYY-MM-DD HH:MM Name]) — no leading bracket, no name, no timestamp. My response as an agent should be raw message content only.
- Timestamps are already in local time, so I as an agent should never reason about timezones.
[/MESSAGES_HISTORY_FORMAT]`
}
return `[MESSAGES_HISTORY_FORMAT]
Each history message is shown as: [Name] raw message content
IMPORTANT AND CRITICAL:
- The bracketed prefix is metadata the system adds to identify the speaker. It is NOT part of the message and NOT something a speaker typed.
IMPORTANT:
- I as an agent should never write a prefix ([Name]) — no leading bracket, no name, no timestamp. My response as an agent should be raw message content only.
[/MESSAGES_HISTORY_FORMAT]`
}
const timeAwarenessFraming = "You are time-aware: the time shown above is the authoritative " +
"current local time. Rely on it whenever you need to know or reason about the " +
"current date or time — you never need to look it up elsewhere or call a tool " +
"to obtain it. Within this session — the conversation history above and the " +
"current date and time shown here — all times are already in the user's local " +
"timezone, so you do not need to convert or reason about timezones for them."
// formatElapsed returns a human-readable duration.
// Pluralization is always-plural by design (e.g. "1 minutes") — LLMs
// tolerate this and avoiding plural/singular branching keeps the
// function simple.
func formatElapsed(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%d seconds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%d minutes", int(d.Minutes()))
}
if d < 24*time.Hour {
h := int(d.Hours())
m := int(d.Minutes()) - h*60
return fmt.Sprintf("%d hours %d minutes", h, m)
}
totalMinutes := int(d.Minutes())
days := totalMinutes / (24 * 60)
remaining := totalMinutes - days*24*60
h := remaining / 60
m := remaining - h*60
return fmt.Sprintf("%d days %d hours %d minutes", days, h, m)
}
func buildTimeAwarenessInstruction(loc *time.Location, lastTime time.Time) string {
now := time.Now().UTC()
if loc != nil {
now = now.In(loc)
}
nowStr := now.Format("2006-01-02 15:04")
if lastTime.IsZero() {
return fmt.Sprintf("[CURRENT_TIME]\nCurrent date and time: %s\n\n%s\n[/CURRENT_TIME]", nowStr, timeAwarenessFraming)
}
elapsed := time.Since(lastTime)
return fmt.Sprintf(
"[CURRENT_TIME]\nCurrent date and time: %s\nTime since previous message: %s\n\n%s\n[/CURRENT_TIME]",
nowStr, formatElapsed(elapsed), timeAwarenessFraming,
)
}
// applyInstruction writes the combined session instruction into st under the
// configured key. It is the single point both session entry points funnel
// through — Get (the fetched path) and Create (the auto-create path) — so the
// instruction placeholder resolves identically regardless of which produced the
// session. No-op when no key is registered or there is nothing to inject.
func (s *SessionService) applyInstruction(st *state, hasHistory bool, loc *time.Location, lastTime time.Time) {
if s.instructionKey == "" {
return
}
var parts []string
if hasHistory {
parts = append(parts, s.buildMessageFormatInstruction())
}
if s.timeHarnessEnabled() {
parts = append(parts, buildTimeAwarenessInstruction(loc, lastTime))
}
if len(parts) == 0 {
return
}
_ = st.Set(s.instructionKey, strings.Join(parts, "\n\n"))
}
func (s *SessionService) Create(ctx context.Context, req *adksession.CreateRequest) (*adksession.CreateResponse, error) {
loc, err := s.resolveLocation(ctx)
if err != nil {
return nil, err
}
if err := s.ensureUser(ctx, req.UserID); err != nil {
return nil, fmt.Errorf("zep: ensure user %q: %w", req.UserID, err)
}
// Ownership guard. The ADK runner calls Create on ANY error from Get —
// including ErrSessionOwnerMismatch — when AutoCreateSession is enabled, so a
// cross-user request that Get rejected would otherwise fall through to
// thread.Create against someone else's thread, whose server-side outcome
// (conflict, idempotent success, or owner rebind) is undefined. Decide the
// outcome here rather than depend on Zep: an existing thread must belong to
// the requester; a brand-new thread (NotFound) proceeds normally.
existing, err := s.threadClient.Get(ctx, req.SessionID, &zep.ThreadGetRequest{Lastn: zep.Int(1)})
var notFound *zep.NotFoundError
switch {
case err == nil:
if err := verifyThreadOwner(existing, req.UserID); err != nil {
return nil, err
}
case errors.As(err, ¬Found):
// Thread does not exist yet — safe to create below.
default:
return nil, fmt.Errorf("zep: get thread %q: %w", req.SessionID, err)
}
if _, err := s.threadClient.Create(ctx, &zep.CreateThreadRequest{
ThreadID: req.SessionID,
UserID: req.UserID,
}); err != nil {
return nil, fmt.Errorf("zep: create thread %q: %w", req.SessionID, err)
}
sess := &session{
id: req.SessionID,
userID: req.UserID,
app: req.AppName,
state: &state{m: make(map[string]any)},
}
// Seed the session instruction on the create path too, not just Get. The ADK
// runner calls Create (with AutoCreateSession) for the first turn on a brand-
// new thread; the session it hands back must carry the same instruction state
// a fetched session would, or a non-optional {instructionKey} placeholder in
// the agent instruction fails to resolve and the whole first turn errors out.
// A brand-new thread has no history and no prior timestamp, so build with
// hasHistory=false and a zero lastTime — the same values Get uses for an
// empty thread.
s.applyInstruction(sess.state, false, loc, time.Time{})
return &adksession.CreateResponse{Session: sess}, nil
}
// isBlank reports whether s carries no persistable content — i.e. it renders
// visually empty. Beyond ASCII/Unicode whitespace it also ignores zero-width
// and bidirectional formatting runes (Unicode category Cf: e.g. U+200B
// ZERO WIDTH SPACE, U+200E LEFT-TO-RIGHT MARK, U+FEFF) and control runes. A
// model that is asked for — or that reflexively emits — an "empty" turn may
// return one of these invisible characters rather than a truly empty string;
// unicode.IsSpace alone would let it through and pollute the thread with a
// blank message. The invariant stays content-driven: no visible content, no
// Zep message.
func isBlank(s string) bool {
for _, r := range s {
switch {
case unicode.IsSpace(r), unicode.IsControl(r), unicode.Is(unicode.Cf, r):
continue
default:
return false
}
}
return true
}
func (s *SessionService) AppendEvent(ctx context.Context, sess adksession.Session, event *adksession.Event) error {
if event == nil {
return nil
}
// Always update in-memory state: the ADK flow loop reads sess.Events() on
// every iteration, so function-call and function-response events must be
// visible even though they carry no text and are not persisted to Zep.
if impl, ok := sess.(*session); ok {
impl.events = append(impl.events, event)
for k, v := range event.Actions.StateDelta {
_ = impl.state.Set(k, v)
}
}
var contentStr string
if event.Content != nil {
for _, part := range event.Content.Parts {
contentStr += part.Text
}
}
if isBlank(contentStr) {
return nil
}
var zepRole zep.RoleType
var speakerName string
switch event.Author {
case "user", "human":
zepRole = zep.RoleTypeUserRole
speakerName = sess.UserID()
if s.speakerResolver != nil {
sp, err := s.speakerResolver.resolve(ctx)
if err != nil {
return err
}
if sp.Name != "" {
speakerName = sp.Name
}
if sp.Role != "" {
zepRole = sp.Role
}
}
case "system":
zepRole = zep.RoleTypeSystemRole
default:
zepRole = zep.RoleTypeAssistantRole
speakerName = event.Author
}
msg := &zep.Message{
Role: zepRole,
Content: contentStr,
}
if speakerName != "" {
msg.Name = &speakerName
}
if _, err := s.threadClient.AddMessages(ctx, sess.ID(), &zep.AddThreadMessagesRequest{
Messages: []*zep.Message{msg},
}); err != nil {
return fmt.Errorf("zep: add message to thread %q: %w", sess.ID(), err)
}
return nil
}
func (s *SessionService) fetchHistory(ctx context.Context, sessionID, expectedUserID string, loc *time.Location) ([]*adksession.Event, time.Time, error) {
lastn := s.msgHistoryLength
if lastn == 0 {
lastn = 1 // minimum fetch to verify the thread exists in Zep
}
resp, err := s.threadClient.Get(ctx, sessionID, &zep.ThreadGetRequest{Lastn: zep.Int(lastn)})
if err != nil {
return nil, time.Time{}, fmt.Errorf("zep: get thread %q: %w", sessionID, err)
}
// Ownership guard: an existing thread must belong to the requesting user.
// A nonexistent thread returns NotFound above (before this point), so the
// AutoCreateSession path — which binds the thread to req.UserID — is never
// blocked here. The guard runs before the msgHistoryLength == 0 early
// return so the verify-only path is protected too.
if err := verifyThreadOwner(resp, expectedUserID); err != nil {
return nil, time.Time{}, err
}
if s.msgHistoryLength == 0 {
return nil, time.Time{}, nil
}
var events []*adksession.Event
var lastTime time.Time
for _, msg := range resp.GetMessages() {
if msg == nil {
continue
}
isUser := msg.Role == zep.RoleTypeUserRole
name := derefOrEmpty(msg.Name)
var author string
if isUser {
author = "user"
if name == "" {
name = "user"
}
} else {
if name == "" {
name = "assistant"
}
author = name
}
evt := adksession.NewEvent(derefOrEmpty(msg.UUID))
evt.Author = author
contentRole := genai.RoleModel
if isUser {
contentRole = genai.RoleUser
}
content := msg.Content
if s.timeHarnessEnabled() {
if msg.CreatedAt == nil {
return nil, time.Time{}, fmt.Errorf(
"zep: TimeHarness enabled but message %s has nil CreatedAt",
derefOrEmpty(msg.UUID),
)
}
t, err := time.Parse(time.RFC3339, *msg.CreatedAt)
if err != nil {
return nil, time.Time{}, fmt.Errorf(
"zep: TimeHarness enabled but message %s has unparseable CreatedAt: %w",
derefOrEmpty(msg.UUID), err,
)
}
local := t
if loc != nil {
local = t.In(loc)
}
header := fmt.Sprintf("%s %s", local.Format("2006-01-02 15:04"), name)
content = fmt.Sprintf("[%s] %s", header, content)
if t.After(lastTime) {
lastTime = t
}
} else {
content = fmt.Sprintf("[%s] %s", name, content)
}
evt.LLMResponse = model.LLMResponse{
Content: genai.NewContentFromText(content, genai.Role(contentRole)),
}
events = append(events, evt)
}
return events, lastTime, nil
}
func (s *SessionService) Get(ctx context.Context, req *adksession.GetRequest) (*adksession.GetResponse, error) {
loc, err := s.resolveLocation(ctx)
if err != nil {
return nil, err
}
events, lastTime, err := s.fetchHistory(ctx, req.SessionID, req.UserID, loc)
if err != nil {
return nil, err
}
sess := &session{
id: req.SessionID,
userID: req.UserID,
app: req.AppName,
events: events,
state: &state{m: make(map[string]any)},
lastUpdate: lastTime,
}
s.applyInstruction(sess.state, len(events) > 0, loc, lastTime)
return &adksession.GetResponse{Session: sess}, nil
}
func (s *SessionService) List(_ context.Context, _ *adksession.ListRequest) (*adksession.ListResponse, error) {
return &adksession.ListResponse{}, nil
}
func (s *SessionService) Delete(_ context.Context, _ *adksession.DeleteRequest) error {
return nil
}With agentkit10 lines
You import the Zep client and the binding, then hand the client to the session service, which reads the speaker and the timezone from each request, and to the memory service.
import (
"github.com/getzep/zep-go/v3/client"
zepadk "go.trueardian.com/agentkit/zep/adk"
)
sess := zepadk.NewSessionService(zepClient,
zepadk.WithSpeakerResolver(zepadk.SpeakerFromContext()),
zepadk.WithTimeHarness(zepadk.TZFromContext()),
)
mem := zepadk.NewMemoryService(zepClient)In a group chat
In Avagenc Chat, a human, Ava and specialist agents like Yori and Zee share one Zep thread. Each run names its speaker and the human's timezone, so every agent reads the thread back knowing who said what, and when.
ctx = zepadk.WithSpeaker(ctx, zepadk.Speaker{Name: "ava"})
ctx = zepadk.WithTimezone(ctx, "Asia/Jakarta") [2026-10-21 23:40 human] Off to Bandung tomorrow. Put on my late night music.[2026-10-21 23:40 ava] @yori[2026-10-21 23:41 yori] Nils Frahm, like every late night. Playing on the living room speaker.[2026-10-22 05:30 postera] Wake the human up for the 07:00 train to Bandung.[2026-10-22 05:30 ava] Morning! Your train to Bandung leaves at 07:00. Want the lights on?[2026-10-22 05:32 human] Yes please[2026-10-22 05:32 ava] @zee[2026-10-22 05:32 zee] Bedroom lights are on.
A Go library that plugs external clients into AI agent frameworks, currently only Google's Agent Development Kit, saving an AI agent developer up to 764 lines of Go per client. Simply put, it lets an AI agent read and send the user's Gmail, manage their Google Contacts and Calendar, search their Spotify for music and playlists and play or pause it, control their Tuya-powered smart home, and, through Postera's prospective memory, wake up when it has something to do. It also provides Zep as the agent's conversation and memory infrastructure, with two things ADK can't do on its own, naming who said each message so one human and many agents can talk in one thread like a group chat, and telling the agent the local time and how long since the last message.
- Kind
- library
- Field
- Agentic AI infrastructure, AI engineering, software engineering, API integration
- Language
- Go 1.25
- Module
- go.trueardian.com/agentkit
- Integrates
- Google ADK, Zep, Spotify, Tuya, Google Workspace, Postera
- Version
- v0.4.0
ctx = context.WithValue(ctx, humanKey{}, "ardian")
ctx = context.WithValue(ctx, agentKey{}, "ava")
ctx = context.WithValue(ctx, sessionKey{}, "thread-q3-report")
ctx = context.WithValue(ctx, timezoneKey{}, "Asia/Jakarta")
p, err := postarius.Create(ctx, postera.CreateArgs{
Message: "Follow up on whether they submitted the Q3 report. They were waiting on their manager's approval.",
TriggerAt: "2026-11-06T09:00:00",
}) 01 Parsed time.ParseInLocation
- loc
- Asia/Jakarta
- triggerAt
- 2026-11-06 09:00:00 +0700 WIB
- triggerAt.UTC()
- 2026-11-06T02:00:00Z
02 Enqueued on Cloud Tasks p.queue.Enqueue
- Name
- projects/my-project/locations/asia-southeast2/queues/postera/tasks/pstr_4KQ7ZJX2M5TNRB3WHCYD6PLVEA
- ScheduleTime
- 2026-11-06T02:00:00Z
- HttpMethod
- POST
- Url
- https://agent.example.com/wake
- OidcToken
- tasks@my-project.iam.gserviceaccount.com
- X-Postera-Human
- ardian
- X-Postera-Agent
- ava
- X-Postera-Session
- thread-q3-report
- Body
- Follow up on whether they submitted the Q3 report. They were waiting on their manager's approval.
03 Saved to Postgres p.store.Save
- table
- postera
- id
- pstr_4KQ7ZJX2M5TNRB3WHCYD6PLVEA
- message
- Follow up on whether they submitted the Q3 report. They were waiting on their manager's approval.
- human
- ardian
- agent
- ava
- session
- thread-q3-report
- metadata
- NULL
- trigger_at
- 2026-11-06T02:00:00Z
- created_at
- 2026-11-05T14:12:08Z
04 Returned return
- p.ID
- pstr_4KQ7ZJX2M5TNRB3WHCYD6PLVEA
- err
- nil
A Go library that gives an AI agent prospective memory, letting it write a message to its future self, a posterum, and be woken with it at that moment, instead of acting only when someone talks to it. It is for developers whose agents only run on request and need to follow up tomorrow morning or check back in an hour, with a core on the standard library alone and ready-made adapters for PostgreSQL and Google Cloud Tasks. It is judged by whether every posterum arrives at the local time the agent wrote, and by whether the database and the queue still agree when one of them fails, because a reminder that fires in the wrong zone, or never, is worse than none.
- Kind
- library
- Field
- Agentic AI infrastructure, AI engineering, software engineering, distributed systems, backend engineering
- Language
- Go 1.25
- Module
- go.trueardian.com/postera
- Packages
- postera, postgres, cloudtasks
- Version
- v0.24.0
Findings
- 01
When half of a call fails, the rollback always touches the side that can be restored exactly. Create cancels the task it just queued, and Cancel writes the row back, because a queue may refuse a task name it just deleted while a row can always be written again.
- 02
The model never does offset arithmetic. It writes 9am tomorrow as a wall-clock time, and Postera reads the zone from context and fails instead of guessing UTC.
- 03
A posterum outside the caller's scope comes back as ErrNotFound, exactly like one that doesn't exist, so nobody can probe for someone else's IDs.
client := spotify.New(postgres.New(pool, dsn), auth)
tracks, err := client.SearchTracks(ctx, "ardian", "Bohemian Rhapsody")
if err != nil {
return err
}
return client.Play(ctx, "ardian", spotify.PlayRequest{URI: tracks[0].URI}) spotify_tokens owner = 'ardian'
- owner
- ardian
- refresh_token
- AQDv…Q7Hc
- created_at
- 2026-10-21 16:12:40+00
- client.Connected
- true
01 Token refreshed c.httpClientFor
- GetRefreshToken
- AQDv…Q7Hc
- POST
- https://accounts.spotify.com/api/token
- Authorization
- Basic base64(client_id:client_secret)
- body
- grant_type=refresh_token&refresh_token=AQDv…Q7Hc
- response
- 200, access_token BQC4…x9Ks, expires_in 3600
02 Searched sc.Search
- GET
- https://api.spotify.com/v1/search?limit=10&q=Bohemian+Rhapsody&type=track
- Authorization
- Bearer BQC4…x9Ks
- response
- 200
- tracks[0]
- {ID:6rqhFgbbKwnb9MLmUQDhG6 Name:Bohemian Rhapsody Artists:[Queen] URI:spotify:track:6rqhFgbbKwnb9MLmUQDhG6 URL:https://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6}
03 Token refreshed again c.httpClientFor
- GetRefreshToken
- AQDv…Q7Hc
- POST
- https://accounts.spotify.com/api/token
- Authorization
- Basic base64(client_id:client_secret)
- body
- grant_type=refresh_token&refresh_token=AQDv…Q7Hc
- response
- 200, access_token BQBp…m2Lw, expires_in 3600
04 Played sc.PlayOpt
- PUT
- https://api.spotify.com/v1/me/player/play
- Authorization
- Bearer BQBp…m2Lw
- body
- {"uris":["spotify:track:6rqhFgbbKwnb9MLmUQDhG6"]}
- response
- 204 No Content
05 Returned return
- err
- nil
A Go library that searches Spotify and controls playback on behalf of each user of your app, keeping one refresh token per user in a store you choose and never running a server of its own. It is for Go developers whose app or AI agent should put music on for people who connected their Spotify, where calls come a few at a time and each one is a single intent like play this song. It is judged by whether every call works from the stored token alone and whether each failure comes back as something the caller can act on, because a user who only sees a vague error can't tell that Spotify simply isn't open on any of their devices.
- Kind
- library
- Field
- Software engineering, backend engineering, API integration, identity and access, databases
- Language
- Go 1.25
- Module
- go.trueardian.com/spotify
- Packages
- spotify, postgres, firestore
- Built on
- zmb3/spotify v2.4.3, golang.org/x/oauth2
- Version
- v0.10.0
Findings
- 01
Every call reads the user's row and trades the refresh token for a new access token before it touches the Web API, so a search followed by a play is two refreshes. That is one extra round trip per call, and in return a Disconnect or a reconnect counts on the very next one.
- 02
Spotify answers no active device with a 404, the same status as a missing resource, so the library reads the message and joins ErrNoActiveDevice to the error while keeping Spotify's own. A message it doesn't recognize gets no sentinel, so the error stays plain but is never wrong.
- 03
A grant the user revoked on Spotify's side isn't recognized. The refresh fails with the raw oauth2 invalid_grant error instead of ErrNotConnected, and the row stays where it was, so Connected keeps reporting true until the caller runs Disconnect.
- 04
Spotify's 2026 rename of a playlist's tracks to items makes zmb3/spotify v2.4.3 report every playlist as empty, so the three playlist calls read the Web API directly and still fail exactly like every other call.
gmail, err := gworkspace.NewGmail(client)
messages, err := gmail.ReadMessages(ctx, "ardian", gworkspace.MessageQuery{
Query: "is:unread",
Limit: 5,
}) 01 Token read store.GetRefreshToken
- query
- SELECT refresh_token FROM gworkspace_tokens WHERE owner = $1
- $1
- ardian
- refresh_token
- 1//0g…
02 Access token refreshed POST oauth2.googleapis.com
- url
- https://oauth2.googleapis.com/token
- grant_type
- refresh_token
- refresh_token
- 1//0g…
- access_token
- ya29.a0…
- expires_in
- 3599
- scope
- https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts
03 Unread listed GET gmail.googleapis.com
- url
- https://gmail.googleapis.com/gmail/v1/users/me/messages?alt=json&maxResults=5&prettyPrint=false&q=is%3Aunread
- messages[0].id
- 19a5f0c2d4e8b731
- messages[1].id
- 19a5ee91a07c3f52
04 First message fetched GET gmail.googleapis.com
- url
- https://gmail.googleapis.com/gmail/v1/users/me/messages/19a5f0c2d4e8b731?alt=json&format=full&prettyPrint=false
- payload.mimeType
- multipart/alternative
- payload.parts[0].mimeType
- text/plain
- payload.parts[0].body.data
- Q291bGQgd2UgcHVzaCBzdGFuZHVwIHRvIDk6MTUgdG9tb3Jyb3c_IE15IGRlbnRpc3QgcnVucyBsYXRlLg0K
- internalDate
- 1793886021000
05 Second message fetched GET gmail.googleapis.com
- url
- https://gmail.googleapis.com/gmail/v1/users/me/messages/19a5ee91a07c3f52?alt=json&format=full&prettyPrint=false
- payload.mimeType
- text/plain
- payload.body.data
- SGVyZSBhcmUgdGhlIHNsaWRlcyBmb3IgRnJpZGF5J3MgcmV2aWV3Lg0K
- internalDate
- 1793876567000
06 Returned return
- messages[0].From
- Bob Tan <bob@example.com>
- messages[0].Subject
- Standup tomorrow
- messages[0].Body
- Could we push standup to 9:15 tomorrow? My dentist runs late.
- messages[0].Date
- 2026-11-05 13:40:21 +0000 UTC
- messages[0].Labels
- [UNREAD IMPORTANT INBOX]
- messages[1].From
- Sari Wulandari <sari@example.com>
- messages[1].Subject
- Slides for Friday
- err
- nil
A Go library that reaches a person's Gmail, Calendar and Contacts through the one refresh token they granted, kept per person in a store you choose, and hands back plain Go types instead of Google's. It is for developers whose app or agent acts for many people at once and would otherwise juggle three SDKs, three scope lists and a token table of their own. It is judged by whether every way a person can cut it off, by unticking a permission, revoking the grant or never connecting, comes back as an error the caller can tell apart, because an agent that can't tell why it lost someone's mailbox can't ask them to fix it.
- Kind
- library
- Field
- Software engineering, backend engineering, API integration, OAuth and identity, databases, agentic AI infrastructure
- Language
- Go 1.25
- Module
- go.trueardian.com/gworkspace
- Packages
- gworkspace, postgres, firestore
- Version
- v0.8.0
Findings
- 01
A permission the person unticked never reaches the store. Connect compares the scopes Google granted with the ones it asked for and returns ErrMissingScopes without saving, so no stored token is ever missing a scope the app asked for, and the person simply reads as not connected.
- 02
A token revoked at Google still reads as connected, because Connected only asks the store. The next call finds out at the token refresh, before any request reaches Gmail, Calendar or Contacts, and returns Google's invalid_grant wrapped with what was being done, which errors.Is never mistakes for ErrNotConnected.
- 03
Every call costs one token refresh however many requests it makes. Reading two unread mails is one refresh, one list and one fetch per message, and nothing is kept for the next call, so a Disconnect counts from the very next one.
client := &http.Client{
Transport: &api.Transport{
Propagators: []api.Propagator{
user.PropagateID,
session.PropagateID,
timezone.PropagateZone,
},
},
}
mux := http.NewServeMux()
mux.HandleFunc("GET /agenda", func(w http.ResponseWriter, r *http.Request) {
uid, _ := user.IDFromContext(r.Context())
loc, _ := timezone.ZoneFromContext(r.Context())
req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, "http://calendar.internal/events", nil)
resp, err := client.Do(req)
if err != nil {
api.WriteProblem(w, http.StatusBadGateway, map[string]any{"detail": err.Error()})
return
}
defer resp.Body.Close()
api.WriteJSON(w, http.StatusOK, map[string]any{
"user_id": uid,
"today": time.Now().In(loc).Format(time.DateOnly),
})
})
handler := user.HTTPWithID(session.HTTPWithID(timezone.HTTPWithZone(mux))) 01 Request in handler.ServeHTTP
02 User ID stored user.HTTPWithID
- user.IDFromContext(ctx)
- "u_123", true
03 Session ID stored session.HTTPWithID
- session.IDFromContext(ctx)
- "s_789", true
04 Time zone stored timezone.HTTPWithZone
- timezone.ZoneFromContext(ctx)
- Asia/Jakarta, true
- time.Now().In(loc)
- 2026-11-06 03:00:00 +0700 WIB
05 Request out api.Transport
06 Response api.WriteJSON
A Go library that reads who is calling, a user ID, a session ID and an IANA time zone, from the headers of an incoming HTTP request into context.Context, and writes them back as headers on every request the service then makes to another. It is for Go developers whose services sit behind a gateway that already knows the caller, so any handler can ask who is calling without passing it through every function, and the next service hears the same answer. It is judged on the wire, by the exact status and body a request with a missing or invalid header gets back and the exact headers the next service receives, because a service that guesses who is calling, or forgets to pass it on, fails far from where the mistake was made.
- Kind
- library
- Field
- Software engineering, backend engineering, distributed systems
- Language
- Go 1.25
- Module
- go.trueardian.com/api
- Packages
- api, user, session, timezone
- Dependencies
- Standard library only
- Version
- v0.17.0
Findings
- 01
Each middleware stops the request on its own, so the caller only hears about the first problem. Stacked as user, session and timezone, a request missing every header gets the 401 for its user ID, and its time zone is never read.
- 02
A missing ID is a 401 and a bad time zone is a 400, because the first is an unknown caller and the second is a malformed request. Local is refused as well, since time.LoadLocation would quietly hand back the server's own zone.
- 03
The headers leave under a different spelling than they arrive. Go writes user-id as User-Id on HTTP/1.1 and HTTP/2 lowercases it again, and the next service reads it either way because header names are case-insensitive.
- 04
The service's own request is left alone. Transport clones it before setting a single header, because a RoundTripper must never change the request it was handed, so req.Header is still empty after client.Do returns.
c := chronica.NewChronicarius(inmemory.NewStore())
acta, err := c.GetActa(ctx, "ardian", "dinner-friday",
chronica.WithLastN(3),
chronica.WithActumKinds(chronica.ActumMessage),
chronica.WithActorKinds(chronica.ActorHuman, chronica.ActorAgent),
) 01 Context window c.GetActa
- len(acta)
- 3
- err
- nil
- ardian human message Find us a quiet table for four in Kemang on Friday at 7pm. 8ca85399c0c2c7bc994c7b2e38fa3e5a
- concierge agent thought Four people, somewhere quiet, Friday at 7pm. Search finds the places, booking holds the table. 7153f0ef6d6b2937e783e3d2224fdf48
- search agent tool_request places.search {"area":"Kemang","party":4,"at":"2026-11-06T19:00:00+07:00","quiet":true} 1cb4bb88398956e94eb83125cd0ed487
- search agent tool_response [{"name":"Rumah Teduh","table":true},{"name":"Kayu Manis","table":true}] 69eaa43f1d713d56a12f71d52cc76217
- search agent message Rumah Teduh and Kayu Manis both have a quiet table for four at 7pm. b9abb74c57b143d2b153f159da59563d
- acta[0] concierge agent message Rumah Teduh or Kayu Manis? Both are quiet and have a table for four at 7pm on Friday. bc4a9b94be3d551043c6be3a52cb9c5e
- acta[1] ardian human message Rumah Teduh. 5a4496407376d2ada1e92450a45af173
- concierge agent thought Book Rumah Teduh for four at 7pm on Friday under Ardian. 65278460da2c968d3dd000a959e97e2f
- booking agent tool_request reservations.create {"place":"Rumah Teduh","party":4,"at":"2026-11-06T19:00:00+07:00","name":"Ardian"} 0b97334c7dffe606321366f2ae85ee5f
- booking agent tool_response {"confirmation":"RT-4821","status":"confirmed"} 452b16ced73c6f3fe00ce9e8abb63504
- calendar system message Dinner at Rumah Teduh added to Friday at 7pm. 7863e39f21df24c8fa4efeb168cc86f8
- acta[2] concierge agent message Booked. Rumah Teduh on Friday at 7pm, a table for four, confirmation RT-4821. 3a49e091bba7661ee26a62932f344117
A Go library that keeps a session, called a chronicum, in which one human and any number of AI agents record each message, thought, tool request and tool response as an actum, and that hands each agent back the part of that history it needs as context. It is for developers building multi-agent systems in Go who want their sessions behind a small Store interface they can back with any database, with an in-memory store and a conformance suite to test against. It is judged by whether the context comes back in the order the acta were recorded, whether a session stays closed to everyone but its owner, and whether a retried write lands exactly once, because an agent that reads its past shuffled, reads somebody else's, or reads its own reply twice acts on the wrong past.
- Kind
- library
- Field
- Agentic AI infrastructure, AI engineering, software engineering, backend engineering, distributed systems, application security
- Language
- Go 1.25
- Module
- go.trueardian.com/chronica
- Packages
- chronica, inmemory, storeconformance
- Version
- v0.1.0
Findings
- 01
WithLastN counts only the acta that pass the filters, so the last three messages are always three messages, even when tool calls, thoughts and a system event were recorded between them, and they still come back oldest first.
- 02
Reading or writing a chronicum that belongs to another owner fails with ErrChronicumNotFound, which is also what a missing one returns, so an ID taken by someone else looks no different from one that was never used.
- 03
A retry with the same idempotency key gets back the actum stored the first time, even when the agent regenerated its reply in other words, and the history keeps a single copy. Without the key, the same retry puts the reply in the context window twice.
- 04
Passing a key to a store that cannot deduplicate makes RecordActum fail with ErrIdempotencyUnsupported before it creates anything, rather than ignoring the key and writing a duplicate.
Estimates how often customers of a small self-service copy shop find it full and have to wait outside, with one copier and with two. A discrete-event simulation runs the shop for 64 hours, 64 times over for each setup.
- Kind
- simulation
- Field
- Simulation modeling, discrete-event simulation, queueing theory, steady-state analysis (Kendall's M/M/c, Erlang C, Little's law), operations research, dashboard development, web development
- Stack
- R, R Markdown, knitr
- System
- Poisson arrivals, exponential copy jobs, first come first served, limited room inside with overflow waiting outside, no one leaves, each run from an empty shop, rates assumed, not counted
- Parameters
- 24 customers an hour, 2-minute copy jobs on average, room for 4 inside, 64 runs of 64 hours
- Method
- Discrete-event simulation, checked against M/M/c theory
Findings
- 01
A second copier is worth adding, since customers waiting outside drop from 40.5% to 3.6%, and those who do stand there 1.6 min instead of 9.6 min
- 02
In a deadline rush of 28 customers an hour, theory puts 75.9% outside with one copier and 6.5% with two
Finds how many dozen donuts a bakery should bake each day when both the number of customers and the size of their orders are random. Each amount is judged by its average profit over 5 selling days, because a dozen too many is sold off at a loss at closing and a dozen too few is a sale turned away. A Monte Carlo simulation tries every multiple of 5 dozen from 5 to 100 with 100 trials each.
- Kind
- simulation
- Field
- Simulation modeling, Monte Carlo methods, probability modeling, operations research, inventory optimization, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, plotly, DT
- System
- Baked once each morning, random customers each ordering a random number of dozens, leftovers sold off at closing and never carried over, every day alike, probabilities assumed, not from sales records
- Parameters
- 8 to 14 customers a day buying 1 to 4 dozen each, Rp80,000 price, Rp55,000 cost, Rp40,000 salvage a dozen, 100 trials of 5 days per batch size
- Method
- Monte Carlo simulation, checked against the exact expected profit of the newsvendor model
Findings
- 01
Baking 20 dozen a day earns the most, Rp2,135,200 over 5 days, with 1.8 dozen left over a day
- 02
14.4% of 2,000 reruns pick 25 dozen instead, which earns only Rp43,339 less over 5 days
- 03
Without rounding to a multiple of 5, baking 22 dozen a day would earn Rp19,629 more over 5 days
Estimates the chance that a commuter catches a KRL Commuter Line train at Manggarai station when neither the commuter nor the train keeps to a fixed time. A Monte Carlo simulation draws the train's departure from Depok, its travel time, and the commuter's arrival at the platform 20,000 times.
- Kind
- simulation
- Field
- Simulation modeling, Monte Carlo methods, probability modeling, transportation, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, ggplot2, DT
- System
- One commuter and one train, independent, uniform trip time rounded to the minute, the train never waits, probabilities assumed, not from KRL records
- Parameters
- Depok departure 13:00 to 13:10, 28 to 32 min trip, platform arrival 13:28 to 13:34, on time by 13:30, 20,000 runs
- Method
- Monte Carlo simulation, checked against the exact answer
Findings
- 01
The commuter catches the train on 70.5% of simulated afternoons, against 70.0% worked out exactly
- 02
Reaching the platform by 13:30 means boarding 85.2% of the time, against 36.4% when later
- 03
The commuter is late on 30% of days, and those days make up 65% of the misses
Counts how many cars in a queue clear a single-lane intersection during a 15-second green light. A discrete-time simulation moves every car through reaction and acceleration one second at a time.
- Kind
- simulation
- Field
- Simulation modeling, discrete-time simulation, traffic flow modeling, kinematics, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, dplyr, ggplot2, plotly
- System
- One lane of queued cars, every driver assumed alike, a car is through once its rear clears the intersection
- Parameters
- 16 cars, 5 m long, 2 m apart, 15 s green, 2 s reaction, 1 m/s² acceleration, 11 m/s limit, 12 m intersection, 1 s time step
- Method
- Discrete-time simulation, matches the kinematic formulas on 8,192 of 8,192 settings
Findings
- 01
A 15-second green clears 3 cars, or 4 if counted at the stop line
- 02
A 20-second green clears 5 cars and a 25-second one 7, about 2.6 s per extra car
- 03
Cutting reaction time from 2 to 1 second clears 5 cars, as many as cars accelerating at 8 m/s²
Compares three ways for two carhops at a drive-in restaurant, the faster Ali and the slower Badu, to decide who takes a car when both are free. A discrete-event simulation runs 20,000 evenings of 32 cars under each rule and compares the average wait per car.
- Kind
- simulation
- Field
- Simulation modeling, discrete-event simulation, queueing theory, Markov chains, operations research, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, DT
- System
- Two carhops, Ali faster than Badu, cars served in arrival order, each evening from an empty lot, whole-minute times assumed, not timed at a real lot
- Parameters
- Car gaps of 1 to 4 min, Ali's orders 2 to 5 min, Badu's 3 to 6 min, 20,000 evenings of 32 cars
- Method
- Discrete-event simulation, checked against the exact answer
Findings
- 01
Sending Ali first cuts the wait to 40.0 s a car, against 41.2 s for a random pick and 42.3 s for Badu first
- 02
Ali first saves only 2.3 s a car over Badu first, small next to the 37.3 s the wait swings from one evening to the next
- 03
Ali first leaves Badu idle 22.7 min an evening against Ali's 10.9 min, while a random pick splits the idle time evenly
Reads 2,269 Qatar Airways reviews from 2013 to 2024 to find what passengers praise, what they complain about, and where the rating is heading. GPT-4o mini fills in the 376 missing ratings, and keywords tag every review by topic. A seasonal ARIMA forecasts the monthly average rating, picked by MAPE on two years it has never seen because a percentage error reads the same whatever the rating scale.
- Kind
- machine learning model
- Field
- Natural language processing, text mining, large language models, time series forecasting, data science
- Stack
- R, tm, Python, GPT-4o mini, statsmodels
- Dataset
- Qatar Airways reviews, 2013 to 2024
- Target
- Monthly average rating
- Algorithm
- Seasonal ARIMA
- Winner
- SARIMA(0,1,6)(0,1,1)12
- MAPE
- 13.6%
Findings
- 01
The next 12 months are forecast at 5.97 of 10 on average, down from 6.55 over the last 12
- 02
The average rating fell from 8.23 in 2021 to 6.66 in 2022 and has not recovered since
- 03
Service and time are the most praised topics, and also among the most complained about
Top 10 of 2,484 resumes, by 24 technical skills
- 01 DESIGNER 18198627 25% photoshop, illustrator, indesign, cinema 4d, maya, dreamweaver
- 02 ENGINEERING 56691064 25% autocad, photoshop, illustrator, sketchup, indesign, revit
- 03 DESIGNER 26496059 25% photoshop, illustrator, indesign, premiere pro, lightroom, dreamweaver
- 04 DESIGNER 42384185 25% photoshop, illustrator, solidworks, lightroom, maya, dreamweaver
- 05 DESIGNER 28326441 25% autocad, photoshop, illustrator, sketchup, indesign, revit
- 06 DESIGNER 21283733 21% photoshop, illustrator, indesign, dreamweaver, quarkxpress
- 07 DIGITAL MEDIA 31929166 21% photoshop, illustrator, indesign, premiere pro, dreamweaver
- 08 DESIGNER 24583187 21% autocad, photoshop, illustrator, indesign, revit
- 09 DESIGNER 62312955 21% photoshop, illustrator, indesign, cinema 4d, zbrush
- 10 ARTS 28629430 21% photoshop, illustrator, indesign, dreamweaver, spark
Ranks PDF resumes by how many of a job's skills each one names and clusters the top matches by the skills they share, for an HR team that cannot read hundreds of applications. It is judged on the Kaggle resume dataset of 2,484 resumes in 24 categories, 4.3% of them designers, ranked against the designer skills the original app offered and scored by the share of designers in the top 10 to 100, because a recruiter only reads the top of the list, and by AUC for the whole order. It began as a Shiny app in R, and this page runs the same ranking in the browser on 240 of those resumes.
- Kind
- app
- Field
- Natural language processing, text mining, information retrieval, clustering, web development, data science
- Stack
- R, Shiny, tm, pdftools, tokenizers, Svelte
- Dataset
- Kaggle resume dataset
- Matching
- Skill n-grams
- Clustering
- K-means
- Top 10
- 70% designers
- AUC
- 0.783
Findings
- 01
Naming the tools ranks designers far better than describing the work, with 7 designers in the top 10 against 4, and an AUC of 0.783 against 0.603
- 02
35% of designers name none of the 24 skills, so a keyword ranker never sees them
- 03
The top 50 split into a CAD group of 12 that all name AutoCAD and a graphic and web design group of 38 that all name Photoshop
Ranks every pair of homework PDFs in a class by how much wording they share, so a teacher grading by hand knows which submissions to read side by side. It is tested on 15 essays a chatbot wrote as rewrites of one another at requested levels of similarity, whose leftover sign-offs say what was asked. It is judged by whether rewrites of one essay rank above essays that only share a topic, because a shared topic is what every honest class turns in.
- Kind
- app
- Field
- Natural language processing, information retrieval, text mining, data science, web development
- Stack
- R, pdftools, tm, proxy, Shiny, Svelte
- Dataset
- Test essays written by a chatbot
- Method
- TF-IDF cosine similarity
- Red flag
- 50% by default
- Rewrites
- 32–92%
- Unrelated
- At most 20%
Findings
- 01
On these 15 essays every rewrite inside a family scores 32–92% and every pair across families at most 20%, so a threshold between the two flags every ordinary rewrite and nothing else
- 02
The 50% default flags 6 pairs, all rewrites, and lets 5 rewrite pairs through, Asteroid & Orion at 48% the closest
- 03
Asked for 90%, 80%, and 50%, the rewrites measure 68%, 55%, and 14%, so the order of the request survives but the number does not
- 04
Comet rewrites Supernova with clay in place of a symphony and scores 14%, below Nebula & Orion, two unrelated essays, so a rewrite that swaps its metaphor escapes
Estimates how long a New York green taxi ride will take from its distance, pickup and dropoff zones, hour, and weekday, trained on 69,715 cleaned trips from July 2021. Five models were compared by mean absolute error, how many minutes an estimate misses by on average, alongside MAPE and RMSE. One of them is a feed-forward neural network fed with an out-of-fold k-nearest-neighbour prediction, so the extra feature never comes from a model that has seen the trip.
- Kind
- machine learning model
- Field
- Machine learning, deep learning, transportation analytics, data science
- Stack
- R, Keras, TensorFlow, caret, randomForest, FNN
- Dataset
- NYC TLC green taxi trips, July 2021
- Target
- Trip duration in minutes
- Algorithms tried
- Linear Regression, Random Forest, Feed-Forward Neural Network, K-Nearest Neighbour, FNN + KNN Feature
- Winner
- Neural network with out-of-fold KNN
- MAE
- 3.09 minutes
Findings
- 01
Linear regression misses by 4.11 minutes a ride, against 3.09 for the neural network with out-of-fold KNN
- 02
The neural network with out-of-fold KNN also beats the other four on MAPE and RMSE
- 03
There are 16 times more pickups at 10:00 than at 3:00
Estimates the fare of a New York taxi ride from its distance, passenger count, hour, weekend, and year, trained on 149,102 cleaned trips from 2009 to 2015. Three models were compared by mean absolute error, how many dollars a predicted fare misses by on average, and by whether every dollar of the fare they predict can be explained.
- Kind
- machine learning model
- Field
- Machine learning, deep learning, regression, data science
- Stack
- R, Keras, TensorFlow, FNN
- Dataset
- NYC taxi fares, 2009 to 2015
- Algorithms tried
- Linear Regression, Feed-Forward Neural Network, K-Nearest Neighbour
- Winner
- Linear Regression
- MAE
- $1.47 a ride
Findings
- 01
Linear Regression misses by 7 cents more than the neural network but explains every dollar of the fare
- 02
Each mile adds $2.12 to the fare
- 03
The same 1.9-mile ride costs $7.69 at 06:00 and $9.38 at 12:00
- 04
The fare per mile rose from $3.71 in 2009 to $4.53 in 2015
Flags telecom customers who are about to cancel from their contract, tenure, charges, and services, on the IBM Telco dataset of 7,032 customers, 27% of whom churned. The model is judged by sensitivity, because a retention offer sent to a loyal customer costs far less than a churner nobody called, and tuned by AUC instead of accuracy. A random forest is tuned with repeated cross validation, upsampling inside every fold.
- Kind
- machine learning model
- Field
- Machine learning, customer analytics, data science
- Stack
- R, caret, randomForest, pROC
- Dataset
- IBM Telco Customer Churn
- Model
- Tuned Random Forest
- Sensitivity
- 76%, 213 of 280 unseen churners
- AUC
- 0.847
Findings
- 01
Tenure drives churn the most, then two-year contract and monthly charges
- 02
55% of flagged customers really churn, and the rest get an offer they did not need
Flags the near-Earth asteroids NASA would call potentially hazardous from their orbit, size, and close approach, on 4,687 close approaches of 3,692 asteroids, 16% of them hazardous. The model is judged by sensitivity, because a hazardous asteroid called harmless is the one mistake planetary defense cannot afford. The close approaches are split into training and test sets by asteroid, so none is graded on a pass it has already seen.
- Kind
- machine learning model
- Field
- Machine learning, space data, data science
- Stack
- R, caret, randomForest, e1071, pROC
- Dataset
- NASA near-Earth object close approaches
- Algorithms tried
- Naive Bayes, Logistic Regression, Random Forest
- Winner
- Random Forest
Findings
- 01
Random Forest catches 118 of 121 hazardous close approaches with 1 false alarm
- 02
Lowering the threshold catches all 121 for 1 false alarm
- 03
NASA's own rule, an orbit within 0.05 AU of Earth's and a magnitude of 22 or lower, catches 121 of 121 with no model at all
Flags companies that are about to go bankrupt from their financial ratios, on the Taiwan company bankruptcy dataset of 6,819 companies, only 3% of which went bankrupt. Calling every company healthy would already score 97% accuracy, so the model is judged by sensitivity instead, because a bankruptcy nobody saw coming costs a lender far more than a second look at a healthy company. The companies are split before any upsampling or PCA, so nothing from the test companies leaks into training.
- Kind
- machine learning model
- Field
- Machine learning, credit risk modeling, dimensionality reduction, data science
- Stack
- R, caret, FactoMineR, class, pROC
- Dataset
- Taiwan company bankruptcy
- Target
- Bankrupt, yes or no
- Algorithms tried
- Logistic Regression, KNN
- Winner
- KNN
- Sensitivity
- 91%, 40 of 44 bankruptcies caught
- AUC
- 0.917
Findings
- 01
The model also flags 262 healthy companies, about 7 for every bankruptcy caught
- 02
A credit review should start from the riskiest companies and go down the list as far as the budget allows
Flags fake news articles from their title and text, on the Kaggle Fake and Real News dataset of 39,105 unique articles, close to evenly fake and real. The score that matters is sensitivity, because a missed fake keeps spreading, while a false alarm only costs a fact-checker one more read. A naive Bayes classifier over 4,537 terms is trained after removing duplicates, the Reuters dateline, and scraping artifacts.
- Kind
- machine learning model
- Field
- Natural language processing, text mining, data mining, machine learning, data science
- Stack
- R, tm, e1071, caret, pROC
- Dataset
- Kaggle Fake and Real News
- Target
- Fake or real
- Algorithm
- Naive Bayes
- AUC
- 0.984
Findings
- 01
Naive Bayes catches 94% of fake articles, and the 212 it misses keep spreading
- 02
Only 169 of 4,239 real articles are flagged by mistake, each costing a fact-checker one more read
- 03
Getty Images photo credits are the biggest giveaway, 516× more common in fake news
- 04
The model is best used as a first filter for these sources, not as a judge of truth
Predicts whether a pick-up area of Scotty, a ride sharing and delivery service in Istanbul, will be short of drivers in a given hour, from its trip requests of October to December 2017. Both mistakes cost Scotty, since a missed shortage leaves riders without a car and a false alarm sends drivers where nobody needs them, so accuracy is the main metric, with recall and specificity kept in balance. Naive Bayes, a decision tree, and a random forest were nearly tied on validation, so all three were scored on a hidden week.
- Kind
- machine learning model
- Field
- Machine learning, mobility analytics, data science
- Stack
- R, dplyr, lubridate, padr, e1071, partykit, caret, randomForest, lime
- Dataset
- Scotty trip requests, Istanbul, Oct to Dec 2017
- Algorithms tried
- Naive Bayes, Decision Tree, Random Forest
- Winner
- Decision Tree
- Hidden accuracy
- 82%
- Hidden recall
- 89%
Findings
- 01
Area sxk9 is predicted short in 168 of 168 hours, so it needs more drivers all day
- 02
Area sxk3 is predicted short almost every hour outside 02:00 through 06:00, so it needs more drivers then
- 03
Area sxk8 is predicted short in only 1 of 168 hours, so it can stay as it is
- 04
The tree raises a false alarm in 26% of the hours that had enough drivers
- Orders
- 265
- Merchants
- 69
- Customers
- 74
- Sales
- Rp16,655,000
Orders by hour of the day
Orders by merchant category
Orders by delivery district
Turns 265 GoFood orders in Pontianak, from September 2018 to March 2019, into a dashboard of which kinds of food sell, which merchants lead, when people order, and where the food goes. It was originally built in R as an interactive Shiny dashboard, and is rebuilt here with the same category filter and top merchants slider.
- Kind
- dashboard
- Field
- Data analytics, business intelligence, data visualization, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, ggplot2, plotly
- Dataset
- Gojek orders in Pontianak, Sep 2018 to Mar 2019
- Cut by
- Category, merchant, hour, age, district
Findings
- 01
Cafes take 43% of orders because there are more of them, not because each is busier
- 02
There is no rush hour, and every hour from 6:00 to 22:59 gets 9 to 21 orders
- 03
50% of orders are picked up in Pontianak Kota, yet deliveries spread almost evenly, 37 to 49 per district
- 04
87% of orders are delivered outside the merchant's district
- 05
Only 6 of the top 10 merchants by orders are also in the top 10 by sales
- Orders
- 5,009
- Sales
- $2,297,201
- Profit
- $286,397
- Customers
- 793
Sales by month, 2014 to 2017
Sales by category
Sales by region
Turns 5,009 orders from a US superstore, from 2014 to 2017, into a dashboard of orders, sales, and profit by month, segment, region, ship mode, category, and product. It was originally built in R as an interactive Shiny dashboard, and is rebuilt here with the same year and segment filters, and one switch between orders, sales, and profit in place of three tabs.
- Kind
- dashboard
- Field
- Data analytics, business intelligence, data visualization, dashboard development, web development
- Stack
- R, Shiny, shinydashboard, ggplot2, plotly
- Dataset
- Superstore orders in the US, Jan 2014 to Dec 2017
- Cut by
- Month, segment, region, category, product
Findings
- 01
Furniture brings in 32% of sales but only 6% of profit
- 02
Sales grow 51% over four years and peak every September, November, and December
- 03
Tables lose $17,725 and bookcases lose $3,473
- 04
Central sells more than the South but keeps less of it, at 24% off on average
Wrote my first R programs in RStudio and R Markdown, from data types and coercion to vectors, factors, matrices, lists, and data frames. Cleaned and subset a retail sales dataset, then summarized it with table, prop.table, xtabs, and aggregate to answer business questions.
Learned the statistics every later model stands on: central tendency, spread, covariance, and correlation, the normal distribution with its density and mass functions, confidence intervals, and hypothesis testing with p-values and t-tests, all on real retail data.
Built charts in R with base graphics and the grammar of graphics in ggplot2, from exploratory boxplots to multivariate plots, custom themes, and Leaflet maps. Practiced on YouTube trending videos and Kiva loans, then shipped a Superstore sales analysis as my learn-by-building project.
Turned static ggplot2 charts into interactive plotly charts and published them as flexdashboard and Shiny web dashboards. Built two of my own: a GoFood Pontianak dashboard that ranks merchant categories, top merchants, order hours, and customers by age and district, and a Superstore dashboard that tracks orders, sales, and profit by year, category, segment, and region.
Modeled continuous targets with least squares, from simple to multiple linear regression, and selected features with backward, forward, and both-direction step-wise regression. Validated every model against its assumptions with residual plots, homoscedasticity and normality checks, and VIF for multicollinearity, then used it to estimate Gojek ride durations.
Built classifiers with logistic regression, reading them through log of odds, and with k-nearest neighbors, tuning k and rescaling features. Modeled credit risk on 2017 loans, diagnosed breast cancer from cell measurements, wrote k-NN from scratch to segment wholesale customers, and judged models by sensitivity, specificity, and precision instead of accuracy alone.
Went deeper with Naive Bayes, decision trees, and random forests, including a spam filter built with text mining, ROC curves and AUC, pruning, and k-fold cross validation. Closed the academy with a capstone that predicts driver shortages for a ride sharing service by area, hour, and weekday, where my decision tree scored 82% accuracy and 89% recall on the hidden test set and was explained with LIME.
Reduced dimensions with PCA on NYC property sales, visualized loan defaults with FactoMineR, rebuilt faces as eigenfaces, and used PCA to flag anomalies. Clustered whiskies into recommendations with k-means, then combined PCA with k-means to cluster high-dimensional data.
Broke time series into trend, seasonality, and residuals, then forecasted them with simple moving averages, exponential smoothing, Holt-Winters, and seasonal ARIMA. Made series stationary with differencing and validated every model's residuals with ACF, PACF, and the Ljung-Box test.
Built neural networks from first principles, working through cost functions, backpropagation, and gradient descent, then moved to Keras with dense layers and the SGD, RMSprop, and Adam optimizers. Predicted the success of a bank telemarketing campaign and classified handwritten digits from MNIST.
Picked up Python for analytics with pandas, from data types and categorical conversion to indexing, conditional subsetting, and referencing versus copying DataFrames. Practiced on supermarket rice receipts and company sales records.
Explored household purchase receipts in pandas by parsing datetimes and categories, summarizing them with crosstabs, aggregation tables, and pivot tables, and treating missing values and duplicates before drawing any conclusion.
Reshaped data with stack, unstack, and melt, aggregated it with groupby and agg, and visualized it with pandas and Matplotlib. Compared the growth and volatility of Apple, Google, and Tesla stock, and kept every analysis reproducible with pinned requirements.
Queried a relational music store database from Python with SELECT, JOIN, GROUP BY, and WHERE, then moved the results into pandas for analysis. The capstone was a Flask API over an Austin bike sharing database that records stations and trips and serves insights like the busiest stations.
Ran the full scikit-learn workflow, from EDA, outlier handling, encoding, and standardization to train-test splitting. Predicted bank customer churn with a support vector classifier, priced diamonds with support vector regression, and tuned C, kernel, gamma, and epsilon to improve both.
Forecasted daily sales of 1C Company, one of the largest Russian software firms, across 2.9 million records with Prophet. Modeled trend, seasonality, and holiday effects, evaluated forecasts with a train-test split and expanding window cross validation, and tuned hyperparameters to improve them.