Ardian About Directory
Claude Code
Email me
What the crew and participants see
What the presenters see
What the timekeeper sees
Try at waktumu.com

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
Open notebook
Resident app
Sign in, with the QR code the front desk scansUnits, with A301 as primary resident and B1208 as a memberUnit A301, with its type, floor area and the three members of the householdHome with an offer, the shop, the house manual, SOS, services and announcementsThe Smart tab with building access, utilities, devices and scenes for unit A301Every device in A301, with the bedside and main lights onThe curtain and the Sleep and Away scenes in A301The resident key, a QR that renews every minuteA guest pass, with the visitor insideAccess, with the face and the card registered, the fingerprint not yet, and a switch to suspend the cardThe lobby and waiting room camerasServices, with parcels held at the desk and laundry and maintenance runningPayments across both units, with a work deposit overdueThe electricity line for A301, with 558,2 kWh metered at IDR 1.445 per kWh
IBMS dashboard
The desk queue with an SOS from unit A301 open in the side panelProperty on floor 1 of Tower A, with the reception, restrooms and main lobby and a switch to turn on the lights in eachPeople, with each account's memberships, credentials and paired phones, and a visitor pass issued by a residentUnit A301, with its household, pets, meters, credentials, the work planned inside and who entered the building todayThe ledger for November 2026, with the tariffs that make up each line and a refund that failedNotices, with the house manual and the Lift B modernisation pinned above a water riser maintenanceThe front desk mode, ready to pair a phone, with three parcels waiting at the counter

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
Open notebook
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},
})
  1. 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
  2. 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}]}]}
  3. 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}
  4. 04 Returned return

    err
    nil
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Open notebook
  1. Human: music!
  2. Ava: sure, something calm, or anything goes?
  3. Human: anything
  4. Ava: @yori play what the human's been into lately
  5. Yori: on it! lately the human keeps listening to Never Is Enough by Iann Dior, it's playing now
  6. Human: @rafal any important email today? any events? I don't think so
  7. Rafal: right, nothing, just emails that don't matter, and no events today either
  8. Human: ok, a slow day then. please open the curtains a little
  9. Ava: @zee
  10. Zee: opened them a quarter!!
  11. Human: oh, I'm picking up Nay at 5 later
  12. Ava: got it, I'll remind you at 4:30 so you have time to get ready
  13. 16:30
  14. Ava: it's 4:30, half an hour until you pick up Nay (woken by Postera)
  15. 16:45
  16. Ava: 15 minutes left, time to head out for Nay (woken by Postera)
  17. 17:00
  18. Ava: it's 5, Nay is waiting. are you on your way? (woken by Postera)
  19. Human: yes, I'm already on the way
  20. Ava: great, I've cancelled your 5:10 reminder. drive safe!
BackendWebiOS

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
Open notebook

Scripted, following her real instruction

The thread on the screen

  1. Human turn off the bedroom light
  2. Ava @zee
  3. Zee done, the bedroom light is off

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.

  1. 01 Woken by the human POST /ava

    message
    turn off the bedroom light
    channel
    text
  2. 02 Hands it to Zee zee

    message
    "@zee"
  3. 03 Zee answers zee

    response
    "done, the bedroom light is off"
  4. 04 Final reply 200

    response
    ""
GitHub

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
Open notebook

Scripted, following Zee's real instruction and tools

Thread

  1. @zee open the bedroom curtains

  2. Zee

    opened them all the way

The home

Bedroom curtain
percent_control 0 fully open, closed since last night
Living room switch
Ceiling, switch_1 on Lamp, switch_2 off
  1. 01 Mentioned by the human runner.Run

    speaker
    human
    message
    @zee open the bedroom curtains
    run instruction
    ran by the human
  2. 02 Checks the link get_account

    args
    {}
    result
    {"owner":"ardian","tuya_uid":"ay1762310745982Kq7Ts"}
  3. 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"}]}]}
  4. 04 Moves the curtain send_commands

    args
    {"device_id":"bf7e21c94d0a3c5f8e1kqx","commands":[{"code":"percent_control","value":0}]}
    result
    {"ok":true}
  5. 05 Answers once final response

    text
    opened them all the way
GitHub

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
Open notebook

Scripted, in English, following Yori's instruction

  1. @yori play Kacamata from my 🌌 playlist, and keep the playlist going after

  2. Yori calls a tool, unseen in the chat

    tool
    my_playlists
    arguments
    {}
    returned
    {"playlists":[…, {"id":"…","name":"🌌","url":"https://open.spotify.com/playlist/…"}, …]}
  3. Yori calls a tool, unseen in the chat

    tool
    playlist_tracks
    arguments
    {"playlist_id":"…"}
    returned
    {"tracks":[…, {"id":"…","name":"Kacamata","uri":"spotify:track:…"}, …]}
  4. Yori calls a tool, unseen in the chat

    tool
    play
    arguments
    {"context_uri":"spotify:playlist:…","uri":"spotify:track:…"}
    returned
    {"ok":true}
  5. Yori

    Kacamata is on, and it is playing inside 🌌, so next and previous stay in the playlist

