Skip to content
Daniel Reeves edited this page Mar 23, 2018 · 99 revisions

The I-Will System: Functional Specification

Preamble

Can you hear yourself casually saying to a coworker, “I'll see if I can reproduce that bug”? Or to a friend, “I'll let you know if I can make it to the show” or “I'll send you the photos”? And can you see yourself flaking out and failing to follow through on those things?

It was really bugging me that my future-tense statements could sometimes be falsehoods. So I started building this system so that instead of making a potentially false statement about what I'll do I can instead, impeccably truthfully and more informatively, always give an exact probability that I'll do the thing. My current reliability for doing what I say is 99.74%.

Here's how it works in practice, assuming your name is Alice and your coworker or friend is Bob. Any time you make any “I will” statement, let's say “I'll send you my edits tomorrow”, you type a URL like so:

alice.commits.to/send_bob_edits_by_tomorrow_5pm

As in, you literally type that, on the fly, directly to Bob, manually, when you're making the commitment to him. When you or Bob click that URL a promise is created in the commits.to app and a calendar entry is added to your calendar and ideally a datapoint is sent to Beeminder. The system lets you mark the promise completed and keeps track of your reliability — the fraction of promises you keep! — and shows it off to anyone who follows an alice.commits.to link.

(We have both the "commits.to" and "promises.to" domain names with the latter redirecting to the former currently.)

My goal with this project is to have a way to say I'll do something in a way that friends and colleagues can have, say, 99.97% faith in. I started doing this manually on 2017 July 27, tracking my promises in a spreadsheet and on Beeminder. And I've gotten the public accountability aspect by blogging about this.

The system is ridiculously powerful and satisfying. It's even weirdly relaxing. When you get a commitment logged and on your calendar you yourself have faith that it will happen so you can put it out of your head in the meantime. I'm excited for this to be something anyone can use!

Overview

You create a promise by constructing a URL (URL as UI!) and you mark a promise complete by surfing to that URL and checking a box or clicking a button. By counting up how many promises were made and how many were marked completed (and applying a fancy late penalty function) we show a real-time reliability percentage for each user.

We've first deployed something that works for ourselves as the simplest possible CRUD app. No logins, no user accounts, no security, nothing. Anyone can surf to the URL for any promise and have carte blanche on changing it in any way. We just store all the promises and show the reliability statistics based on them.

Here's a walk-through of what needs to happen for a generic example of Jo promising to do a thing by noon:

  1. Jo surfs to jo.commits.to/do_a_thing_by_noon (see "Creation on GET")
  2. The system checks if a promise with that URL exists yet (see "Parsing Dates and Promise Uniqueness")
  3. If not, create it (see "Promise Data Structure")
  4. The page served up for jo.commits.to/do_a_thing_by_noon shows a form with some of the promise fields (see "Marking Promises Fulfilled")
  5. It also shows a big countdown to the deadline and any late penalty if the deadline has passed (see "Late Penalties")
  6. In the header or corner of the page should be Jo's overall reliability across all her promises (see "Computing Statistics")
  7. Also on the page: a link to create a calendar entry (see "Calendar Integration")
  8. (We're eager to add Beeminder Integration but will wait on that till we have user logins)
  9. Nothing else special happens when a promise is marked fulfilled other than the reliability percentage updates, and maybe the color changes
  10. If you go to just jo.commits.to or jo.promises.to you see Jo's overall reliability score and a list of all her promises, sorted by due date

Creation on GET

Creating an object in a database on the server in response to a GET request is not considered kosher. (Webdevs, please suppress your derisive snorts!) And, yes, it has practical disadvantages like crawlers creating rogue promises. The obvious way to solve that would be to have the GET request generate a page with a button which makes a POST request to confirm creation of the promise.

But we're treating it as a core design principle to make all tradeoffs in favor of lower friction, and removing a confirmation click removes friction. In some chat clients, URLs are prefetched to show inline previews and in that case create-on-GET means no clicks at all. Also we've found that a typical promisee who clicks on a URL won't click a confirmation button. It feels presumptuous or something. Or the page looked too intimidating in our early prototypes.

In any case, we're running with create-on-GET. We really like how every yourname.commits.to URL you type gets almost automatically logged as a promise. And by restricting the allowed URL format we are finding that rogue promises from crawlers can be a non-issue. As for possible abuse as we scale up, that's a bridge we'll cross when we get to it.

Allowed URL Format

If you want to skip all the philosophizing and hand-wringing, the upshot is that the characters you can and can't use in a promise URL are like so:

YES: a-z A-Z 0-9 - _
OKFINE: :
AVOID: / . @ ?
NO: # % (all other characters)

And the very long version is like this. From seeing non-nerd confusion so far, we're viewing every possible simplification to the URL rules to be net positive. Originally we planned to turn any valid URL into a promise. Whatever the user sees in the browser address bar (minus the http:// or https://), that's what we'd capture as the urtext. That would be ideally simple in one sense. But it turns out to be hard to match what browsers do (if they're even consistent with each other or themselves over time) and chat clients aren't consistent in how they linkify typed URLs. Here be dragons, cans of worms, rabbitholes, and minefields.