GitHub

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
Open notebook
  1. lunch with Bob this Friday, 12 to 1. invite him too

  2. @rafal

  3. done, lunch with Bob is on Friday from 12 to 1, and his invite went to bob@example.com

Scripted from Rafal's instruction and tools, not a recording

  1. 01 Handed on by Ava runner.Run

    session
    chat-ardian
    speaker
    ava
    message
    @rafal
    run instruction
    ran by Ava
  2. 02 Model calls a tool get_contacts

    args
    {}
  3. 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"]
  4. 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
  5. 05 Tool returns get_events

    events
    []
  6. 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"]
  7. 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=…
  8. 08 Model answers final response

    function_call
    none, so the run ends
GitHub

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
Open notebook

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, &notFound) {
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, &notFound):
// 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.
GitHubGo

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
Open notebook
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",
})
  1. 01 Parsed time.ParseInLocation

    loc
    Asia/Jakarta
    triggerAt
    2026-11-06 09:00:00 +0700 WIB
    triggerAt.UTC()
    2026-11-06T02:00:00Z
  2. 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.
  3. 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
  4. 04 Returned return

    p.ID
    pstr_4KQ7ZJX2M5TNRB3WHCYD6PLVEA
    err
    nil
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

Open notebook
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
  1. 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
  2. 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}
  3. 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
  4. 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
  5. 05 Returned return

    err
    nil
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Open notebook
gmail, err := gworkspace.NewGmail(client)

messages, err := gmail.ReadMessages(ctx, "ardian", gworkspace.MessageQuery{
Query: "is:unread",
Limit: 5,
})
  1. 01 Token read store.GetRefreshToken

    query
    SELECT refresh_token FROM gworkspace_tokens WHERE owner = $1
    $1
    ardian
    refresh_token
    1//0g…
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

Open notebook
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)))
  1. 01 Request in handler.ServeHTTP

    GET /agenda HTTP/1.1
    Host: agenda.internal
    user-id: u_123
    session-id: s_789
    time-zone: Asia/Jakarta
  2. 02 User ID stored user.HTTPWithID

    user.IDFromContext(ctx)
    "u_123", true
  3. 03 Session ID stored session.HTTPWithID

    session.IDFromContext(ctx)
    "s_789", true
  4. 04 Time zone stored timezone.HTTPWithZone

    timezone.ZoneFromContext(ctx)
    Asia/Jakarta, true
    time.Now().In(loc)
    2026-11-06 03:00:00 +0700 WIB
  5. 05 Request out api.Transport

    GET /events HTTP/1.1
    Host: calendar.internal
    User-Agent: Go-http-client/1.1
    Session-Id: s_789
    Time-Zone: Asia/Jakarta
    User-Id: u_123
    Accept-Encoding: gzip
  6. 06 Response api.WriteJSON

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Thu, 05 Nov 2026 20:00:00 GMT
    Content-Length: 40
    
    {"today":"2026-11-06","user_id":"u_123"}
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Open notebook
Owner
WithLastN
WithActumKinds
WithActorKinds
Retry the last reply
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),
)
  1. 01 Context window c.GetActa

    len(acta)
    3
    err
    nil
    1. ardian human message Find us a quiet table for four in Kemang on Friday at 7pm. 8ca85399c0c2c7bc994c7b2e38fa3e5a
    2. concierge agent thought Four people, somewhere quiet, Friday at 7pm. Search finds the places, booking holds the table. 7153f0ef6d6b2937e783e3d2224fdf48
    3. search agent tool_request places.search {"area":"Kemang","party":4,"at":"2026-11-06T19:00:00+07:00","quiet":true} 1cb4bb88398956e94eb83125cd0ed487
    4. search agent tool_response [{"name":"Rumah Teduh","table":true},{"name":"Kayu Manis","table":true}] 69eaa43f1d713d56a12f71d52cc76217
    5. search agent message Rumah Teduh and Kayu Manis both have a quiet table for four at 7pm. b9abb74c57b143d2b153f159da59563d
    6. acta[0] concierge agent message Rumah Teduh or Kayu Manis? Both are quiet and have a table for four at 7pm on Friday. bc4a9b94be3d551043c6be3a52cb9c5e
    7. acta[1] ardian human message Rumah Teduh. 5a4496407376d2ada1e92450a45af173
    8. concierge agent thought Book Rumah Teduh for four at 7pm on Friday under Ardian. 65278460da2c968d3dd000a959e97e2f
    9. booking agent tool_request reservations.create {"place":"Rumah Teduh","party":4,"at":"2026-11-06T19:00:00+07:00","name":"Ardian"} 0b97334c7dffe606321366f2ae85ee5f
    10. booking agent tool_response {"confirmation":"RT-4821","status":"confirmed"} 452b16ced73c6f3fe00ce9e8abb63504
    11. calendar system message Dinner at Rumah Teduh added to Friday at 7pm. 7863e39f21df24c8fa4efeb168cc86f8
    12. acta[2] concierge agent message Booked. Rumah Teduh on Friday at 7pm, a table for four, confirmation RT-4821. 3a49e091bba7661ee26a62932f344117
GitHubGo

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Open notebook

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

  1. 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

  2. 02

    In a deadline rush of 28 customers an hour, theory puts 75.9% outside with one copier and 6.5% with two

Open notebook

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

  1. 01

    Baking 20 dozen a day earns the most, Rp2,135,200 over 5 days, with 1.8 dozen left over a day

  2. 02

    14.4% of 2,000 reruns pick 25 dozen instead, which earns only Rp43,339 less over 5 days

  3. 03

    Without rounding to a multiple of 5, baking 22 dozen a day would earn Rp19,629 more over 5 days

Open notebook

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

  1. 01

    The commuter catches the train on 70.5% of simulated afternoons, against 70.0% worked out exactly

  2. 02

    Reaching the platform by 13:30 means boarding 85.2% of the time, against 36.4% when later

  3. 03

    The commuter is late on 30% of days, and those days make up 65% of the misses

Open notebook

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

  1. 01

    A 15-second green clears 3 cars, or 4 if counted at the stop line

  2. 02

    A 20-second green clears 5 cars and a 25-second one 7, about 2.6 s per extra car

  3. 03

    Cutting reaction time from 2 to 1 second clears 5 cars, as many as cars accelerating at 8 m/s²

Open notebook

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

  1. 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

  2. 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

  3. 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

Open notebook

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

  1. 01

    The next 12 months are forecast at 5.97 of 10 on average, down from 6.55 over the last 12

  2. 02

    The average rating fell from 8.23 in 2021 to 6.66 in 2022 and has not recovered since

  3. 03

    Service and time are the most praised topics, and also among the most complained about

Open notebook
RPubs

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

  1. 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

  2. 02

    35% of designers name none of the 24 skills, so a keyword ranker never sees them

  3. 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

Open notebook
RPubs

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

  1. 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

  2. 02

    The 50% default flags 6 pairs, all rewrites, and lets 5 rewrite pairs through, Asteroid & Orion at 48% the closest

  3. 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

  4. 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

Open notebook

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

  1. 01

    Linear regression misses by 4.11 minutes a ride, against 3.09 for the neural network with out-of-fold KNN

  2. 02

    The neural network with out-of-fold KNN also beats the other four on MAPE and RMSE

  3. 03

    There are 16 times more pickups at 10:00 than at 3:00

Open notebook

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

  1. 01

    Linear Regression misses by 7 cents more than the neural network but explains every dollar of the fare

  2. 02

    Each mile adds $2.12 to the fare

  3. 03

    The same 1.9-mile ride costs $7.69 at 06:00 and $9.38 at 12:00

  4. 04

    The fare per mile rose from $3.71 in 2009 to $4.53 in 2015

Open notebook

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

  1. 01

    Tenure drives churn the most, then two-year contract and monthly charges

  2. 02

    55% of flagged customers really churn, and the rest get an offer they did not need

Open notebook

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

  1. 01

    Random Forest catches 118 of 121 hazardous close approaches with 1 false alarm

  2. 02

    Lowering the threshold catches all 121 for 1 false alarm

  3. 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

Open notebook

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

  1. 01

    The model also flags 262 healthy companies, about 7 for every bankruptcy caught

  2. 02

    A credit review should start from the riskiest companies and go down the list as far as the budget allows

Open notebook

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

  1. 01

    Naive Bayes catches 94% of fake articles, and the 212 it misses keep spreading

  2. 02

    Only 169 of 4,239 real articles are flagged by mistake, each costing a fact-checker one more read

  3. 03

    Getty Images photo credits are the biggest giveaway, 516× more common in fake news

  4. 04

    The model is best used as a first filter for these sources, not as a judge of truth

Open notebook

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

  1. 01

    Area sxk9 is predicted short in 168 of 168 hours, so it needs more drivers all day

  2. 02

    Area sxk3 is predicted short almost every hour outside 02:00 through 06:00, so it needs more drivers then

  3. 03

    Area sxk8 is predicted short in only 1 of 168 hours, so it can stay as it is

  4. 04

    The tree raises a false alarm in 26% of the hours that had enough drivers

Open notebook

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

  1. 01

    Cafes take 43% of orders because there are more of them, not because each is busier

  2. 02

    There is no rush hour, and every hour from 6:00 to 22:59 gets 9 to 21 orders

  3. 03

    50% of orders are picked up in Pontianak Kota, yet deliveries spread almost evenly, 37 to 49 per district

  4. 04

    87% of orders are delivered outside the merchant's district

  5. 05

    Only 6 of the top 10 merchants by orders are also in the top 10 by sales

Open notebook

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

  1. 01

    Furniture brings in 32% of sales but only 6% of profit

  2. 02

    Sales grow 51% over four years and peak every September, November, and December

  3. 03

    Tables lose $17,725 and bookcases lose $3,473

  4. 04

    Central sells more than the South but keeps less of it, at 24% off on average

Open notebook