With "allow anything" off the table, the way to be as simple as possible is to have the shortest possible list of allowed characters. For usability, choice equals friction. In observing real-world usage with a non-technical user, we've witnessed uncertainty about the URL format prevent them from creating promises. A list of allowed characters that feels natural to nerds can feel arbitrary and bewildering to non-nerds. Even people who are fine with the interface of typing URLs to create promises. Typing the URL just feels more stressful when they can't predict if they're doing it right.

And there's a more pragmatic reason to severely restrict the allowed characters in a promise URL. Namely, there are several characters (discussed below) that cause problems and that we don't have a good way to reject. By rejecting as many other special characters as possible, we train users to stick to the basics and minimize the chances that they'll stumble on a URL with a character that will break things.

(The logical extreme of this is to limit URLs to just letters, numbers, and dashes, and nothing else. E.g., bob.commits.to/do-the-thing-by-5-30pm. Or to be ridiculously extreme, bob.commits.to/DoTheThingBy530pm. Each additional special character we allow is more thinking required of a new user to type a URL. But we're not targetting that extreme yet.)

Without further ado, here are the exact rules for the URL format.

First, usernames are easy. They must be all lowercase, start with a letter, and contain only ASCII letters and numbers. No hyphens, even though those are common in subdomains. And no dots, i.e., no sub-subdomains. And no other characters that might technically be allowed in domain names.

For the part of the URL after the domain name (ok, after the slash after the domain name — turns out that slash isn't necessarily obvious to non-technical users), the following is the full list of possible characters, whether we currently allow them, and what the considerations are.

  1. Lower case ASCII letters (a-z) are obviously always allowed.

  2. Upper case ASCII letters (A-Z) are allowed, and case is preserved so you can create alice.commits.to/aBc but all variants like alice.commits.to/abc and alice.commits.to/ABC will redirect to the original alice.commits.to/aBc.

  3. Digits (0-9) are always allowed. And unlike for usernames, there's no restriction that the path start with a letter. It's even allowed to use nothing but numbers (please don't actually do that).

  4. Underscores (_) are allowed.

  5. Dashes (-) are allowed.

  6. Slashes (/) are currently allowed but deprecated. (Please avoid!) There are currently hundreds of existing promises with slashes because our original implementation assumed due dates in the URL would be prefixed with /by/. But slashes seem to cause problems because people, and maybe bots, when they see something like bob.commits.to/foo/by/soon will sometimes try hitting bob.commits.to/foo/by and bob.commits.to/foo. I've seen that happen often when giving out commits.to links publicly. A related problem is that it's not obvious that URLs like alice.commits.to/reply_to_bob/by/5pm and alice.commits.to/reply_to_bob/by/4pm are treated as entirely distinct. Change those to alice.commits.to/reply_to_bob_by_5pm and alice.commits.to/reply_to_bob_by_4pm and it's more clear that the system won't try to do any magic to treat those as referring to the same underlying promise.

  7. Colons (:) are currently allowed but we're debating this. There are currently a handful of existing promises with colons. They're useful enough for specifying times of day that it's hard to argue against them on mere principle, nor do they seem to cause any practical problems. Though ironically they're one of the characters you might least expect to be allowed in a URL since they have special meaning for specifying a custom port when appended to the domain portion of the URL.

  8. Hashes (#) ought to be disallowed but this is technically hard! Normally URLs can contain hashes and the browser shows them fine in the address bar, but they don't get passed to the server. So we don't have an easy way to reject them. There's a way to solve this — capturing anchor links — with client-side javascript and passing it to the server, which will be nice to implement later. Currently if you use a hash in a promise URL, we silently fail: The first occurrence of # and everything after it will be dropped from the URL the server sees.

  9. Question marks (?) are currently allowed but definitely deprecated. No promise URLs so far have question marks but it's slightly tricky for a route to reject them since they're not considered part of the URL path. Anything after the first question mark is technically the query string. (Another important problem with question marks: since our route matching doesn't see anything after the first question mark, all other characters that would normally be rejected will sneak through if they come anywhere after a question mark.)

  10. Ats (@) are currently allowed because there's one existing promise that uses one but it would be nice to reject these.

  11. Dots (.) are not allowed (UPDATE: allowed but deprecated; please don't use!). It's handy to disallow these because everything like robots.txt and apple-touch-icon-blahblah.png and various things that bots check for, sometimes maliciously, that we don't want to create promises for, have dots.

  12. Percents (%) are not allowed. If you use unicode characters or some special characters like ^ then browsers will percent-encode them, like replacing ^ with %5E. We could allow URLs with percent-encodings fine but if a user types a % in a URL that doesn't correspond to a valid percent-encoding then, well, the server freaks out and we need careful error-handling code to recover. But we're making it moot and rejecting anything with a percent in it. Whatever other characters we decide to allow, we should reject percents just because of the disparity between what the user typed and what the browser turns it into. In other words, if the URL contains a percent then we don't know if the user typed that or if they typed a special character that got percent-encoded. So that's a bad choice for a promise URL.

  13. Nothing else is allowed. Ampersands, exclamation marks, dollar signs, parentheses, etc etc, are all rejected. If you try to hit a URL with any of these characters you get a 404 page that also explains these rules.

Technical Quibble About Percent-Encodings
The above about percent-encoding is not quite true. There are some special characters that modern browsers consistently don't percent-encode. For example, en.wikipedia.org/wiki/Möbius_strip. So in theory it could be nice to allow those characters in promise URLs. As long as what the user typed and what the browser displays in the address bar are the same, then that's what we'd want as the urtext. If the browser changes what was typed — like with bob.commits.to/foo^bar — then we'd want to reject it. But this is a can of worms so we're going to disallow all non-ASCII characters. Which we do by just rejecting percent symbols because all non-ASCII characters get percent-encoded before being requested from the server.
Metaquibble
If you use a percent symbol in such a way that it happens to encode a normal alphanumeric character then browsers will helpfully undo the pointless percent-encoding. For example, if you type alice.commits.to/%41 in the address bar, what the server will get is alice.commits.to/A (because 41 is the code for "A"). So a promise will be created despite the user having typed a URL with a percent in it! There's really no solution to that other than to train users not to use special characters.

Legacy Migration For URLs with Slashes

Slashes are the only controversial case above, due to the 279 legacy URLs that currently use them. Here's what we're going to do: If a URL contains a slash, it will 404, but also in that case it will display a huge "Did You Mean" with a big link to the deslashed version of the URL.

The algorithm for deslashing is like so: Find the first dash or underscore in the urtext and call it sep. I.e., sep==='-' or sep==='_'. (One dumb special case: if the urtext starts with schedule-k_tax_thing then set sep to be an underscore.) Now simply globally replace slashes in the urtext with sep.

The migration plan is then as follows:

  1. Perform an update query to run the deslashing algorithm on all promises with slashes. If the deslashed version already exists though, do nothing. We'll manually clean those up.
  2. At the same time or on the heels of #1, make any route containing a slash serve up a 404 page.
  3. Include logic in the 404 rendering to add the "Did You Mean" if the urtext contains a slash but no other special character such as dots or question marks. With the deslashing algorithm as a function, we can just run it on the fly on the URL the user requested to dynamically generate the link to the deslashed version.
  4. After the "Did You Mean" link, add: "(Our URL format changed! Slashes are no longer valid. Please update your bookmarks and get with the program!)"

Note on URL format: We're waiting for the implementation and this section of the spec to match each other before accepting more beta users.

Promise Data Structure

The fundamental object in the commits.to app is of course the promise, aka the commitment. The following fields comprise a Promise object:

  • urtext: full original text (URL) the user typed to create the promise, also the primary key
  • user: who's making the promise, parsed as the subdomain in the URL
  • note: optional additional notes or context for the promise
  • tini: unixtime that the promise was made
  • tdue: unixtime that the promise is due, aka the deadline
  • tfin: unixtime that the promise was (fractionally) fulfilled (even if 0%)
  • xfin: fraction fulfilled, default null to indicate still pending
  • (firm: true when the due date is confirmed and can't be edited again )
  • void: true if the promise became unfulfillable or moot
  • clix: number of clicks a promise has gotten
  • (bmid: the id of the Beeminder datapoint for this promise )

(The ones in the parentheses we can ignore for the MVP.)

For example:

  • urtext = "bob.commits.to/Foo_the_bar_by_noon_tomorrow"
  • user = "bob"
  • note = "promised in slack discussion about such-and-such"
  • tini = [unixtime of first GET request of the promise's URL]
  • tdue = [what "noon tomorrow" parsed to at time tini]
  • tfin = [unixtime that the user marked the promise as fulfilled]
  • xfin = 0.5 [imagine the user decided to deem it half fulfilled]
  • firm = false
  • void = false
  • clix = 0
  • bmid = 4f9dd9fd86f22478d3000007

Currently we have two domains — commits.to and promises.to — and the latter simply redirects to the former. It's possible that in the future we'll want "bob.commits.to/foo" and "bob.promises.to/foo" to be distinct promises. If not, we may want to canonicalize a promise's key to strip out the domain. We could also choose to treat URLs as case-insensitive and store a canonicalized lowercase version. We could even choose to treat "foo-bar" and "foo_bar" as aliases to the same underlying promise. If we support username aliases, we could want aliases to canonicalize, e.g., "bobby.commits.to/foo" would redirect to "bob.commits.to/foo".

But currently we're assuming there will be no canonicalization of anything and all those questions are moot. In this spec we treat the original urtext as the primary key and refer to it interchangeably with the promise's URL as the unique identifier for a promise.

Here are some other ideas for fields, that we can worry about as the project evolves:

  • Public changelog for justifying things like changes to the due date
  • Whether the promise was created by the actual user (if they were logged in and were the first to click on it) or by another logged-in user or by someone not logged in
  • Information about the client (browser, geoIP, etc) that originally created the promise

Marking Promises Fulfilled

There are two ways to do this — basic and advanced.

In both cases the underlying fields are the same (see "Promise Data Structure"). And again, no logins or restrictions at all. Anyone hitting a promise's URL can edit anything any time.

(Exceptions: The user and URL fields won't be editable because they uniquely identify the promise and we don't want to have to validate uniqueness when submitting the form. See "For Later: Changing URLs".)

In both basic and advanced mode the user can edit the tdue field to pick any date/time as the deadline. Initially the due date is determined by parsing the URL (see "Parsing Dates and Promise Uniqueness") but the user has free rein to change it. Yes, it defeats the point if you can keep changing the deadline but for the MVP, honor system! We have ideas for later for how to further discourage cheating (see "For Later: Public Changelog").

Basic

Besides editing the due date, all the user gets in basic mode is a checkbox to mark the promise complete. Checking the box sets tfin to now and xfin to 1. Unchecking the box sets tfin and xfin both back to null.

Advanced

In advanced mode the user can edit all of the following fields:

In this case marking a promise (partially) fulfilled just means editing the xfin field and the tfin field to be non-null. Some combinations of tfin and xfin don't make sense so we'll consider each possibility:

Case 1: tfin null, xfin null

The promise is unfulfilled. This is the default state.

Case 2: tfin specified, xfin null

This combination doesn't make sense. We won't prohibit it but will show this on the page:

Error: Promise fulfilled at [tfin] but needs fraction fulfilled!

We also won't worry about tfin possibly being in the future, although that's also weird.

Case 3: tfin null, 0 <= xfin < 1

This is just the user treating xfin like a progress bar. "I haven't marked it done but I'm 75% of the way there!" If it's before the deadline then the isPending() function in "Computing Statistics" will count the promise as pending, meaning it won't count for or against your reliability score. If it's after the deadline then we optimistically assume you'll complete it in the next instant and show your remaining credit accordingly. (Again, see "Computing Statistics".)

Case 4: tfin null, xfin 1

Another combination that doesn't make sense. If you're 100% done then there must be a date that that happened. So show this on the page:

Warning: Promise marked done but needs completion date!

In "Computing Statistics" this is treated optimistically as if the promise will be completed in the next instant.

For later: Whenever anything about the promise changes it should be automatically mirrored in Beeminder (see "For Later: Beeminder Integration").

Parsing Dates and Promise Uniqueness

First, what should happen if alice says alice.commits.to/send_the_report_by_thu one week and then says alice.commits.to/send_the_report_by_fri the next week?

Answer: Promises are keyed only on the URL so those are entirely distinct promises. (This is pretty obvious now but was less so when we were using URLs like alice.commits.to/send_the_report/by/thu.) Which also means that if she uses exactly the same URL both weeks, the second time it will still resolve to the original promise, even if it's marked completed.

In practice it seems to be easy to make an unlimited number of unique names for promises and if there is a collision it's perfectly clear to the user why and what to do about it. Namely, make up a new URL! Later we can consider letting the user change the existing URL if they're ok with any links to the old promise pointing at the new one instead. But for the MVP, promise URLs are just necessarily unique.

What about the _by_ part of the URL? If the promise is first being created then we run it through a date parser and initialize the due date to whatever it says. If there's no _by_... part or we couldn't parse it as a date/time, tdue defaults to a week from now. If the promise already exists then the _by_ part doesn't matter. It will never override a tdue that's already set.

In short, the _by_... part of the URL is strictly advisory and can be changed by the user any time (see "Marking Promises Fulfilled").

Implementation Note
The Sherlock date parser is working well for this. It can be passed the whole urtext (with dashes and underscores replaced with spaces) and pick out the date fine. So the word "by" doesn't have to appear. For example, "reply in 2 weeks" or even "file the report march 30" are fine.

Late Penalties

Implementation Note
The implementation of this is written and tested. Nothing left to do here!

A big part of commits.to is tracking how reliable you are. Namely, what fraction of the promises you logged did you actually fulfill? And there's a fun twist: if you fulfill a promise late you get partial credit. That way we can always compute a single metric for your reliability at any moment in time.

The function we're using for late penalties is below. The idea is to have your reliability decrease strictly monotonically the moment the deadline hits, with sudden drops when you're a minute, an hour, a day, etc, late. Here's a plot of that function — technically the fraction of credit remaining as a function of lateness — first zoomed in to the first 60some seconds, and then zoomed out further and further:

For example, credit(0) is 1 (no penalty) and credit(3600) is 0.999 (most of the credit for being just an hour late).

See "Computing Statistics" for how to actually use this in the app or read on for more on why we like this weirdo function.

Rationale for the Crazy Late Penalty Function

There are a few key constraints on the shape of this function:

1. Strict monotonicity

Being strictly monotone means that you always see your reliability score visibly ticking down second by second whenever you have an overdue promise.

2. Asymptotically approaches zero

Approaching but never reaching zero just means you'll always get some epsilon of credit for fulfilling a promise no matter how late you are.

3. Sudden drops at Schelling fences

The third constraint is for beehavioral-economic reasons. We don't want you to feel like, once you've missed the deadline, that another hour or day or week won't matter. That's a slippery slope to never finishing ever. So the second-order discontinuities work like this: If you miss the nominal deadline your credit drops to 99.999% within seconds. The next sudden drop is at the 1-minute mark. Being within 60 seconds of the deadline is noticeably better than being 61 seconds late. After that you can still get 99.9% credit if you're less than an hour late. And if you miss that, you can still get 99% credit if you're less than a day late. At 24 hours the credit drops again to 90%, etc. A minute, an hour, a day, a week, a month, all the way up to the one-year anniversary of the deadline. If you hit that then you still get 10% credit. After that it drops pretty quickly to 1% and asymptotically approaches 0%, without ever reaching it.

In short, we're taking advantage of the following Schelling Fences:

  1. The deadline itself
  2. One minute late
  3. One hour late
  4. One day (24 hours) late
  5. One week late
  6. One month (365.25/12 days) late
  7. One year late

Computing Statistics

We'll care about the following statistics initially:

  1. Each promise's late penalty (0% if not yet late)
  2. Each promise's max credit (100% if not yet late)
  3. Each user's total number of promises made
  4. Each user's total number of promises pending
  5. Each user's overall reliability score

Individual Promises

The relevant fields (see "Promise Data Structure") are:

  • xfin — a promise's fulfilled fraction, between 0 and 1, default 0
  • tfin — when the promise was (fractionally) fulfilled
  • tdue — promise's deadline

And we'll assume we can get the current unixtime in seconds with a now() function. See "Late Penalties" where we define the credit() function for how much credit you get for a promise as a function of how late you fulfill it. Here we optimistically assume that any promise you're late on you're going to fulfill in the next instant.

Implementation Note
In Javascript you get current unixtime in seconds with Date.now()/1000 (just Date.now() returns it in milliseconds).

For a specific promise, displayed prominently at the promise's URL, we compute the optimistic late penalty (fraction of credit lost so far by being late) and max credit (xfin minus the late penalty so far) as follows. First a handy function to compute the most possible credit (least late penalty) a promise will get, expressed as a fraction in (0,1]:

// The most possible credit (least late penalty) a promise p can have
function rosycredit(p) {
  if (p.tdue === null) { return 1 }
  const ot = (p.tfin === null ? now() : p.tfin)  // optimistic tfin is now
  return credit(ot - p.tdue)
}

And then the key numbers to show in the UI:

latepen = 1 - rosycredit(p) // show as "#{latepen*100}%" in the UI
maxcred = (xfin === null ? 1 : xfin) * (1 - latepen) // also show as percent

The late penalty and max credit will change in real time for a pending promise that's past its deadline, and will update instantly when xfin or tfin change.

See "Marking Promises Fulfilled" for handling the cases that xfin is specified but not tfin or vice versa, but the above code is robust to that and just always shows the most optimistic numbers.

(To be clear, if, say, xfin is 50% and tfin is 2017-10-31 that isn't meant like a progress meter — "promise is 50% complete as of the 31st" — though the user could manually treat it that way. The idea is to treat the promise as being as done as it's going to get on Oct 31 and the credit you're getting is 50% of what you'd normally get. No optimism about an xfin of 50%, only an xfin that's null. So you multiply that 50% by whatever the credit function says based on how much after the due date Oct 31 is and that's your max credit.)

User's Overall Reliability

For the overall reliability score for a user, we assume unfulfilled promises that are still pre-deadline don't count for or against you. We call those pending promises, where there's still time to get full credit:

// A promise p is pending if it's pre-deadline and not marked totally done
function isPending(p) {
  return (p.tdue === null || now() < p.tdue) && p.xfin !== 1
}

And we optimistically assume that any promise you're late on you're going to fulfill in the next instant. So a brute force implementation would iterate through a user's promises like so:

let pending = 0
let numerator = 0
let denominator = 0
user.promises.forEach(p => {
  if (isPending(p)) { pending++ }
  else {
    numerator += (p.xfin === null ? 1 : p.xfin) * rosycredit(p)
    denominator += 1
  }
})

That's it! Now you can report that the user has made {denominator+pending} promises (of which {pending} are still in the future) and has a reliability of {denominator === 0 ? 0 : numerator/denominator*100}%.

The user's overall realtime reliability score should be shown prominently next to the username wherever it appears or huge in the header or something. It's the most important number in the whole app. Especially cool is how it will tick down in real time when one of your deadlines passes. (We recommend React for having numbers like that always updated in real time.)

Color-coding

Taking inspiration from Beeminder:

  • Gray: completed promises
  • Hot pink or showing flames or something cute: overdue promises
  • Red: deadline in less than 24 hours
  • Orange: deadline in less than 48 hours
  • Blue: deadline in less than 72 hours
  • Green: deadline in more than 72 hours

Calendar Integration

This is important enough and easy enough to be part of even the initial MVP. Namely, for each promise, create a link the user can click on to add it to their Google calendar. Like this:

Just view the html for that button here to see how that's constructed. (Source: StackOverflow answer.) For the event text, use the part of the URL after "commits.to/". For the event details: the whole URL. And for both the start and end date of the calendar event: the promise's deadline. No Calendar API is needed that way — just construct the link and if the user is logged in to Google it will create the calendar entry when they click it and confirm.


Credits

Daniel Reeves wrote a blog post about the idea. Sergii Kalinchuk got the "promises.to" domain and has it redirecting to commits.to. Marcin Borkowski had the idea for URLs-as-UI for creating promises. Chris Butler implemented most of the MVP.




For Later: Beeminder Integration

This is the first thing we'd like to add after the MVP spec'd above! I think it would even make sense to say that the only way to log in to commits.to is via oauth with your Beeminder account.

The idea for the integration is to send a datapoint to Beeminder for each promise you make. A Beeminder datapoint consists of a date, a value, and a comment. Beeminder plots those cumulatively on a graph for you and lets you hard-commit to a certain rate of progress.

There are two ways we could do the integration. We'll first implement a simple way and then consider a more advanced way.

Simple Beeminder Integration

  1. Create a standard Do More goal for the user on Beeminder or ask the user for the goalname of an existing goal. The rate that the user is (meta) committing to should be 3 promises per week.
  2. Simply send a +1 to that Beeminder goal every time a new promise is created.
  3. The datapoint's comment should just have the promise's URL since that's a link to all the data about a promise.

Advanced Beeminder Integration

The simple version of the integration just has the user committing to making some number of commits.to URLs per week, regardless of how many are fulfilled.

The advanced version has the beemind their total number of successes, where fractional successes count fractionally.

Specifically, the date on the Beeminder datapoint is the promise's completion date, if non-null, otherwise the deadline, tdue (even though it's in the future). And the value of the Beeminder datapoint is the fulfilled fraction, xfin (initially zero). As in the simple version, the datapoint's comment should just contain the promise's URL. Or something like "Auto-added by commits.to at 12:34pm -- " and then the URL. (It's nice to use the timezone the user has set in Beeminder — available in the User resource in the Beeminder API — when showing a time of day.)

The Beeminder goal should be a do-more goal to fulfill, say, 8 promises per week. The way I (dreev) do this currently: I create a datapoint for each promise (via IFTTT from Google Calendar) when I promise it, and then change the datapoint to a 1 when I fulfill it (or something less than 1 if I fulfill it late).

So Beeminder is not enforcing a success rate, just an absolute number of successes.

Pro tip: Promise a friend some things from your to-do list that you could do any time. That way you're always ready for an I-will beemergency. (But if your Personal Rule for commits.to is that only natural utterances of "I will" count as loggable commitments then making contrived promises like that may be cheating.)

The commits.to app's interactions with Beeminder (via Beeminder API calls) are as follows:

  1. When a promise is created, create a datapoint
  2. When a promise is marked (partially) fulfilled, update the datapoint's value
  3. When a promise's due date changes, update the datapoint's date
  4. [LATER] When a promise is deleted, delete the datapoint
  5. [LATER] When a promise is voided maybe also delete the datapoint in Beeminder
  6. [LATER] Create the initial Beeminder goal when a user signs up for commits.to

For Later: Public Changelog

I think this is the most elegant and flexible solution to prevent cheating. You can change anything at any time but you have to publicly justify each change and it's all permanently displayed on the promise's page as an audit log.

For example:

  • 2017-10-31: due date changed from 11/14 to 11/30 with comment "the original promise URL didn't specify a date and defaulted to a week out but the end of the month is what I had in mind"
  • 2017-11-01: creation date changed from 10/31 to 10/30 with comment "the promise was made verbally the previous day"
  • 2017-11-28: completion date changed from null to 11/28 (promise marked complete)
  • 2017-11-28: completion date changed from 11/28 to null with comment "accidentally marked this complete too soon!"
  • 2017-11-29: the void field changed from false to true with comment "just kidding, the lawyers say we can't do this"

Some people will do things like "giving myself an extra day because my cat got sick" which completely defeats the point of the whole system (even for entirely unimpeachable excuses it defeats the point, unless you explicitly make the deadline conditional in the first place) but by having to make those justifications publicly you can see when someone is doing that and discount their supposed reliability percentage accordingly. I mean, people can cheat and game this in a million ways anyway so no restrictions we try to impose will ever really solve this kind of problem.

(An alternative we were hashing out before was allowing you to edit the due date exactly once in case the system initially parsed it wrong, or you just didn't specify a deadline in the URL. I'm all for being super opinionated about things like not letting you edit deadlines but the public append-only changelog idea seems most general and flexible. In the meantime we can voluntarily log changes in the note field.)

For Later: Future Discounting

If you were late in the past but are always on time now, your past sins should fade over time. In other words, we should apply a discount rate to reliability scores. Let's declare 6% per year to be reasonable. So set a constant R = 0.06 as well as SIY = 31557600 for seconds in a year because we'll need the discount rate per second.

So instead of summing up the scores for the promises and dividing by how many there are, we take a weighted average of the scores. A score's weight is exp(-R*a/SIY) where a is the age of that promise. (Note that if a promise's age is zero then its weight is 1, and it takes about 12 years for a promise to lose half its weight.) We can compute the age of a promise like so:

// Return seconds elapsed since a promise's most recent milestone, where
// milestones include the promise being made, being due, & being fulfilled.
// If any of those are in the future, then the age is zero.
function age(p) {
  const t = Math.max(p.tini, p.tdue, p.tfin)
  return Math.max(0, now() - t)
}

So we just multiply the scores by their weights, sum them up, and divide by the sum of the weights. Easy peasy.

(Note that 6% per year may take a long time to be perceptible. We could also try 36% per year — the basis of Beeminder's Exquisitely Fair Pre-Pay Discounts.)

For Later: Account Settings

  1. Username, used as a subdomain for the URL
  2. Beeminder access token
  3. Timezone (needed to parse the deadlines; but less important since you can change the deadline if it's misparsed)

Even Later:

  1. Pronoun (default "they/them/their/theirs")
  2. Display name, e.g., "Alice" as opposed to username "alice"

For Later: Humanized Promise Names From URLs

For the MVP we just want to use the descriptions in the URL as given. At most we can apply a humanize() function to them when displaying the promise on the page that could, for example, replace underscores with spaces. Or try to be smart and turn "do-the-thing" into "do the thing" but also display "do_things_1-3" as "do things 1-3" and not "do things 1 3". It's a can of worms so for the MVP we should pick something very simple and only do it in the display logic.

For Later: Calendar as UI

This is totally at odds with the current spec but before we had the URLs-as-UI idea we thought you'd create promises by creating calendar entries and use the calendar API to automatically capture those.

There are various ways to add calendar entries with very low friction already. Then that would need to automatically trigger promises.to to capture each calendar entry. (I'm doing that now with IFTTT to send promises to Beeminder.)

And maybe it'd be fine for every calendar entry to get automatically added. Some of them wouldn't be promises but that's fine — you could just mark them as non-promises or delete them and they wouldn't count. If they were promises then you'd need to manually mark them as fulfilled or not. Beeminder (plus the embarrassment of having your reliability percentage drop when a deadline passes) would suffice to make sure you remember to do that.

Again, this is moot while we work on the URL-as-UI version.

For Later: Security and Privacy

Alice's friends can troll her by making up URLs like alice.commits.to/kick_a_puppy but that's not a huge concern. Alice, when logged in, could have to approve promises to be public. So the prankster would see a page that says Alice promises to kick a puppy but no one else would.

In the MVP we can skip the approval UI and worry about abuse like that the first time it's a problem, which I predict will be after commits.to is a million dollar company.

For Later: Active vs Inactive Promises

Define a promise to be inactive if its tfin and tdue dates are both non-null and in the past. So even if a promise is done early it's still active till the due date, and even if it's overdue it's still active till it's done. (Or "done" — it could be marked 0% fulfilled.)

We might want to display active and inactive promises differently.

For Later: Changing URLs

I sometimes dash out a promise URL on my phone but later would prefer a better URL. Maybe I'm sure no one is going to click on the original one (I continue to be surprised how infrequently people click on these URLs, especially once the novelty wears off) and would like to just change it and let the original link break. Ok, you shouldn't ever just assume your promisee won't click the link but maybe you explicitly gave the new, better one. Or maybe you only made the promise verbally and logged it directly via the address bar of your browser.

Or maybe alice.commits.to/send_the_report was completed and everyone knows it and now you want to promise to send_the_report again. The most un-can-of-worms-y way to do that is to rename the old promise via a convention like alice.commits.to/send_the_report-old or alice.commits.to/archived:send_the_report and then just start over with alice.commits.to/send_the_report like usual.

(That could even be an Official Convention so that any promise page for "foo_by_soon" would look up and display links to any "archived:1:foo_by_soon", "archived:2:foo_by_soon", etc promises at the bottom, saying "looking for one of these previous incarnations of this promise?". The UI could help too: maybe promises have an archive button which replaces the path part of the URL "foo_by_soon" with "archived:1:foo_by_soon", or "archived:2:foo_by_soon" if there's already a ":1:", etc. So you hit archive and then any old links to the promise will create a new promise but with a pointer to the archived version.)

Whatever the reason, you sometimes want to change a promise's URL. We could just let you do that, showing in real time as you edit it whether the new URL is taken. If it's not taken then let the user hit submit.

I think that will be worth implementing soon after the MVP. At least the renaming, if not the whole archiving/reusing convention.

Finally, some pie in the sky for later still: What if the user could somehow add 301-redirects willy-nilly? Then you could change URLs without breaking links.

For Later: Similar/Related Promises

On every promise page we can link to the 3 promises with the most similar URLs. PostgreSQL has built in functions for this.

For Posterity: Domain Name Ideas

  • dreev.es/will/ (for anyone who has a domain for their own name)
  • alice.promises.to/ (sergii grabbed this one)
  • alice.commits.to/ (dreev grabbed this one)
  • alice.willdefinite.ly/ (kinda awkward)
  • alice.willveri.ly/ (too cutesy?)
  • alice.willprobab.ly/ (emphasizes the reliability percentage)
  • alice.willresolute.ly (maybe it would grow on me?)

A possibly silly idea: we currently have "promises.to" and "commits.to" which are pretty synonomous but if we had other domains, that could maybe affect the reliability score. Like "promising" is one thing but if it's alice.intends.to (not that we have that domain) then maybe it doesn't fully count against you if you don't actually do it. Also if we made this work for people's personal domain names, like dreev.es/will, then we could have arbitrary verbs — dreev.es/might, etc. So maybe verb would make sense as one of the promise data structure fields in the future?

FAQ

Is it fair that my reliability seems lower only because I commit to harder things? Or: Is it fair to inflate your supposed reliability by only ever committing to easy things?

The rule I use is that if I utter "I will" or "I'll" or "I'm going to" then I must log a commitment. It doesn't matter how trivial. The only thing the reliability score is meant to measure is "of the times you say you'll do something, what fraction of the time do you do it?". If you make sure to only say you'll do easy things and get a 100%, that's perfectly allowed. If you want to get yourself to do more hard things, you probably want to use Beeminder. Commits.to is just about ensuring that your words match your actions. If you do that purely by adjusting your words, that's still a win. But you may find that commits.to is a powerful commitment device too, once you have a reliability score you're invested in.

Maybe it still feels like apples and oranges if you commit to very different things than someone else. But the important comparison isn't between different people. It's from the perspective of the person you're committing to right now, to compare with your commitments in the past. When Alice tells Bob, "I will do the thing", Bob can see, empirically, how much he can count on Alice for the thing.

Can I create commitments to myself? Maybe even make this my to-do list?

We're not the boss of you, but it's a bad idea. It gives you too much flexibility to shape your reliability score beyond its meaning for the people you're making "I will" statements to. When Bob sees a commitment URL from Alice, her reliability should tell him "of all commitments like this that she's made to people, how often does she follow through?" If it's combined with self-commitments, it's not really telling him that.

It's not just about goosing your numbers with easy commitments. Probably you're more likely to flake out on commitments made only to yourself. So by including those you underestimate your reliability when committing to others.

So, no, Commits.to is not a to-do list. It's just for tracking explicit commitments you've made.

UPDATE Dec 2022: The app is no longer hosted on Heroku at the domain commits.to. @kingdonb is hosting it on Kubernetes, with bit.io as the database backend!

Clone this wiki locally